盗引·中篇·spring aop

spring aop: jdk 动态代理和 cglib 动态代理的特点、区别、使用方式、原理及各自对反射的优化、二者在 spring 中的统一、通知顺序、从 @Aspect 到 Advisior、静态通知调用、动态通知调用。


版本

  • jdk:8
  • spring:5.3.20
  • spring boot:2.7.0

1 spring aop

1.1 aop

AOP (Aspect Oriented Programming) 即面向切面编程,其同 OOP (Object Oriented Programming) 即面向对象编程一样,是一种编程思想,其含义为通过预编译方式和运行期间动态代理实现程序功能的统一维护。AOP 是 OOP 的延续,是对 OOP 的一种补充。

aop 采取了横向抽取机制,取代了传统的纵向继承机制,其可拦截指定的方法并对其功能性增强,且其是非侵入式,分离了业务代码与非业务代码。常见的 aop 应用场景有日志记录、事务管理、权限认证、性能统计、异常处理等。

aop 是一种编程思想,其典型的实现有 spring aop 和 AspectJ。二者最大的区别是 spring aop 使用动态代理,AspectJ 使用静态代理。

spring 中 aop 是基于动态代理实现的,即基于 jdk 动态代理和 cglib 动态代理。但其也整合了 AspectJ。spring 自己实现的 aop 可以通过 xml、接口实现来使用,但不支持注解,所以 spring aop 引入了 AspectJ。实际上 spring aop 只是整合了 AspectJ 中的注解使用方式,最终会将 AspectJ 注解相关解析成 spring aop 自己的实现,当然底层依然是基于动态代理。

1.2 spring aop

1.2.1 八大核心概念
  • Joinpoint(连接点):即类中可以被增强的方法,统称为连接点。包括抽象方法、普通方法等。
  • Pointcut(切入点):又称切点,即需要被拦截的连接点,或者理解为需要被增强的方法。连接点中包含了切入点。
  • **Advice(通知) **:即增强,即为需要被增强的方法添加的增强内容。
  • Aspect(切面):切面 = 切点 + 通知。
  • Introduction(引介):即一种特殊的通知,即在运行期间动态的扩展类。即在运行期间为类添加一些属性和方法。
  • Target(目标):即要被增强的方法所在的类。其实例被称为目标对象。
  • Weaving(织入):即将通知(增强内容)按照切入点添加到目标中的过程。
  • Proxy(代理):即目标类被织入通知后产生的类,其实例被称为代理对象。
1.2.2 五种通知方式
  • before(前置通知):即通知内容在目标方法执行前执行。
  • after-returing(返回后通知):即通知内容在目标方法返回后执行。
  • after-throwing(抛出异常后通知):即通知内容在目标方法抛出异常后执行。
  • after(后置通知):即通知内容在目标方法执行后执行。
  • around(环绕通知):即通知内容在目标方法执行前后执行。
1.2.3 三种织入时期
  • 编译期:即在类编译期将通知内容织入到目标类中,AspectJ 就采用这种方式。实际上 AspectJ 的织入可细分为两种方式,即编译期织入和后编译期织入,二者的区别是:编译期织入是将通知内容在 .java 文件编译时直接织入到其 .class 字节码文件中;后编译期织入是将已经织入通知内容的 .class 字节码文件或整个 jar 包织入。
  • 类加载期:即在 jvm 加载类时将通知内容织入到目标类中,AspectJ 也可使用这种方式。
  • 运行期:即在程序运行期将通知内容织入到目标类中(实际上是生成了一份新的字节码文件),动态代理就采用这种方式。

2 设计实现

切面 = 切点 + 通知。spring aop 设计实现中,切面、切点、通知接口分别为 Advisor、Pointcut、Advice。其中针对五种不同的通知方式又扩展了五个通知接口,分别是 MethodBeforeAdvice(前置通知接口)、AfterReturningAdvice(返回后通知接口)、ThrowsAdvice(抛出异常后通知接口)、AfterAdvice(后置通知接口)、MethodInterceptor(环绕通知接口)。同时,为了集成 AspectJ aop 实现中的注解功能,又为 AspectJ 中切点、通知相关的注解提供了默认的实现了。 spring aop 的相关类图如下:

  • Advisor:即切面接口,切面 = 切点 + 通知。Advisor 通过字接口 PointcutAdvisor 间接持有了一个 Pointcut(切点),又直接持有了一个 Advice(通知)。spring aop 中的所有不同通知方式的 aop 最后都会转换为一个 Advisor 即切面去执行。
  • Pointcut:即切点接口。
  • ExpressionPointcut:即 SpEL 切点接口。
  • Advice:即通知接口。
  • MethodBeforeAdvice:即前置通知接口。
  • AfterReturningAdvice:即返回后置通知接口。
  • ThrowsAdvice:即抛出异常后通知接口。
  • AfterAdvice:即后置通知接口(finally 快执行完后)。
  • MethodInterceptor:即环绕通知接口。非环绕通知最终都会转化为环绕通知执行。
  • AspectJExpressionPointcut:即为集成 AspectJ 的 @Pointcut 注解功能而提供的表达式切点实现类。亦可理解为由 @Pointcut 注解声明的切点最终会被解析为 AspectJExpressionPointcut 的实例。
  • AspectJMethodBeforeAdvice:即为集成 AspectJ 的 @Before 注解功能而提供的前置通知实现类。亦可理解为由 @Before 注解声明的通知最终都会被解析为 AspectJMethodBeforeAdvice 的实例。非环绕通知。
  • AspectJAfterReturningAdvice:即为集成 AspectJ 的 @AfterReturning 注解功能而提供的返回后通知实现类。亦可理解为由 @AfterReturning 注解声明的通知最终都会被解析为 AspectJAfterReturningAdvice 的实例。非环绕通知。
  • AspectJAfterAdvice:即为集成 AspectJ 的 @After 注解功能而提供的后置通知实现类。亦可理解为由 @After 注解声明的通知最终都会被解析为 AspectJAfterAdvice 的实例。实现了 MethodInterceptor 接口,所以其本质为环绕通知。
  • AspectJThrowingAdvice:即为集成 AspectJ 的 @AfterThrowing 注解功能而提供的抛出异常后通知实现类。亦可理解为由 @AfterThrowing 注解声明的通知最终都会被解析为 AspectJThrowingAdvice 的实例。实现了 MethodInterceptor 接口,所以其本质为环绕通知。
  • AspectJAroundAdvice:即为集成 AspectJ 的 @Around 注解功能而提供的环绕通知实现类。亦可理解为由 @Around 注解声明的通知最终都会被解析为 AspectJAroundAdvice 的实例。实现了 MethodInterceptor 接口,所以其本质为环绕通知。
  • MethodBeforeAdviceInterceptor:@Before 注解对应通知 AspectJMethodBeforeAdvice 对应的环绕通知。
  • AfterReturningAdviceInterceptor:@AfterReturning 注解对应通知 AspectJAfterReturningAdvice 对应的环绕通知。

