前言
有一定开发经验的同学对AOP应该很了解吧,如果不了解,可以先查看如下文章进行科普一下https://baike.baidu.com/item/AOP/1332219?fr=aladdin,再来阅读本文。
示例前置准备
注: 本示例基于springboot进行演示
1、在项目pom引入aop的GAV
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2、编写业务服务
@Service
public class EchoService {
@CostTimeRecoder
public void echo(String message){
System.out.println("echo ->" + message);
}
}
3、编写aspect切面
@Aspect
public class EchoAspect {
@Before(value = "execution(* com.github.lybgeek.aop.service.EchoService.echo(..))")
public void before(JoinPoint joinPoint){
System.out.println("USE AOP BY ASPECT WITH ARGS: " + Arrays.toString(joinPoint.getArgs()));
}
}
实现AOP的常见套路
1、在编译期阶段实现AOP
方法一:通过aspectj-maven-plugin插件在编译期进行织入
在项目的pom引入如下内容
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<groupId>com.nickwongdev</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.12.6</version>
<configuration>
<complianceLevel>${java.version}</complianceLevel>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.encoding}</encoding>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.5</version>
</dependency>
</dependencies>
通过执行如下maven命令 ,进行项目编译
mvn clean compile
执行测试类
public class AspectMavenPluginMainTest {
public static void main(String[] args) {
EchoService.echo("aspectMavenPlugin");
}
}
发现切面已经执行。我们在查看下生成的EchoService.class文件有没有发生什么变化
public class EchoService {
public EchoService() {
}
public static final void echo(String message) {
JoinPoint var1 = Factory.makeJP(ajc$tjp_0, (Object)null, (Object)null, message);
EchoAspect.aspectOf().before(var1);
System.out.println("echo ->" + message);
}
static {
ajc$preClinit();
}
}
发现多了一些切面的内容。
注: 本示例利用别人重新封装的插件,而非Codehaus的官方提供的插件,Codehaus的官方提供的插件只能支持JDK8(包含JDK8)以下的版本,而本示例的插件可以支持到JDK13
本示例的插件github地址:https://github.com/nickwongdev/aspectj-maven-plugin
Codehaus的官方插件地址:https://github.com/mojohaus/aspectj-maven-plugin
以及相应介绍:https://www.mojohaus.org/aspectj-maven-plugin/index.html
方法二:利用APT + JavaPoet 在编译期实现切面逻辑
如果对于APT不了解的小伙伴,可以查看我之前的文章聊聊如何运用JAVA注解处理器(APT)
而JavaPoet是JavaPoet 是生成 .java 源文件的 Java API,具体查看官方文档
https://github.com/square/javapoet
或者查看此博文
https://weilu.blog.csdn.net/article/details/112429217
不过JavaPoet 只能生产新的代码,无法对原有的代码进行修改。因此在演示此方法时,本文就通过生成一个继承EchoService的子类,来实现AOP功能
生成的子类如下
public final class LybGeekEchoServiceCostTimeRecord extends EchoService {
public LybGeekEchoServiceCostTimeRecord() {
}
public final void echo(String message) {
long startTime = System.currentTimeMillis();
super.echo(message);
long costTime = System.currentTimeMillis() - startTime;
System.out.println("costTime : " + costTime + "ms");
}
}
注: 因为JavaPoet 是通过生成新代码,而非进行在源代码进行插桩,因此也不是很符合我们我要求
方法三:利用APT+AST在编译期进行织入
AST抽象语法树,可以在编译期对字节码进行修改,达到插桩的效果。因之前我有写过一篇文章
聊聊如何通过APT+AST来实现AOP功能
本示例就不贴相应的代码了
2、在JVM进行类加载时进行AOP
核心是用利用aspectjweaver在JVM进行类加载时进行织入。具体实现步骤如下
1、在项目的POM引入aspectjweaver GAV
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
2、创建切面类和需要被织入的目标类
即示例前置准备的内容
3、在src/main/resource目录下创建META-INF/aop.xml文件
<aspectj>
<weaver options="-XnoInline -Xset:weaveJavaxPackages=true -Xlint:ignore -verbose -XmessageHandlerClass:org.springframework.aop.aspectj.AspectJWeaverMessageHandler">
<!--在编织时导入切面类和需要被切入的目标类-->
<include within="com.github.lybgeek.aop.aspect.EchoAspect"/>
<include within="com.github.lybgeek.aop.service.EchoService"/>
</weaver>
<aspects>
<!--指定切面类-->
<aspect name="com.github.lybgeek.aop.aspect.EchoAspect"/>
</aspects>
</aspectj>
4、指定VM参数
-javaagent:aspectjweaver.jar的路径
示例:
-javaagent:D:\repository\org\aspectj\aspectjweaver\1.9.5\aspectjweaver-1.9.5.jar
5、测试
public class AspectjweaverMainTest {
public static void main(String[] args) {
EchoService echoService = new EchoService();
echoService.echo("Aspectjweaver");
}
}
查看控制台
3、在运行时进行AOP
我们以spring aop为例
1、手动代理(直接使用底层API)
主要是利用AspectJProxyFactory 、ProxyFactoryBean 、ProxyFactory
public class AopApiTest {
@Test
public void testAopByAspectJProxyFactory(){
AspectJProxyFactory aspectJProxyFactory = new AspectJProxyFactory(new EchoService());
aspectJProxyFactory.addAspect(EchoAspect.class);
EchoService echoService = aspectJProxyFactory.getProxy();
echoService.echo("AspectJProxyFactory");
}
@Test
public void testAopByProxyFactoryBean(){
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
proxyFactoryBean.setTarget(new EchoService());
AspectJExpressionPointcutAdvisor aspectJExpressionPointcutAdvisor = new AspectJExpressionPointcutAdvisor();
aspectJExpressionPointcutAdvisor.setExpression("execution(* com.github.lybgeek.aop.service.EchoService.echo(..))");
aspectJExpressionPointcutAdvisor.setAdvice((MethodBeforeAdvice) (method, args, target) -> System.out.println("USE AOP BY ASPECT WITH ARGS: " + Arrays.toString(args)));
proxyFactoryBean.addAdvisor(aspectJExpressionPointcutAdvisor);
EchoService echoService = (EchoService) proxyFactoryBean.getObject();
echoService.echo("ProxyFactoryBean");
}
@Test
public void testAopByProxyFactory(){
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(new EchoService());
AspectJExpressionPointcutAdvisor aspectJExpressionPointcutAdvisor = new AspectJExpressionPointcutAdvisor();
aspectJExpressionPointcutAdvisor.setExpression("execution(* com.github.lybgeek.aop.service.EchoService.echo(..))");
aspectJExpressionPointcutAdvisor.setAdvice((MethodBeforeAdvice) (method, args, target) -> System.out.println("USE AOP BY ASPECT WITH ARGS: " + Arrays.toString(args)));
proxyFactory.addAdvisor(aspectJExpressionPointcutAdvisor);
EchoService echoService = (EchoService) proxyFactory.getProxy();
echoService.echo("ProxyFactory");
}
2、自动代理
这个是我们平时用得最多的。自动代理常见实现手段就是在spring bean ioc阶段的后置处理器阶段进行增强
示例
@Configuration
public class AopConfig {
@Bean
public EchoAspect echoAspect(){
return new EchoAspect();
}
}
因为自动代理太常见了,java开发必备技能,就不多做介绍了
总结
本文主要从编译期,JVM加载器期、运行期这三个环节,来讲述如何进行AOP。如果对性能有强烈要求的话,推荐在编译期或者JVM加载期进行织入。如果想对方法修饰符为final、static、private进行织入,也可以考虑在编译期进行实现。不过在编译期或者JVM加载期进行织入有个弊端就是,出现问题不好排查。如果不是对性能有极致要求的话,推荐在运行时,进行AOP进行切入,主要是出现问题,相对好排查。有时候基于业务角度而非技术角度,进行权衡,可能会得出意想不到的效果
demo链接
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-aop