接口方法上的注解无法被@Aspect声明的切面拦截的原因分析

转载自  接口方法上的注解无法被@Aspect声明的切面拦截的原因分析

前言

在Spring中使用MyBatis的Mapper接口自动生成时,用一个自定义的注解标记在Mapper接口的方法中,再利用@Aspect定义一个切面,拦截这个注解以记录日志或者执行时长。但是惊奇的发现这样做之后,在Spring Boot 1.X(Spring Framework 4.x)中,并不能生效,而在Spring Boot 2.X(Spring Framework 5.X)中却能生效。

这究竟是为什么呢?Spring做了哪些更新产生了这样的变化?此文将带领你探索这个秘密。

 

案例

核心代码

@SpringBootApplicationpublic class Starter {public static void main(String[] args) {SpringApplication.run(DynamicApplication.class, args);}}@Servicepublic class DemoService {@AutowiredDemoMapper demoMapper;public List<Map<String, Object>> selectAll() {return demoMapper.selectAll();}}/*** mapper类*/@Mapperpublic interface DemoMapper {@Select("SELECT * FROM demo")@DemoList<Map<String, Object>> selectAll();}/*** 切入的注解*/@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Demo {String value() default "";}/*** aspect切面,用于测试是否成功切入*/@Aspect@Order(-10)@Componentpublic class DemoAspect {@Before("@annotation(demo)")public void beforeDemo(JoinPoint point, Demo demo) {System.out.println("before demo");}@AfterDemo("@annotation(demo)")public void afterDemo(JoinPoint point, Demo demo) {System.out.println("after demo");}}

 

测试类

@RunWith(SpringRunner.class) @SpringBootTest(classes = Starter.class)public class BaseTest {@AutowiredDemoService demoService;@Testpublic void testDemo() {demoService.selectAll();} }

在Spring Boot 1.X中,@Aspect里的两个println都没有正常打印,而在Spring Boot 2.X中,都打印了出来。

 

调试研究

已知@Aspect注解声明的拦截器,会自动切入符合其拦截条件的Bean。这个功能是通过@EnableAspectJAutoProxy注解来启用和配置的(默认是启用的,通过AopAutoConfiguration),由@EnableAspectJAutoProxy中的@Import(AspectJAutoProxyRegistrar.class)可知,@Aspect相关注解自动切入的依赖是AnnotationAwareAspectJAutoProxyCreator这个BeanPostProcessor。在这个类的postProcessAfterInitialization方法中打上条件断点:beanName.equals(“demoMapper”)