2 使用方式

spring aop 可以使用 xml、接口实现、注解 三种方式进行配置,其中前两种为 spring aop 提供,第三种为 AspectJ 提供。

2.1 接口实现配置

用接口实现的方式使用 aop,主要是实现切面、切点、通知相关的接口。如切面 Advisor;切点 Pointcut;通知 MethodBeforeAdvice、AfterReturningAdvice、AfterThrwosAdvice、AfterAdvice、MethodInterceptor。

// 目标类
@Component
public class Zed {public void attack() {System.out.println("Zed attack()");}public void taunt() {System.out.println("Zed taunt()");}
}
@Configuration
public class SpringAop {// 前置通知@Beanpublic MethodBeforeAdvice beforeAdvice() {return (method, args, target) -> {System.out.println("Spring aop 前置通知");};}// Zed attack() 方法的前置通知 切面@Beanpublic Advisor before(MethodBeforeAdvice beforeAdvice) {AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();pointcut.setExpression("execution(public * org.xgllhz.spring.aop.test.Zed.attack())");return new DefaultPointcutAdvisor(pointcut, beforeAdvice);}// 环绕通知@Beanpublic MethodInterceptor aroundAdvice() {return invocation -> {System.out.println("Spring aop 前环绕通知");Object result = invocation.proceed();System.out.println("Spring aop 后环绕通知");return result;};}// Zed taunt() 方法的环绕通知 切面@Beanpublic Advisor around(MethodInterceptor aroundAdvice) {AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();pointcut.setExpression("execution(public * org.xgllhz.spring.aop.test.Zed.taunt())");return new DefaultPointcutAdvisor(pointcut, aroundAdvice);}
}
// 测试结果
Spring aop 前置通知
Zed attack()Spring aop 前环绕通知
Zed taunt()
Spring aop 后环绕通知

2.2 注解配置

用注解方式使用 spring aop,主要是使用切点、通知相关的注解。如切点 @Pointcut;通知 @Before、@AfterReturning、@AfterThrows、@After、@Around。这些源自 AspectJ 实现的 aop 中,但 spring aop 适配了这些注解,使得可以在 spring aop 中使用这些注解来配置 aop。

// 目标类
@Component
public class Fizz {public void attack() {System.out.println("Fizz attack()");}public void taunt() {System.out.println("Fizz taunt()");}
}
@Aspect
@Component
public class AspectJAop {// 切点@Pointcut("execution(public * org.xgllhz.spring.aop.test.Fizz.attack())")public void attackPointcut() {}// 前置通知@Before("attackPointcut()")public void beforeAdvice() {System.out.println("AspectJ 前置通知");}// 切点@Pointcut("execution(public * org.xgllhz.spring.aop.test.Fizz.taunt())")public void tauntPointcut() {}// 环绕通知@Around("tauntPointcut()")public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {System.out.println("AspectJ 前环绕通知");Object result = joinPoint.proceed();System.out.println("AspectJ 后环绕通知");return result;}
}
// 测试结果
AspectJ 前置通知
Fizz attack()AspectJ 前环绕通知
Fizz taunt()
AspectJ 后环绕通知

3 jdk 与 cglib 动态代理

spring aop 底层基于动态代理,即 jdk 动态代理和 cglib 动态代理。

3.1 jdk 动态代理

3.1.1 简介

jdk 动态代理即由 jdk 的 java.lang.reflect 包中的 Proxy、InvocationHandler、ProxyGenerator 等类提供的生成代理对象的功能。

jdk 动态代理要求被代理的目标类必须实现一个或多个接口,这也是 jdk 动态代理的限制所在,即未实现接口的类无法被 jdk 动态代理。同时,jdk 动态代理是基于接口实现的(亦或是方法实现),所以目标类中的成员变量、static 方法、final 方法、private 方法均不能被其代理。也可以理解为 jdk 动态代理是基于方法实现的。

3.1.2 使用示例
// 接口
public interface Hero {void attack();
}
// 目标类
public class Zed implements Hero {@Overridepublic void attack() {System.out.println("invoke target method");}
}
public class JdkProxyTest {public static void main(String[] args) {// 目标对象Hero target = new Zed();// 类加载器 负责在运行期间加载动态生成的代理类的字节码文件ClassLoader classLoader = JdkProxyTest.class.getClassLoader();// 生成代理对象Hero proxyInstance = (Hero) Proxy.newProxyInstance(classLoader, new Class[]{ Hero.class }, (proxy, method, args1) -> {System.out.println("前置增强");// invoke() 方法的第一个参数是目标对象 第二个是目标对象的方法的参数Object result = method.invoke(target, args1);System.out.println("后置增强");return result;});proxyInstance.attack();}
}
// 测试结果
前置增强
invoke target method
后置增强

如上述代码所示,Hero 为目标类要实现的接口,Zed 为目标类,attack() 方法为要代理的方法。jdk 动态代理的核心是 Proxy 类和 InvocationHandler 接口,其中 Proxy 父类生成代理对象,InvocationHanlder 接口负责提供要增强的内容及目标方法的调用。获取代理对象的方法为 Proxy 类的 newProxyInstance() 方法,为静态方法,其需要三个参数,分别是 ClassLoader、Class<?>[]、InvocationHandler。其中 ClassLoader 负责在运行期间加载动态生成的代理类的字节码文件(实际上是个 byte 数组);Class<? >[] 则为目标类实现的接口;InvocationHandler 则为增强内容。

jdk 动态代理其核心为代理对象的创建,亦可为代理类的动态生成。代理类类对象由 Proxy 的内部类 ProxyBuilder 的静态方法 defineProxyClass() 生成,在这里会按一定规则生成代理类的类名,然后调用 ProxyGenerator 的 generateProxyClass() 方法生成代理类的字节码数组,接着调用 Unsafe 类的 defineClass() 方法生成代理类的类对象。

// 获取代理对象
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) {Objects.requireNonNull(h);final Class<?> caller = System.getSecurityManager() == null? null: Reflection.getCallerClass();// 查找或生成指定代理类的构造器Constructor<?> cons = getProxyConstructor(caller, loader, interfaces);// 根据代理类构造器及 InvocationHanlder 生成代理对象return newProxyInstance(caller, cons, h);
}
// Proxy.ProxyBuilder 获取代理类的类对象
private static Class<?> defineProxyClass(Module m, List<Class<?>> interfaces) {// 按照规则生成代理类类名 形如 $ProxyN 其中 N 代表第 N 次生成代理类long num = nextUniqueNumber.getAndIncrement();String proxyName = proxyPkg.isEmpty()? proxyClassNamePrefix + num: proxyPkg + "." + proxyClassNamePrefix + num;ClassLoader loader = getLoader(m);trace(proxyName, m, loader, interfaces);// 调用 ProxyGenerator.generateProxyClass 方法生成代理类的字节码数组// 其接受三个参数 分别为代理类类名、要实现的接口名、访问权限byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces.toArray(EMPTY_CLASS_ARRAY), accessFlags);try {// 调用 UNSAFE.defineClass() 生成代理类对象Class<?> pc = UNSAFE.defineClass(proxyName, proxyClassFile,0, proxyClassFile.length,loader, null);reverseProxyCache.sub(pc).putIfAbsent(loader, Boolean.TRUE);return pc;} catch (ClassFormatError e) {throw new IllegalArgumentException(e.toString());}
}
3.1.3 模拟实现

