详解面向切面AOP
AOP
再看AOP,这是一种面向切面编程思想,相比面向对象编程,可以说是站在更改维度关注对象,我们知道,对象包含由属性和行为。 基于AOP,我们可以把一段代码插入到对象中形成新的对象,这是织入的过程,目的是将公共的内容写入到业务代码中,通过配置或简单的编码完成整个过程。 这样一来不用修改原有的业务代码,同时又能自由完成目标代码的增强,按照代码的设计思想,确实是降低业务与功能的耦合。
大部分框架都是为我们提供切面织入目标过程的封装。
实现
通过该图可以看到AOP相关的实现主要包括ASM、Cglib、JDK Proxy、AspectJ、Javassit,这些实现主要都是对字节码直接操作,只不过对目标对象的增强可以发生在编译时、编译后或运行时。
关于AOP我们说的比较多的就是代理,这属于设计模式的一种,但是AOP真正做的不仅仅是对目标的代理,更多的是修改,像我们常用的代理工具Cglib、JDK Proxy,都是基于面向对象的特性,生成新的 目标对象,通过继承与代理模式来实现最终的增强效果。
在Java中,大部分情况下都是对方法的增强,比如Spring AOP,这样可以解决几乎所有的业务问题;当然切点不局限于类方法,还可以包括字段、方法、构造函数、静态初始值等,比如AspectJ,只不过需要特定的 编译器来实现。
下面我们看下剩下的几项实现AOP的技术,前面说到,Spring AOP主要基于Cglib、JDK Proxy,在运行时实现目标对象的代理。但是Spring中却引入了aspectj相关的依赖,但没有用到AspectJ编译器
JDK Proxy
JDK动态代理,主要是基于目标接口,通过ByteArrayOutputStream直接构建字节数组,最终生成代理接口的实现类,基于InvocationHandler实现代码的扩展与增强,通过反射来调用目标代码的调用。
1.目标接口
1 2 3 4 5 | public interface HelloService { String hello(String name); } |
2.目标实现类
1 2 3 4 5 6 7 8 9 | @Slf4j public class HelloServiceImpl implements HelloService{ @Override public String hello(String name) { log.info( "+++ 执行方法:hello" ); return String.format( "hello, %s" , name); } } |
3.代理工厂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | public class JdkProxyFactory { public static <T> T create(Class<T> targetClass, InvocationHandler invocationHandler){ return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{targetClass}, invocationHandler); } @Slf4j public static class LogInvocationHandler implements InvocationHandler{ private Object target; public LogInvocationHandler(Object target) { this .target = target; } /** * * @param proxy * @param method * @param args * * @return * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { log.info( ">>> before" ); Object result = method.invoke(target, args); // 执行被代理方法 log.info( ">>> afterReturning : {}" , result); return result; } catch (Throwable e) { log.info( ">>> afterThrowing : {}" , e.getMessage()); throw e; } finally { log.info( ">>> after" ); } } } } |
4.执行测试
1 2 3 4 5 6 7 8 9 | public class JdkProxyTests { @Test public void testJdkProxy(){ HelloService helloService = JdkProxyFactory.create(HelloService. class , new JdkProxyFactory.LogInvocationHandler( new HelloServiceImpl())); helloService.hello( "JDK Proxy" ); } } |
Cglib
Cglib基于目标类来实现代理,已目标类为参考基于ASM直接操作字节码,构造目标对象的子类行,基于MethodInterceptor接口实现目标代码的增强,通过父类调用来执行原目标代码,因此在执行效率上会高于JDK动态代理。
添加依赖
1 2 3 4 5 | <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version> 3.3 . 0 </version> </dependency> |
1.目标类
1 2 3 4 5 6 7 8 | @Slf4j public class HiService { public String hi(String name){ log.info( "+++ 执行方法:hi" ); return String.format( "hi, %s" , name); } } |
2.代理工厂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | public class CglibFactory{ /** * * @param targetClass * @param methodInterceptor * @return * @param <T> */ public static <T> T create(Class<T> targetClass, MethodInterceptor methodInterceptor){ Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(targetClass); enhancer.setCallback(methodInterceptor); return (T) enhancer.create(); } @Slf4j public static class LogMethodInterceptor implements MethodInterceptor { /** * * @param target 目标对象 * @param method 目标方法 * @param args 参数 * @param methodProxy 代理方法,注意执行方式 methodProxy.invokeSuper * @return * @throws Throwable */ @Override public Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { try { log.info( ">>> before" ); Object result = methodProxy.invokeSuper(target, args); // 执行被代理方法 log.info( ">>> afterReturning : {}" , result); return result; } catch (Throwable e) { log.info( ">>> afterThrowing : {}" , e.getMessage()); throw e; } finally { log.info( ">>> after" ); } } } } |
3.执行测试
1 2 3 4 5 6 7 8 9 10 11 12 | public class CglibTests { /** * */ @Test public void testCglib(){ HiService hiService = CglibFactory.create(HiService. class , new CglibFactory.LogMethodInterceptor()); hiService.hi( "Cglib" ); } } |
AspectJ
AspectJ是一个功能强大的面向切面编程框架,是对Java面向对象的扩展,支持编译时、编译后、加载时为目标对象(不仅仅是类方法)织入代理。
切面织入时机:
- 编译期织入(compiler-time weaving):在类进行编译的时候就将相应的代码织入到元类文件的.class文件中
- 编译后织入(post-compiler weaving):在类编译后,再将相关的代码织入到.class文件中
- 加载时织入(load-time weaving):在JVM加载.class 文件的时候将代码织入
我们可以通过AspectJ编译器或者maven插件aspectj-maven-plugin来实现。
AspectJ编译器
- 下载
aspectj
- 安装
java -jar aspectj-1.9.6.jar 配置环境变量PATH与系统变量CLASSPATH
- 使用
通过下面的命令可实现编译时织入的效果:
1 2 | # ajc [Options] [file... | @file ... | -argfile file...] ajc - 1.8 -sourceroots .\src\main\java\ -cp %CLASS_PATH% -outjar main.jar |
通过ajc编译后并打包成main.jar,即是编译时实现了目标对象的代理,通过反编译工具可以查看到编译后的目标对象已经被修改。
AspectJ使用
编译时织入(Compile-Time Weaving)
编译时织入
1.目标对象:
1 2 3 4 5 6 7 | public class CTWObject { public void run() { System.out.println( "-- Compile-Time Weaving --" ); } } |
2.Aspect:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public aspect CTWAspect { pointcut pc(): execution(* com.sucl.blog.aspectj.target.CTWObject.*()); before(): pc(){ System.out.println( " >> before CTW << " ); } void around(): pc(){ System.out.println( " >> around before CTW << " ); proceed(); System.out.println( " >> around before CTW << " ); } after(): pc(){ System.out.println( " >> after CTW << " ); } } |
3.配置maven插件 aspectj-maven-plugin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <!-- 编译期织入 --> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version> 1.14 . 0 </version> <configuration> <complianceLevel> 1.8 </complianceLevel> <source> 1.8 </source> <target> 1.8 </target> <showWeaveInfo> true </showWeaveInfo> <verbose> true </verbose> <Xlint>ignore</Xlint> <encoding>UTF- 8 </encoding> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>test-compile</goal> </goals> </execution> </executions> </plugin> |
4.执行测试
1 2 3 4 5 6 7 8 | public class AspectJCTWTests { @Test public void call() { CTWObject CTWObject = new CTWObject(); CTWObject.run(); } } |
编译后织入(Post-Compile Weaving)
针对编译好的文件,比如jar中的class文件
1.编写测试的目标对象,并打包成jar文件
1 2 3 4 5 6 7 | public class PCWObject { public void run() { System.out.println( "-- Post-Compile Weaving --" ); } } |
2.引入上面的目标jar
1 2 3 4 5 | <dependency> <groupId>com.sucls.blog</groupId> <artifactId>PCW-target</artifactId> <version>${project.version}</version> </dependency> |
3.配置maven插件 aspectj-maven-plugin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <!-- 编译后织入 --> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <!-- <version> 1.14 . 0 </version>--> <version> 1.11 </version> <configuration> <complianceLevel> 1.8 </complianceLevel> <source> 1.8 </source> <target> 1.8 </target> <encoding>UTF- 8 </encoding> <weaveDependencies> <weaveDependency> <groupId>com.sucls.blog</groupId> <artifactId>PCW-target</artifactId> </weaveDependency> </weaveDependencies> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>test-compile</goal> </goals> </execution> </executions> </plugin> |
4.编译
1 | mvn clean compile |
5.执行测试
1 2 3 4 5 6 7 8 | public class AspectJPCWTests { @Test public void call(){ PCWObject pcwObject = new PCWObject(); pcwObject.run(); } } |
运行时织入(Load-Time Weaving)
配置VM参数
1 | -javaagent:${project.basedir}/lib/aspectjweaver- 1.9 . 7 .jar |
或者配置maven-surefire-plugin
插件
1 2 3 4 5 6 7 8 9 10 11 12 | <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version> 2.10 </version> <configuration> <argLine> -javaagent:${project.basedir}/lib/aspectjweaver- 1.9 . 7 .jar </argLine> <useSystemClassLoader> true </useSystemClassLoader> <forkMode>always</forkMode> </configuration> </plugin> |
- 配置aop.xml
/src/main/resources/META-INF/aop.xml
1 2 3 4 5 6 | <aspectj> <aspects> <!-- 以 @Aspect 形式编写切面(aj需要对应编译器编译)--> <aspect name= "com.sucl.blog.aspectj.aspect.LogAspect" /> </aspects> </aspectj> |
启动测试
1 2 3 4 5 6 7 8 | public class AspectJLTWTests { @Test public void call(){ LTWObject LTWObject = new LTWObject(); LTWObject.run(); } } |
结束语
不管是javassit,还是jdk proxy或者cglib来实现AOP,都是通过对字节码的修改,只不过对字节码操作方式不一样。通过上面的例子我们可以认识到各种AOP框架的使用方式。在究其原理时, 能够能够知道这些工具到底为我们做了什么。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
· Manus的开源复刻OpenManus初探