public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {if (bean != null) {// 缓存中尝试获取,没有则尝试包装Object cacheKey = getCacheKey(bean.getClass(), beanName);if (!this.earlyProxyReferences.contains(cacheKey)) {return wrapIfNecessary(bean, beanName, cacheKey);}}return bean;}

在wrapIfNecessary方法中,有自动包装Proxy的逻辑:

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {// 如果是声明的需要原始Bean,则直接返回if (beanName != null && this.targetSourcedBeans.contains(beanName)) {return bean;}// 如果不需要代理,则直接返回if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {return bean;}// 如果是Proxy的基础组件如Advice、Pointcut、Advisor、AopInfrastructureBean则跳过if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {this.advisedBeans.put(cacheKey, Boolean.FALSE);return bean;}// Create proxy if we have advice.// 根据相关条件,查找interceptor,包括@Aspect生成的相关Interceptor。// 这里是问题的关键点,Spring Boot 1.X中这里返回为空,而Spring Boot 2.X中,则不是空Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);if (specificInterceptors != DO_NOT_PROXY) {// 返回不是null,则需要代理this.advisedBeans.put(cacheKey, Boolean.TRUE);// 放入缓存Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));// 自动生成代理实例this.proxyTypes.put(cacheKey, proxy.getClass());return proxy;}this.advisedBeans.put(cacheKey, Boolean.FALSE);return bean;}

调试发现,Spring Boot 1.X中specificInterceptors返回为空,而Spring Boot 2.X中则不是空,那么这里就是问题的核心点了,查看源码:

protected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, TargetSource targetSource) {List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);if (advisors.isEmpty()) {// 如果是空,则不代理return DO_NOT_PROXY;}return advisors.toArray();}protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {// 找到当前BeanFactory中的AdvisorList<Advisor> candidateAdvisors = findCandidateAdvisors();// 遍历Advisor,根据Advisor中的PointCut判断,返回所有合适的AdvisorList<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);// 扩展advisor列表,这里会默认加入一个ExposeInvocationInterceptor用于暴露动态代理对象,之前文章有解释过extendAdvisors(eligibleAdvisors);if (!eligibleAdvisors.isEmpty()) {// 根据@Order或者接口Ordered排序eligibleAdvisors = sortAdvisors(eligibleAdvisors);}return eligibleAdvisors;}protected List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> beanClass, String beanName) {ProxyCreationContext.setCurrentProxiedBeanName(beanName);try {// 真正的查找方法  return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass);}finally {ProxyCreationContext.setCurrentProxiedBeanName(null);}}

这里的核心问题在于AopUtils.findAdvisorsThatCanApply方法,这里的返回在两个版本是不一样的,由于这里代码过多就不贴上来了,说明下核心问题代码是这段:

// AopProxyUtils.javapublic static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {// ... 省略for (Advisor candidate : candidateAdvisors) {if (canApply(candidate, clazz, hasIntroductions)) {eligibleAdvisors.add(candidate);}}// ... 省略}public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) {if (advisor instanceof IntroductionAdvisor) {return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass);}else if (advisor instanceof PointcutAdvisor) {// 对于@Aspect的切面,是这段代码在生效PointcutAdvisor pca = (PointcutAdvisor) advisor;return canApply(pca.getPointcut(), targetClass, hasIntroductions);}else {// It doesn't have a pointcut so we assume it applies.return true;}}

基本定位了问题点,看下最终调用的canApply方法,Spring Boot 1.X与2.X这里的代码是不一样的

Spring Boot 1.X中源码,即Spring AOP 4.X中源码

/*** targetClass是com.sun.proxy.$Proxy??即JDK动态代理生成的类* hasIntroductions是false,先不管*/public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {Assert.notNull(pc, "Pointcut must not be null");// 先判断class,这里两个版本都为trueif (!pc.getClassFilter().matches(targetClass)) {return false;}MethodMatcher methodMatcher = pc.getMethodMatcher();// 如果method是固定true,即拦截所有method,则返回true。这里当然为falseif (methodMatcher == MethodMatcher.TRUE) {// No need to iterate the methods if we're matching any method anyway...return true;}// 特殊类型,做下转换,Aspect生成的属于这个类型IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;if (methodMatcher instanceof IntroductionAwareMethodMatcher) {introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;}// 取到目标class的所有接口Set<Class<?>> classes = new LinkedHashSet<Class<?>>(ClassUtils.getAllInterfacesForClassAsSet(targetClass));// 再把目标calss加入遍历列表classes.add(targetClass);for (Class<?> clazz : classes) {Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);// 遍历每个类的每个方法,尝试判断是否matchfor (Method method : methods) {if ((introductionAwareMethodMatcher != null &&introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) ||methodMatcher.matches(method, targetClass)) {return true;}}}return false;}

Spring Boot 2.X中源码,即Spring AOP 5.X中源码

public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {Assert.notNull(pc, "Pointcut must not be null");if (!pc.getClassFilter().matches(targetClass)) {return false;}MethodMatcher methodMatcher = pc.getMethodMatcher();if (methodMatcher == MethodMatcher.TRUE) {// No need to iterate the methods if we're matching any method anyway...return true;}IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;if (methodMatcher instanceof IntroductionAwareMethodMatcher) {introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;}Set<Class<?>> classes = new LinkedHashSet<>();// 这里与1.X版本不同,使用Jdk动态代理Proxy,先判断是否是Proxy,如果不是则加入用户Class,即被动态代理的class,以便查找真正的Class中是否符合判断条件// 因为动态代理可能只把被代理类的方法实现了,被代理类的注解之类的没有复制到生成的子类中,故要使用原始的类进行判断// JDK动态代理一样不会为动态代理生成类上加入接口的注解// 如果是JDK动态代理,不需要把动态代理生成的类方法遍历列表中,因为实现的接口中真实的被代理接口。if (!Proxy.isProxyClass(targetClass)) {classes.add(ClassUtils.getUserClass(targetClass));}classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass));for (Class<?> clazz : classes) {Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);for (Method method : methods) {// 比1.X版本少遍历了Proxy生成的动态代理类,但是遍历内容都包含了真实的接口,其实是相同的,为什么结果不一样呢?if ((introductionAwareMethodMatcher != null &&introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) ||methodMatcher.matches(method, targetClass)) {return true;}}}return false;}

调试信息图

 

上面的代码执行结果不同,但是区别只是少个动态代理生成的类进行遍历,为什么少一个遍历内容结果却是true呢?肯定是introductionAwareMethodMatcher或者methodMatcher的逻辑有改动,其中methodMatcher和introductionAwareMethodMatcher是同一个对象,两个方法逻辑相同。看代码:

/** AspectJExpressionPointcut.java* method是上面接口中遍历的方法,targetClass是目标class,即生成的动态代理class*/public boolean matches(Method method, @Nullable Class<?> targetClass, boolean beanHasIntroductions) {obtainPointcutExpression();Method targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);ShadowMatch shadowMatch = getShadowMatch(targetMethod, method);// Special handling for this, target, @this, @target, @annotation// in Spring - we can optimize since we know we have exactly this class,// and there will never be matching subclass at runtime.if (shadowMatch.alwaysMatches()) {return true;}else if (shadowMatch.neverMatches()) {return false;}else {// the maybe caseif (beanHasIntroductions) {return true;}// A match test returned maybe - if there are any subtype sensitive variables// involved in the test (this, target, at_this, at_target, at_annotation) then// we say this is not a match as in Spring there will never be a different// runtime subtype.RuntimeTestWalker walker = getRuntimeTestWalker(shadowMatch);return (!walker.testsSubtypeSensitiveVars() ||(targetClass != null && walker.testTargetInstanceOfResidue(targetClass)));}}

这段代码在Spring Boot 1.X和2.X中基本是相同的,但是在AopUtils.getMostSpecificMethod(method, targetClass);这一句的执行结果上,两者是不同的,1.X返回的是动态代理生成的Class中重写的接口中的方法,2.X返回的是原始接口中的方法。

而在动态代理生成的Class中重写的接口方法里,是不会包含接口中的注解信息的,所以Aspect中条件使用注解在这里是拿不到匹配信息的,所以返回了false。

而在2.X中,因为返回的是原始接口的方法,故可以成功匹配。

问题就在于AopUtils.getMostSpecificMethod(method, targetClass)的逻辑:

// 1.Xpublic static Method getMostSpecificMethod(Method method, Class<?> targetClass) {// 这里返回了targetClass上的重写的method方法。Method resolvedMethod = ClassUtils.getMostSpecificMethod(method, targetClass);// If we are dealing with method with generic parameters, find the original method.return BridgeMethodResolver.findBridgedMethod(resolvedMethod);}// 2.Xpublic static Method getMostSpecificMethod(Method method, @Nullable Class<?> targetClass) {// 比1.X多了个逻辑判断,如果是JDK的Proxy,则specificTargetClass为null,否则取被代理的Class。Class<?> specificTargetClass = (targetClass != null && !Proxy.isProxyClass(targetClass) ?ClassUtils.getUserClass(targetClass) : null);// 如果specificTargetClass为空,直接返回原始method。// 如果不为空,返回被代理的Class上的方法Method resolvedMethod = ClassUtils.getMostSpecificMethod(method, specificTargetClass);// If we are dealing with method with generic parameters, find the original method.// 获取真实桥接的方法,泛型支持return BridgeMethodResolver.findBridgedMethod(resolvedMethod);}

至此原因已经完全明了,Spring在AOP的5.X版本修复了这个问题。

 

影响范围

原因已经查明,那么根据原因我们推算一下影响范围

  1. Bean是接口动态代理对象时,且该动态代理对象不是Spring体系生成的,接口中的切面注解无法被拦截

  2. Bean是CGLIB动态代理对象时,该动态代理对象不是Spring体系生成的,原始类方法上的切面注解无法被拦截。

  3. 可能也影响基于类名和方法名的拦截体系,因为生成的动态代理类路径和类名是不同的。

如果是Spring体系生成的,之前拿到的都是真实类或者接口,只有在生成动态代理后,才是新的类。所以在创建动态代理时,获取的是真实的类。

接口动态代理多见于ORM框架的Mapper、RPC框架的SPI等,所以在这两种情况下使用注解要尤为小心。

有些同学比较关心@Cacheable注解,放在Mapper中是否生效。答案是生效,因为@Cacheable注解中使用的不是@Aspect的PointCut,而是CacheOperationSourcePointcut,其中虽然也使用了getMostSpecificMethod来获取method,但是最终其实又从原始方法上尝试获取了注解:

// AbstractFallbackCacheOperationSource.computeCacheOperationsif (specificMethod != method) {//  Fallback is to look at the original methodopDef = findCacheOperations(method);if (opDef != null) {return opDef;}// Last fallback is the class of the original method.opDef = findCacheOperations(method.getDeclaringClass());if (opDef != null && ClassUtils.isUserLevelMethod(method)) {return opDef;}}

看似不受影响,其实是做了兼容。

可以参考后面的内容,有提到Spring相关的issue

 

解决方案

如何解决这个问题呢?答案是在Spring Boot 1.X中没有解决方案。。因为这个类太基础了,除非切换版本。

使用其他Aspect表达式也可以解决此问题,使用注解方式在1.X版本是无解的。

表达式参考如下链接:

  1. Spring 之AOP AspectJ切入点语法详解(最全面、最详细。)

    https://blog.csdn.net/zhengchao1991/article/details/53391244

  2. Spring Aspect的Execution表达式

    https://blog.csdn.net/lang_niu/article/details/51559994

本来以为在注解Demo中加入@Inherited可解决的,结果发现不行,因为这个@Inherited只在类注解有效,在接口中或者方法上,都是不能被子类或者实现类继承的,看这个@Inherited上面的注释

/*** Indicates that an annotation type is automatically inherited.  If* an Inherited meta-annotation is present on an annotation type* declaration, and the user queries the annotation type on a class* declaration, and the class declaration has no annotation for this type,* then the class's superclass will automatically be queried for the* annotation type.  This process will be repeated until an annotation for this* type is found, or the top of the class hierarchy (Object)* is reached.  If no superclass has an annotation for this type, then* the query will indicate that the class in question has no such annotation.** <p>Note that this meta-annotation type has no effect if the annotated* type is used to annotate anything other than a class.  Note also* that this meta-annotation only causes annotations to be inherited* from superclasses; annotations on implemented interfaces have no* effect.* 上面这句话说明了只在父类上的注解可被继承,接口上的都是无效的** @author  Joshua Bloch* @since 1.5*/@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.ANNOTATION_TYPE)public @interface Inherited {}

 

扩展阅读

问题及可能的影响范围已经详细分析完了,下面我们好奇一下,这个核心问题类AopUtils.java的提交记录中,作者有写什么吗

AopUtils.java类GitHub页面

https://github.com/spring-projects/spring-framework/blob/master/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java

查看这个类的历史记录,注意Commits on Apr 3, 2018这个日期的提交,其中提到:

Consistent treatment of proxy classes and interfaces for introspection

Issue: SPR-16675

Issue: SPR-16677

针对proxy classes做了内省配置,相关issue是SPR-16677,我们看下这个issue。

Spring Framework/SPR-16677

https://jira.spring.io/browse/SPR-16677

这个issue详细描述了这次提交的原因及目的。

读者感兴趣的话可以详细的阅读。

注意AopUtils.java的最新提交,又做了一些优化,可以研究一下。

 

扩展知识

上面的示例代码依赖于数据库,现做一个模拟Mapper类的改进,可以直接无任何依赖的重现该问题:

已知Mybatis的Mapper接口是通过JDK动态代理生成的逻辑,而Mapper接口相关的Bean生成,是通过AutoConfiguredMapperScannerRegistrar自动注册到BeanFactory中的,注册进去的是MapperFactoryBean这个工厂Bean类型。

而MapperFactoryBean的getObject方法,则是通过getSqlSession().getMapper(this.mapperInterface)生成的,mapperInterfact是mapper接口。

底层是通过Configuration.getMapper生成的,再底层是mapperRegistry.getMapper方法,代码如下

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);if (mapperProxyFactory == null) {throw new BindingException("Type " + type + " is not known to the MapperRegistry.");}try {// 调用下面的方法生成代理实例return mapperProxyFactory.newInstance(sqlSession);} catch (Exception e) {throw new BindingException("Error getting mapper instance. Cause: " + e, e);}}public T newInstance(SqlSession sqlSession) {// 创建MapperProxy这个InvocationHandler实例final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);return newInstance(mapperProxy);}protected T newInstance(MapperProxy<T> mapperProxy) {// 调用jdk动态代理生成实例,代理的InvocationHandler是MapperProxyreturn (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}

可以看到底层是通过JDK动态代理Proxy生成的,InvocationHandler是MapperProxy类。

清楚原理之后,我们对上面的实例做下改造,把Mybatis的引用简化。

@Configurationpublic class DemoConfiguraion {@Beanpublic FactoryBean<DemoMapper> getDemoMapper() {return new FactoryBean<DemoMapper>() {@Overridepublic DemoMapper getObject() throws Exception {InvocationHandler invocationHandler = (proxy, method, args) -> {System.out.println("调用动态代理方法" + method.getName());return Collections.singletonList(new HashMap<String, Object>());};return (DemoMapper) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[] {DemoMapper.class}, invocationHandler);}@Overridepublic Class<?> getObjectType() {return DemoMapper.class;}@Overridepublic boolean isSingleton() {return true;}};}}

上面的代码可达到与Mapper同样的效果,大家可以本地随便玩哈。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/323620.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

springboot实现用户统一认证、管理(单点登录)

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂 前言 现在是&#xff1a;2022年5月25日13:44:16 最近和模拟登录杠上了&#xff0c;这不&#xff0c;又来了个需求&#xff0c;还是以这个技术点入手的。 需求大概是这样的&#xff1a;为了…

学习ASP.NET Core,怎能不了解请求处理管道[1]: 中间件究竟是个什么东西?

ASP.NET Core管道虽然在结构组成上显得非常简单&#xff0c;但是在具体实现上却涉及到太多的对象&#xff0c;所以我们在 “通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流程”&#xff08;上篇、中篇、下篇&#xff09; 中围绕着一个经过极度简化的模拟管道讲述…

springboot实现用户统一认证、管理

“大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂”前言现在是&#xff1a;2022年5月25日13:44:16最近和模拟登录杠上了&#xff0c;这不&#xff0c;又来了个需求&#xff0c;还是以这个技术点入手的。需求大概是这样的&#xff1a;为了统…

Mybatis 使用的 9 种设计模式,真是太有用了

转载自 Mybatis 使用的 9 种设计模式&#xff0c;真是太有用了 虽然我们都知道有26个设计模式&#xff0c;但是大多停留在概念层面&#xff0c;真实开发中很少遇到&#xff0c;Mybatis源码中使用了大量的设计模式&#xff0c;阅读源码并观察设计模式在其中的应用&#xff0c;…

springboot实现用户统一认证、管理-前端实现

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂 前言 现在是&#xff1a;2022年6月2日15:43:51 上篇文章讲述了springboot中实现用户统一认证的具体内容&#xff0c;主要从后端角度出发的&#xff0c;其实大部分功能还是前端与后端交互的…

Unity3damp;amp;C#分布式游戏服务器ET框架介绍-组件式设计

前几天写了《开源分享 Unity3d客户端与C#分布式服务端游戏框架》&#xff0c;受到很多人关注&#xff0c;QQ群几天就加了80多个人。开源这个框架的主要目的也是分享自己设计ET的一些想法&#xff0c;所以我准备写一系列的文章&#xff0c;介绍下自己的思路跟设计&#xff0c;每…

springboot+vue实现用户统一认证、管理-前端实现

“大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂”前言现在是&#xff1a;2022年6月2日15:43:51上篇文章讲述了springboot中实现用户统一认证的具体内容&#xff0c;主要从后端角度出发的&#xff0c;其实大部分功能还是前端与后端交互的…

JS中 [] == ![]结果为true,而 {} == !{}却为false, 追根刨底

转载自 JS中 [] ![]结果为true&#xff0c;而 {} !{}却为false&#xff0c; 追根刨底 console.log( [] ![] ) // true console.log( {} !{} ) // false 在比较字符串、数值和布尔值的相等性时&#xff0c;问题还比较简单。但在涉及到对象的比较时&#xff0c;问题就变…

Centos7 amp;amp; Docker amp;amp; Jenkins amp;amp; ASP.NET Core

写在前面 Docker一直很火热&#xff0c;一直想把原本的Jenkins自动部署工具搬到Docker上面&#xff0c;无奈今年一直忙于各种事情&#xff0c;迟迟未实施这个事情&#xff0c;正好迎来了dotnet core 2.0 的正式发布&#xff0c;升级项目的同时&#xff0c;顺便直接将Jenkins搬到…

国民体质测定标准手册及标准解析成JSON文件计算分数,java解析excel文件

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂 前言 现在是&#xff1a;2022年6月14日10:07:27 最近在做体质测评的功能&#xff0c;需要依据《国民体质测定标准手册及标准》&#xff0c;根据用户的个人信息&#xff0c;从而计算出各个…

getchar与putchar用法

#include<stdio.h>main(){int i;igetchar();//相当于char i;scanf("%c",&i); putchar(i);//相当于printf("%c",i); 需要i是字符才能输出不能是变量printf("\n");printf("%d",i);}输出结果一致 #include<stdio.h>main…

TCP为什么是三次握手和四次挥手

转载自 TCP为什么是三次握手和四次挥手 为什么建立连接是三次握手断开连接是四次挥手&#xff1f; 三次握手的流程和四次挥手的流程是什么&#xff1f; 三次握手与四次回收分别对应TCP连接与断开过程 tcp报文格式 标志位含义 ACK&#xff1a;确认序号有效。 SYN&#x…

HTM文件中使用vue

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂。 代码啊&#xff0c;尤其是比较重要客户的项目&#xff0c;即使包出去了&#xff0c;代码也一定要回到自己手里&#xff0c;不然干着急。 这个项目&#xff0c;已经经过两手了&#xff0c…

LVS三种模式的区别及负载均衡算法

转载自 LVS三种模式的区别及负载均衡算法 LVS简介 LVS&#xff08;Linux Virtual Server&#xff09;即Linux虚拟服务器&#xff0c;是一个虚拟的服务器集群系统&#xff0c;由章文嵩博士在1998年5月成立&#xff0c;在linux2.6后将lvs自动加入了kernel模块&#xff0c;我们…

王者荣耀是怎样炼成的(一)《王者荣耀》用什么开发,游戏入门,unity3D介绍

在国内&#xff0c;如果你没有听说过《王者荣耀》&#xff0c;那你一定是古董级的人物了。 《王者荣耀》&#xff08;以下简称“农药”&#xff09;&#xff0c;专注于移动端&#xff08;Android、IOS&#xff09;的MOBA游戏。笔者看到这么火爆&#xff0c;就萌生了了解一下这类…

新工作感悟~辞旧迎新~

“大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂”现在是&#xff1a;2022年6月21日22:33:34公众号又好久没有更新啦。从以前的日更&#xff0c;到后来的周更&#xff0c;再到后来的月更……不知道会不会到不更的结局。。。最近换工作了&…

关于Spring底层原理面试的那些问题,你是不是真的懂Spring?

转载自 关于Spring底层原理面试的那些问题&#xff0c;你是不是真的懂Spring&#xff1f; 1.什么是 Spring 框架&#xff1f;Spring 框架有哪些主要模块&#xff1f; Spring 框架是一个为 Java 应用程序的开发提供了综合、广泛的基础性支持的 Java 平台。Spring帮助开发者解…

ASP.NET Core Web服务器 Kestrel和Http.sys 特性详解

1.1. 名词解释 内核态: CPU可以访问内存所有数据, 包括外围设备, 例如硬盘, 网卡. CPU也可以将自己从一个程序切换到另一个程序。 用户态: 只能受限的访问内存, 且不允许访问外围设备. 占用CPU的能力被剥夺, CPU资源可以被其他程序获取。 1.2. Kestrel基本工作原理 Kestrel是…

Failed to execute

今天用dev c无论打编译什么都是出现如下结果&#xff1a; 后来终于找到解决办法了: 原来是这里出现问题了&#xff0c;我的电脑是32位的&#xff0c;必须也是32位的编译系统。否则不管输入什么都是上面的结果&#xff1b; 所以以后不管下载软件还是编译东西第一步一定要看自…

asp.net core 2.0 web api基于JWT自定义策略授权

JWT(json web token)是一种基于json的身份验证机制&#xff0c;流程如下&#xff1a; 通过登录&#xff0c;来获取Token&#xff0c;再在之后每次请求的Header中追加Authorization为Token的凭据&#xff0c;服务端验证通过即可能获取想要访问的资源。关于JWT的技术&#xff0c;…