模拟实现 jdk 动态代理,主要在于最后生成的代理类。

// 目标类要实现的接口
public interface Hero {Object attack();
}
// 目标类
public class ZedHero implements Hero {@Overridepublic Object attack() {System.out.println("执行目标方法");return "zed";}
}
// 模仿 InvocationHandler 接口
public interface InvocationHandler {Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
// 最终生成的代理类
public class $Proxy0 implements Hero {private InvocationHandler handler;   // 所持有的调用处理器对象private static Method attack;   // 被代理的 attack 方法对象// 在静态代码块中通过反射从目标类实现的接口的获取到要代理的方法对象static {try {attack = Hero.class.getDeclaredMethod("attack");} catch (NoSuchMethodException e) {throw new NoSuchMethodError(e.getMessage());}}// 有参构造 接受一个调用处理器对象参数public $Proxy0(InvocationHandler handler) {this.handler = handler;}// 最终被代理的 attack 方法@Overridepublic Object attack() {try {// 内部直接调用 InvocationHanlder 的 invoke() 发方法// 而 invoke() 方法内不仅会调用目标类的 attack() 方法 还会执行增强逻辑return this.handler.invoke(this,attack, null);} catch (RuntimeException | Error e) {throw e;} catch (Throwable e) {throw new UnsupportedOperationException(e);}}
}
3.1.4 反射优化

jdk 动态代理中是通过反射来调用目标对象的方法的。众所周知,java 中反射是存在性能问题的,所以 jdk 对其作出了优化。即当反射调用某一个方法时,默认情况下前 15 次调用都是反射调用(实际上是通过 DelegatingMethodAccessorImpl/NativeMethodAccessorImpl 实例调用的),从第 16 次开始则会给反射调用的方法生成一个方法访问类(即 GeneratedMethodAccessorXXX 类)(此处可理解为直接调用方法而非反射),再调用该方法时则会用该类实例以直接调用的方式调用。反射调用次数可通过 -Dsun.reflect.inflationThreshold=xxx 莫命令来指定,也可通过 -Dsun.reflect.noInflation=true 来忽略前十五次的反射调用,从直接调用开始(jdk 为什么不直接将其设置为 true,这样就可以避免反射了么。而 jdk 没有这么做的原因是 jdk 或其它 java 框架中多次使用了反射,若为 false,则会给每个反射调用的方法生成一个 GeneratedMethodAccessorXXX 类,这样会使类爆炸,大量占用内存(仅个人理解))。

public class MethodInvokeTest {public static void attack(Integer i) {System.out.println("瞬狱影杀阵 " + i);}// 方法反射调用时 底层使用 MethodAccessor 实现类private static void show(int i, Method foo) throws Exception {Method getMethodAccessor = Method.class.getDeclaredMethod("getMethodAccessor");getMethodAccessor.setAccessible(true);Object invoke = getMethodAccessor.invoke(foo);if (invoke == null) {System.out.println(i + ":" + null);return;}Field delegate = Class.forName("jdk.internal.reflect.DelegatingMethodAccessorImpl").getDeclaredField("delegate");delegate.setAccessible(true);System.out.println(i + ":" + delegate.get(invoke));}public static void main(String[] args) throws Exception {Method attackMethod = MethodInvokeTest.class.getDeclaredMethod("attack", Integer.class);for (int i = 1; i <= 17; i++) {attackMethod.invoke(null, i);show(i, attackMethod);}}
}
// 测试结果
// 从结果可看出 前 15 次都是使用 NativeMethodAccessorImpl 实例反射调用
// 从第 16 次开始使用 GeneratedMethodAccessor1 实例 而 GeneratedMethodAccessor1 的调用方式为直接调用
// 注:该测试需要添加 jvm 参数
// --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED
瞬狱影杀阵 1:jdk.internal.reflect.NativeMethodAccessorImpl@224edc67
瞬狱影杀阵 2:jdk.internal.reflect.NativeMethodAccessorImpl@224edc67
瞬狱影杀阵 3:jdk.internal.reflect.NativeMethodAccessorImpl@224edc67
瞬狱影杀阵 4:jdk.internal.reflect.NativeMethodAccessorImpl@224edc67
瞬狱影杀阵 5:jdk.internal.reflect.NativeMethodAccessorImpl@224edc67
瞬狱影杀阵 6:jdk.internal.reflect.NativeMethodAccessorImpl@224edc67
瞬狱影杀阵 7:jdk.internal.reflect.NativeMethodAccessorImpl@224edc67
瞬狱影杀阵 8:jdk.internal.reflect.NativeMethodAccessorImpl@224edc67
瞬狱影杀阵 9:jdk.internal.reflect.NativeMethodAccessorImpl@224edc67
瞬狱影杀阵 10:jdk.internal.reflect.NativeMethodAccessorImpl@224edc67
瞬狱影杀阵 11:jdk.internal.reflect.NativeMethodAccessorImpl@224edc67
瞬狱影杀阵 12:jdk.internal.reflect.NativeMethodAccessorImpl@224edc67
瞬狱影杀阵 13:jdk.internal.reflect.NativeMethodAccessorImpl@224edc67
瞬狱影杀阵 14:jdk.internal.reflect.NativeMethodAccessorImpl@224edc67
瞬狱影杀阵 15:jdk.internal.reflect.NativeMethodAccessorImpl@224edc67
瞬狱影杀阵 16:jdk.internal.reflect.GeneratedMethodAccessor1@6f7fd0e6
瞬狱影杀阵 17:jdk.internal.reflect.GeneratedMethodAccessor1@6f7fd0e6

3.2 cglib 动态代理

3.2.1 简介

cglib 是一个强大的高性能代码生成包,它可以在运行期扩展 java 类或实现 java 接口,被广泛应用于许多 aop 框架,如 spring aop、dynaop。其底层使用一个小而快的字节码处理框架 asm,通过 asm 来转换字节码并生成类。

cglib 动态代理是基于继承实现的,即其是通过继承目标类来生成一个代理类。所以目标类中的成员变量、static 方法、final 方法、private 方法是不能被其代理的。也可以理解为 cglib 动态代理是基于方法重写的。

3.2.2 使用示例
// 目标类
public class Zed {public void attack() {System.out.println("Zed attack()")}
}
public class CglibProxyTest {public static void main(String[] args) {// 目标对象Zed target = new Zed();// 代理对象// create() 第一个参数是代理父类型 第二个参数是回调(这里使用一个回调接口的子接口)// 回调方法的第一个参数是代理对象 第二个参数是目标方法 第三个参数是目标方法入参 第四个参数是目标方法的代理方法对象Target proxyInstance = (Target) Enhancer.create(Zed.class, (MethodInterceptor) (proxy, method, objects, methodProxy) -> {System.out.println("前置增强");// 内部使用代理Object result1 = method.invoke(target, objects);// 内部未使用代理Object result2 = methodProxy.invoke(target, objects);// 内部未使用代理Object result3 = methodProxy.invokeSuper(proxy, objects);System.out.println("后置增强");return result1;});proxyInstance.attack();}
}
// 测试结果
前置增强
Zed attack()
后置增强

如上述代码所示,cglib 动态代理是通过 Enhancer 类的静态方法 create() 来获取代理对象的,该方法接受两个参数,分别代表目标类、回调接口,其中目标类是作为父类在生成代理类时被用来继承,而回调接口则是承载了增强代码和目标方法的调用。

与 jdk 动态代理的另一不同点在于目标方法的调用。jdk 动态代理中是直接通过反射来调用目标方法,而 cglib 则提供了三种目标方法的调用方式:

  • method.invoke(target, objects):其和 jdk 动态代理类似,直接在内部使用反射来调用目标方法。第一个参数为目标对象,第二个参数为目标方法的参数。
  • methodProxy.invoke(target, objects):其未使用反射,它会正常(间接)调用目标方法。第一个参数为目标对象,第二个参数为目标方法参数。spring 即采用这方方式。
  • methodProxy.invokeSuper(proxy, objects):其未使用反射,它会正常(间接)调用目标方法。第一个参数为代理对象,第二个参数为目标方法参数。其省略了目标对象。
3.2.3 模拟实现

模拟 cglib 动态代理,重点在于最后生成的代理类。

// 目标类
public class Zed {public void attack() {System.out.println("影忍法·灭魂劫");}public void attack(Integer q) {System.out.println("影奥义·诸刃 " + q);}
}
// 模拟方法拦截器 即回调接口
public interface MethodInterceptor {// 代理方法 参数分别为:代理对象、目标方法、目标方法参数、目标方法代理对象Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable;}
// 最后生成的代理类
public class CglibProxy extends Zed {// 代理类内部持有方法拦截器的实例 通过其来调目标方法private MethodInterceptor methodInterceptor;public CglibProxy() {}// 代理类内部持有了目标方法的方法对象private static Method method0;private static Method method1;// 代理类在内部持有了目标方法的代理对象private static MethodProxy methodProxy0;private static MethodProxy methodProxy1;static {try {// 在静态代码块中实例化目标方法对象method0 = Zed.class.getDeclaredMethod("attack");method1 = Zed.class.getDeclaredMethod("attack", Integer.class);// 在静态代码块中实例化目标方法的代理对象 通过 MethodProxy 的静态方法 create() 来获得// create() 方法接受四个参数,分别是目标类、cglib代理类、目标方法参数及返回值描述、目标方法名称,代理方法名称// 其中目标方法参数及返回值描述 形如 '(Q)V'// 其通过 jvm 指令解析 形如 (Q)V 表示返回值为 void 入参只有一个(目前还没深入研究)methodProxy0 = MethodProxy.create(Zed.class, CglibProxy.class, "()V", "attack", "attackSuper");methodProxy1 = MethodProxy.create(Zed.class, CglibProxy.class, "(Q)V", "attack", "attackSuper");} catch (NoSuchMethodException e) {throw new NoSuchMethodError(e.getMessage());}}public void setMethodInterceptor(MethodInterceptor methodInterceptor) {this.methodInterceptor = methodInterceptor;}// 以下这两个方法是为反射优化准备的public void attackSuper() {super.attack();}public void attackSuper(Integer q) {super.attack(q);}// 重写了目标类的目标方法 并在其内部通过方法拦截器来间接调用目标方法@Overridepublic void attack() {try {this.methodInterceptor.intercept(this, method0, new Object[0], methodProxy0);} catch (Throwable e) {throw new UndeclaredThrowableException(e);}}@Overridepublic void attack(Integer q) {try {this.methodInterceptor.intercept(this, method1, new Object[]{q}, methodProxy1);} catch (Throwable e) {throw new UndeclaredThrowableException(e);}}
}
3.2.4 反射优化

与 jdk 动态代理一样,cglib 动态代理中也使用了反射来调用目标对象的方法,所以其也会存在反射性能的问题。

首先,cglib 调用目标方法有三种方式:

  • method.invoke(target, objects):使用反射调用,调用足够多的次数才会进行优化。
  • methodProxy.invoke(target, objects):未使用反射,间接调用。结合目标对象使用。spring 采用这种调用方式。
  • methodProxy.invokeSuper(proxy, objects):未使用反射,间接调用。结合代理对象使用。

而 cglib 不同于 jdk 的反射优化方式就在于后两种调用方式,即结合目标对象调用和结合代理对象调用。其优化思想是为目标方法额外生成代理类。即当调用 MethodProxy 的 invoke() 或 invokeSuper() 方法是会额外生成两个代理类(可以称之为方法代理类),分别是 TargetFastClassProxy 和 ProxyFastCassProxy。其中 TargetFastClassProxy 方法代理类是直接为目标类生成的方法代理类,而 ProxyFastClassProxy 则是为目标类的代理类的方法(具体是代理类中的后缀为 Super 的方法)生成的方法代理类。二者思想大同小异,都是通过代理类在代理类内部直接调用目标类的方法。

所以,对于 cglib 动态代理,则可能会生成三个代理类,分别是目标类的代理类、目标方法的代理类、目标类的代理类的目标方法的代理类。

就上述实例代码而言,其最终生成的 TargetFastClassProxy 类和 ProxyFastClassProxy 类如下:

public class TargetFastClassProxy {// 记录了目标类的目标方法的签名static Signature signature0 = new Signature("attack", "()V");static Signature signature1 = new Signature("attack", "(Q)V");// 通过方法签名来获取方法编号(此处方法编号是固定的)public int getIndex(Signature signature) {if (signature0.equals(signature)) {return 0;} else if (signature1.equals(signature)) {return 1;} else {return -1;}}// 通过方法编号来调用对应的目标方法public Object invoke(int index, Object target, Object[] args) {if (0 == index) {// 此处是直接使用目标对象来调用目标方法 而非反射的方式((Zed) target).attack();return null;} else if (1 == index) {((Zed) target).attack((Integer) args[0]);return null;} else {throw new RuntimeException("Method doest not exist");}}// 测试public static void main(String[] args) {TargetFastClassProxy targetFastClassProxy = new TargetFastClassProxy();int index = targetFastClassProxy.getIndex(new Signature("attack", "()V"));targetFastClassProxy.invoke(index, new Target(), null);}
}
public class ProxyFastClassProxy {// 记录了目标类的代理类的目标方法的签名// 仔细回忆下 在 3.2.2 使用示例中是不是有个类叫 CglibProxy 它里面是不是有两个方法分别是 attackSuper() 和 attackSuper()// 这两个方法就是被这个代理类代理的方法static Signature signature0 = new Signature("attackSuper", "()V");static Signature signature1 = new Signature("attackSuper", "(Q)V");// 通过方法签名获取方法编号public int getIndex(Signature signature) {if (signature0.equals(signature)) {return 0;} else if (signature1.equals(signature)) {return 1;} else {return -1;}}// 通过方法编号来调用指定方法public Object invoke(int index, Object proxy, Object[] args) {if (0 == index) {// 这里是直接使用代理对象来调用// 而 CglibProxy 的 attackSuper() 方法内部则是直接使用目标对象调用了目标方法 整个流程也是非反射的((CglibProxy) proxy).attackSuper();return null;} else if (1 == index) {((CglibProxy) proxy).attackSuper((Integer) args[0]);return null;} else {throw new RuntimeException("Method doest not exist");}}// 测试public static void main(String[] args) {ProxyFastClassProxy proxyFastClassProxy = new ProxyFastClassProxy();int index = proxyFastClassProxy.getIndex(new Signature("attackSuper", "()V"));proxyFastClassProxy.invoke(index, new CglibProxy(), null);}
}

3.3 jdk 与 cglib 反射优化

通过上述案例,则可知 jdk 与 cglib 在针对反射性能问题上的优化的异同点:

  • 相同点

    • 1、二者都是通过生成额外的代理类实现的(即方法代理类)。
    • 2、二者的优化手段都是通过直接调用目标对象的方法。
  • 不同点
    • 1、jdk 是在目标方法被调用了 15 后才生成额外的代理类的,而 cglib 则是在第一次调用目标方法时就生成了额外的代理类(即 TargetFastClassProxy 或 ProxyFastClassProxy)。
    • 2、jdk 会为每一个目标方法都生成额外的代理类,而 cglib 则会为目标类的所有目标方法或代理类所有目标方法只生成一个额外的代理类。

4 spring aop 与动态代理

spring aop 底层基于 jdk 动态代理和 cglib 动态代理,其是通过一系列的配置和接口以及实现类来统一了 jdk 和 cglib 的动态代理实现。

  • ProxyConfig

    即代理配置类,亦是代理配置的父类,即允许其它配置类对其进行继承扩展,如许多 ProxyCreator 代理创建器类都会根据自身实际使用场景对其进行自定义配置。其主要成员变量如下:

    // 表示是否基于完整的类创建代理 即为 true 时使用 cglib 为 false 时使用 jdk 默认为 false
    private boolean proxyTargetClass = false;// 表示是否对代理进行优化 为 true 时表示优化 默认为 false(但具体怎么优化 从哪里优化没研究过)
    private boolean optimize = false;// 表示是否应该冻结代理配置 代理配置被冻结后将不能再进行修改 即为 true 时将冻结 默认为 false
    private boolean forzen = false;// 表示生成的代理对象是否可以强转为 Advised 实例 即为 true 时不可以 为 false 时可以 默认为 false 强转为 Advised 实例后可获取一些代理相关的信息
    boolean opaque = false;// 表示生成的代理对象是否可以被 aop 的 AopContext 的 ThreadLocal 线程局部变量共享出去(共享的目的是为了目标对象能够访问) 即为 true 时将共享 默认为 false
    boolean exposeProxy = false;
    
  • Advised

    即切面接口。实际上它和切面关系不大,更像是一个综合性功能的接口,声明了许多功能性的方法,可通过此接口来获取代理配置、代理对象的相关信息。且当 ProxyConfig 的 opaque 配置为 true 时,凡是通过 spring aop 生成的代理对象都可强转为 Advised 类型。

    // 获取代理配置是否被冻结
    boolean isFrozen();// 获取代理是否基于完整的类而不是接口
    boolean isProxyTargetClass();// 获取代理是否将被暴露
    boolean isExposeProxy();// 获取被代理的所有接口类型(即目标类实现的接口)(不包括被代理的类类型)
    Class<?>[] getProxiedInterfaces();// 判断一个指定的类型是否为被代理的接口类型
    boolean isInterfaceProxied(Class<?> intf);// 获取代理对象的目标类
    TargetSource getTargetSource();// 获取 aop 中所有的切面(切面 = 切点 + 通知)
    Advisor[] getAdvisors();// 向 aop 上下文中添加一个切面
    void addAdvisor(Advisor advisor) throws AopConfigException;
    
  • AdvisedSupport

    即 Advised 接口的实现类,其除了实现 Advised 接口声明的方法外,还维护了三个重要的成员变量,如下所示:

    // 目标类 默认为空
    TargetSource = targetSource = EmptyTargetSource.INSTANCE;// 目标类所实现的接口类型集合
    private List<Class<?>> interfaces = new ArrayList<>();// 从目标类中解析出来的切面集合 目标类中配置的所有通知最终会被解析成单个的 Advisor 切面并被维护在该集合中
    private List<Advisor> advisors = new ArrayList<>();
    
  • ProxyCreatorSupport

    其继承自 AdvisedSupport,除了拥有父类的相关功能外,其最重要的一点是持有一个 AopProxyFactory 成员变量,并对外提供了该变量相关的方法。该成员变量意为 aop 代理工厂,即提供一个创建代理的工厂。

    // AopProxyFactory aop 代理工厂
    private AopProxyFactory aopProxyFactory;
    
  • ProxyFactory

    即代理工厂,其继承自 ProxyCreatorSupport,其最重要的功能配置特定的代理工厂并使用工厂获取代理对象。

    // 先通过 AopProxyFactory 的 createAopProxy() 方法获取具体的动态代理实现(即 AopProxy 的实例)
    // 然后再通过 AopProxy 的 getProxy() 方法创建代理对象
    public Object getProxy() {return createAopProxy().getProxy();
    }// 基于 jdk 动态代理获取代理对象
    public static <T> T getProxy(Class<T> proxyInterface, TargetSource targetSource) {return (T) new ProxyFactory(proxyInterface, targetSource).getProxy();
    }// 基于 cglib 动态代理获取代理对象
    public static Object getProxy(TargetSource targetSource) {ProxyFactory proxyFactory = new ProxyFactory();proxyFactory.setTargetSource(targetSource);proxyFactory.setProxyTargetClass(true);return proxyFactory.getProxy();
    }
    
  • AopProxyFactory

    即 aop 代理工厂接口,主要功能是获取一个 aop 代理实现。注意 AopProxyFactory 和 ProxyFactory 的区别,AopProxyFactory 工厂是用来生产一个 aop 代理实现的,如 jdk 动态代理、cglib 动态代理;而 ProxyFactory 工厂是用来生产代理对象的。AopProxyFactory 接口只有一个功能,如下:

    // 根据传入的代理配置获取一个 aop 代理实现 如 jdk 动态代理、cglib 动态代理
    AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException;
    
  • DefaultAopProxyFactory

    即 AopProxyFactory 接口的默认实现类,其主要实现了接口中的 createAopProxy() 方法。因此此处是决定 sping aop 最终使用那种动态代理实现的关键。

    // 根据指定的配置获取创建一个 aop 代理实现
    @Override
    public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {if (!NativeDetector.inNativeImage() &&(config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) {Class<?> targetClass = config.getTargetClass();if (targetClass == null) {throw new AopConfigException("");}if (targetClass.isInterface() || Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) {// 若目标类是个接口、代理类、lambda 表达式 则返回 jdk 动态代理实现return new JdkDynamicAopProxy(config);}// 若代理需要被优化、基于完整的类创建代理、代理配置未基于接口 则返回 cglib 动态代理实现return new ObjenesisCglibAopProxy(config);}else {// 若代理不需要被优化、不是基于完整类创建代理、代理配置基于接口 则返回 jdk 动态代理实现return new JdkDynamicAopProxy(config);}
    }
    
  • AopProxy

    即 aop 代理接口,该接口定义了实际获取代理对象的功能。且 spring 提供了两个实现类,分别是 JdkDynamicAopProxy 和 CglibAopProxy,即分别代表 jdk 动态代理和 cglib 动态代理。

    public interface AopProxy {Object getProxy();Object getProxy(@Nullable ClassLoader classLoader);
    }
    
  • JdkDynamicAopProxy

    即 spring aop 实现的 jdk 动态代理。该类实现类 AopProxy 接口,用来创建代理对象;同时实现了 InvocationHandler 接口,用来回调目标方法,其中目标方法是以反射的方式调用。

    final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {// 构造方法 实例化了 Advised 实例和目标类所实现的接口public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException {this.advised = config;this.proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);findDefinedEqualsAndHashCodeMethods(this.proxiedInterfaces);}@Overridepublic Object getProxy() {return getProxy(ClassUtils.getDefaultClassLoader());}@Overridepublic Object getProxy(@Nullable ClassLoader classLoader) {// 使用 Proxy 类的静态方法 newProxyInstance() 创建代理对象return Proxy.newProxyInstance(classLoader, this.proxiedInterfaces, this);}// 回调方法 内部以反射的方式调用了目标方法@Override@Nullablepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {..}
    }
    
  • CglibAopProxy

    即 spring aop 实现的 cglib 动态代理类,该类实现了 AopProxy 接口。内部通过 Enhancer 的 create() 方法来创建代理对象。同时定义了内部类 DynamicAdvisedInterceptor,其实现了 MethodInterceptor 接口,来作为回调接口的实现,内部通过 MethodProxy#invoke(Object Target, Object[] objects) 方法来以非反射的方式调用目标方法。

    class CglibAopProxy implements AopProxy, Serializable {public CglibAopProxy(AdvisedSupport config) throws AopConfigException {this.advised = config;this.advisedDispatcher = new AdvisedDispatcher(this.advised);}@Overridepublic Object getProxy() {return getProxy(null);}@Overridepublic Object getProxy(@Nullable ClassLoader classLoader) {...// 配置 cglib EnhancerEnhancer enhancer = createEnhancer();enhancer.setSuperclass(proxySuperClass);enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader));// 获取回调(其定义了多个回调内部实现类 如 equals() 的、hashCode() 的、动态代理的)Callback[] callbacks = getCallbacks(rootClass);enhancer.setCallbackTypes(types);// 创建代理类和代理对象return createProxyClassAndInstance(enhancer, callbacks);}protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {enhancer.setInterceptDuringConstruction(false);enhancer.setCallbacks(callbacks);return (this.constructorArgs != null && this.constructorArgTypes != null ?enhancer.create(this.constructorArgTypes, this.constructorArgs) :enhancer.create());}// 在回调接口的内部实现类中调用了此静态方法 在此方法内调用了方法代理类的 invoke() 方法(该方法是以非反射的方式调用目标方法)@Nullableprivate static Object invokeMethod(@Nullable Object target, Method method, Object[] args, MethodProxy methodProxy)throws Throwable {return methodProxy.invoke(target, args);}
    }
    

5 从 @Aspect 到 Advisor

前文中提到了 spring aop 的使用方式,即接口实现配置和注解配置。其中接口实现配置是通过直接实现 spring aop 提供的相关切点、通知、切面接口来使用 aop 的;而注解配置则是使用了 spring aop 集成自 AspectJ 框架的相关注解来使用 aop 的。

这两者之间是存在紧密联系的,即 spring aop 会将通过注解配置的切面转换成相关接口实例。具体则是 @Aspect 到 Advisor 的过程。这里我们可以将通过注解配置的切面称之为高级切面,将通过实现接口配置的切面称之为低级切面。而这个转换工作则是由 AnnotationAwareAspectJAutoProxyCreator BPP 来完成的。

AnnotationAwareAspectJAutoProxyCreator BPP 主要有两个功能:

  • 切面转换

    即将 @AspectJ 高级切面转换为 Advisor 低级切面,由 findEligibleAdvisors() 方法来完成。该方法会找到两种切面,一种是通过直接实现 Advisor 接口实现的低级切面,一种是通过解析 @AspectJ 高级切面得到的低级切面 Advisor。最终返回一个 List< Advisor> 集合。

  • 创建代理

    即创建代理对象。由 wrapIfNecessary() 方法完成。该方法会先通过调用 findEligibleAdvisors() 方法获得所有的低级切面,然后为其创建代理。创建代理的时机在 spring bean 生命周期中表现为:实例化 -> (1) 属性设置 -> 初始化 (2),即创建代理的时机为:当发生循环依赖时在属性设置(依赖注入)前创建代理;当未发生循环依赖时在初始化后创建代理。

6 通知顺序

通知顺序由具体的切面使用决定,分为两种,即低级切面和高级切面。具体顺序如下:

  • 低级切面

    低级切面的通知顺序需要借助 @Order 注解来设置,其值越小通知越早。注:该注解只能作用于类,作用于方法无效。

  • 高级切面

    高级切面由具体的通知注解决定,即 @Around before、@Before、@AfterThrowing、@AfterReturning、@After、@Around after。分别表示为:前环绕通知、前置通知、异常后通知、返回后通知、后置通知、后环绕通知。

7 静态通知调用与动态通知调用

所谓静态通知调用指的时目标方法与通知方法之间不存在参数传递的情况,而动态通知调用则指的是目标方法与通知方法之间存在参数传递的情况。这里的参数传递是指将目标方法的参数绑定到通知方法上。

7.1 静态通知调用

静态通知调用的流程为(以 jdk 动态代理为例):

  • a 切面转换

    即将 @Aspect 注解标注的高级切面转换成 Advisor 低级切面。

    • @Around 切面通知被解析为 Advisor 的子实现类 AspectJAroundAdvice。
    • @Before 切面通知被解析为 Advisor 的子实现类 AspectJMethodBeforeAdvice。
    • @AfterThrowing 切面通知被解析为 Advisor 的子实现类 AspectJAfterThrowingAdvice。
    • @AfterReturning 切面通知被解析为 Advisor 的子实现类 AspectJAfterReturningAdvice。
    • @After 切面通知被解析为 Advisor 的子实现类 AspectJAfterAdvice。
  • b 通知转换

    即将非环绕通知转换为环绕通知。这里的环绕通知是指 MethodInterceptor 类型(org.aopalliance.intercept.MethodInterceptor)。

    ProxyFactory 会将非环绕通知转换为环绕通知,且通知调用时会以 MethodInterceptor 类型调用。

    而通过 @Aspect 注解配置的切面中的部分通知不是环绕通知,如 @Before、@AfterReturning。所以此处 spring aop 运用适配器模式将非环绕通知转换成了环绕通知。

    • MethodBeforeAdviceAdapter 将 @Before 对应的通知 AspectJMethodBeforeAdvice 适配为 MethodBeforfeAdviceInterceptor。
    • AfterReturningAdviceAdapter 将 @AfterReturning 对应的通知 AspectJAfterReturningAdvice 适配为 AfterReturningAdviceInterceptor。
  • c 通知调用

    由 MethodInvocation 接口调用通知。因为方法存在嵌套情况,通知又基于方法,所以通知也存在嵌套情况,故此处使用责任链模式来调用。即调用顺序为 由外而内 -> 目标方法 -> 由内而外。

7.2 动态通知调用

动态通知调用即目标方法与通知方法之间存在参数传递的情况(将目标方法的参数绑定到通知方法上)。调用流程与静态通知调用的区别在于通知转换上,即其在通过代理工厂将非环绕通知转换到环绕通知时(ProxyFactory.getInterceptorsAndDynamicInterceptionAdvice()),将动态通知转化成了 InterceptorAndDynamicMethodMatcher 类型。该类型中维护了环绕通知和切点成员变量,即 MethodInterceptor 和 MethodMatcher。其中 MethodMatcher 是表达式切点 AspectJExpressionPointcut 的父接口。从而实现了动态通知调用。

注:动态通知调用需要再次解析切点以便为通知方法绑定参数,故其复杂度较高,较静态通知调用而言性能较差。

8 通知调用 MethodInvocation

在 spring aop 中,以 jdk 动态代理为例,通知调用采用了责任链设计模式,其中每一个通知(MethodInterceptor)都代表一个职责,所有的通知会组成一个通知链(MethodInvocation),且在最中间调用了目标方法。

责任链设计模式在 java 和 spring 框架中有很多的引用,常用在过滤器、拦截器中。如 servlet 中的过滤器,主要体现在 FilterChain 与 Filter 中;spring security 中的 SpringSecurityChain 与 Filter;spring aop 中的 MethodInvocation 与 MethodInterceptor 等。

以下将模拟实现 spring aop 通知调用的工作流程:由外而内 -> 目标方法 -> 由内而外。

// 目标类
public class Zed {// 目标方法public void attack() {System.out.println("禁奥义·瞬狱影杀阵");}
}
// 通知一
public class OneAdvice implements MethodInterceptor {@Overridepublic Object invoke(MethodInvocation invocation) throws Throwable {System.out.println("OneAdvice 环绕前通知");Object result = invocation.proceed();System.out.println("OneAdvice 环绕后通知");return result;}
}// 通知二
public class TwoAdvice implements MethodInterceptor {@Overridepublic Object invoke(MethodInvocation invocation) throws Throwable {System.out.println("TwoAdvice 环绕前通知");Object result = invocation.proceed();System.out.println("TwoAdvice 环绕后通知");return result;}
}// 通知三
public class ThreeAdvice implements MethodInterceptor {@Overridepublic Object invoke(MethodInvocation invocation) throws Throwable {System.out.println("ThreeAdvice 环绕前通知");Object result = invocation.proceed();System.out.println("ThreeAdvice 环绕后通知");return result;}
}
// 自定义实现 MethodInvocation(这个可以理解为 通知链)
public class MyMethodInvocation implements MethodInvocation {private Object target;   // 目标对象private Method method;   // 目标方法private Object[] args;   // 目标方法参数private List<MethodInterceptor> methodInterceptors;   // 通知集合(环绕通知集合)private int count = 1;   // 通知调用次数 因为中间会调用一次目标方法 调用目标方法时不会调用通知 所在其初始值为 1public MyMethodInvocation(Object target, Method method, Object[] args, List<MethodInterceptor> methodInterceptors) {this.target = target;this.method = method;this.args = args;this.methodInterceptors = methodInterceptors;}@Overridepublic Method getMethod() {return this.method;}@Overridepublic Object[] getArguments() {return this.args;}@Overridepublic Object proceed() throws Throwable {// 若调用次数大于通知数量 则说明通知链中每个通知都已调用完毕(递归到最底层) 此时应该调用目标方法if (this.count > this.methodInterceptors.size()) {return this.method.invoke(this.target, this.args);}// 获取通知链中下一个通知 然后调用MethodInterceptor interceptor = this.methodInterceptors.get(this.count++ - 1);return interceptor.invoke(this);}@Overridepublic Object getThis() {return this.target;}@Overridepublic AccessibleObject getStaticPart() {return this.method;}
}
// 测试
public class MethodInvocationTest {public static void main(String[] args) throws Throwable {// 构建一个通知链List<MethodInterceptor> methodInterceptors = List.of(new OneAdvice(), new TwoAdvice(), new ThreeAdvice());// 在目标对象的目标方法上调用通知链MyMethodInvocation methodInvocation = new MyMethodInvocation(new Zed(), Zed.class.getMethod("attack"),new Object[0], methodInterceptors);methodInvocation.proceed();}
}
// 测试结果 由结果可见 通知调用为 由外而内 -> 目标方法 -> 由内而外
OneAdvice 环绕前通知
TwoAdvice 环绕前通知
ThreeAdvice 环绕前通知
禁奥义·瞬狱影杀阵
ThreeAdvice 环绕后通知
TwoAdvice 环绕后通知
OneAdvice 环绕后通知

难得 可以同座 何以 要忌讳赤裸

史上最烂 spring aop 原理分析相关推荐

  1. spring AOP原理分析:静态代理;JDK实现接口动态代理;Cglib继承父类代理;SpringAop的责任链模式调用

    普通静态代理 代理类和真实类都需要实现同一个接口 接口 package com.fchan.layui.represent.service; /*** 静态代理demo*/ public interf ...

  2. Spring事务原理分析(一)--@EnableTransactionManagement 到底做了什么?

    目录 一.概述 二.事务的ACID属性 三.事务的隔离级别 四.事务的传播行为 五.Spring声明式事务环境搭建 六.@EnableTransactionManagement分析 七.AutoPro ...

  3. modelandview使用过程_面试问烂的 Spring AOP 原理、Spring MVC 过程

    点击上方 Java后端,选择 设为星标 优质文章,及时送达 作者:莫那一鲁道链接:www.jianshu.com/p/e18fd44964eb Spring AOP ,SpringMVC ,这两个应该 ...

  4. 面试问烂的 Spring AOP 原理、SpringMVC 过程(求求你别问了)

    Spring AOP ,SpringMVC ,这两个应该是国内面试必问题,网上有很多答案,其实背背就可以.但今天笔者带大家一起深入浅出源码,看看他的原理.以期让印象更加深刻,面试的时候游刃有余. Sp ...

  5. Maven——Maven核心概念——史上最烂系列

    Maven 是目前最流行的自动化构建工具,对于生产环境下多框架.多模块整合开发有重要作用.Maven 是一款在大型项目开发过程中不可或缺的重要工具.(自己粗略写了一个SSM项目之后,回顾头来看这个,会 ...

  6. SpringClound——SpringClound入门概述——史上最烂

    SpringClound--微服务概述--史上最烂 SpringClound--SpringClound入门概述--史上最烂 SpringCloud--Eureka--史上最基本 SpringClou ...

  7. Spring事务原理分析-部分一

    Spring事务原理分析-部分一 什么事务 事务:逻辑上的一组操作,组成这组操作的各个单元,要么全都成功,要么全都失败. 事务基本特性 ⑴ 原子性(Atomicity) 原子性是指事务包含的所有操作要 ...

  8. 浅谈:Spring Boot原理分析,切换内置web服务器,SpringBoot监听项目(使用springboot-admin),将springboot的项目打成war包

    浅谈:Spring Boot原理分析(更多细节解释在代码注释中) 通过@EnableAutoConfiguration注解加载Springboot内置的自动初始化类(加载什么类是配置在spring.f ...

  9. Spring AOP方法分析

    Spring AOP方法分析 此示例显示如何配置Spring AOP方法概要分析.我们可以在任何服务(或其他)类中使用Spring AOP和任何方法,而无需在任何服务类中编写任何一行分析代码.面向方面 ...

最新文章

  1. LVS Nginx HAProxy 三种负载均衡优缺点比较
  2. EMOS SPF开启收不到信 及WEB收件箱不显示邮件列表等问题解决处理记录
  3. python爬虫简单示例_最简单爬虫示例(入门级)
  4. java泛型范围_Java泛型类型中的通配符参数在其范围内的正式条件是什么?
  5. 在linux上使用scp命令拷贝一个目录到另一台服务器的时候报not a regular file错误的解决办法...
  6. pc套件 无法连接pc CDC Comms Interface
  7. netapp管理地址_NETAPP存储系统管理员手册.doc
  8. jsp登陆界面链接mysql_用jsp实现网站登录界面的制作,并连接数据库
  9. 安装PyTorch详细过程
  10. bzoj 3772: 精神污染 (主席树+dfs序)
  11. linux如何添加旅游,旅游散记
  12. 【知识图谱问答】DBpedia介绍
  13. 微信小程序编译的错误解决办法:Error: accessSync:fail no such file or directory
  14. 微信小程序-开放标签
  15. POI获取文本单元格的数字变成科学计数法的处理方法
  16. 人手一份!八大危险作业操作规程、作业票证(模板)、安全培训齐了~
  17. 用python实现随机生成银行卡号,输出卡号和密码信息
  18. 美剧中50句经典俚语完美解析
  19. PCF8563时钟芯片(C语言单片机编写)
  20. 【QT开发笔记-基础篇】| 第五章 绘图QPainter | 5.14 平移、旋转、缩放

热门文章

  1. Word文档插入图片的问题
  2. “听我说谢谢你”还能用古诗来说?清华搞了个“据意查句”神器,一键搜索你想要的名言警句...
  3. 课程实训-校园导游系统
  4. 亚马逊发布“不可变”量子账本数据库产品
  5. 数据库初级入门sqlite3版本
  6. Kicad改主题 层颜色 (护眼黑底 层颜色类似立创EDA 或者Altium Designer)
  7. C语言简易程序设计————11、打印楼梯与笑脸
  8. win10资源管理器卡死无响应终极解决办法(亲测有效)
  9. 不用光盘和u盘怎么重装系统win10
  10. [JZOJ3385] [NOIP2013模拟] 黑魔法师之门 解题报告(并查集)