您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
AspectJ浅析系列(三)自定义注解
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
AspectJ浅析系列(三)自定义注解
自猿其说Tech
2021-10-19
IP归属:未知
56840浏览
计算机编程
### 1 前言 读完本系列前两篇的你,是已经能够用AspectJ实现些简单的小功能,比如翻译Hallo World,还能对切入点和通知有了基础的了解和应用,现在你所缺少的就是如何将这个神器应用到实际开发了。 那一天,弱弱的JK分配到一个需求,团队的大哥跟你说,“JK啊,前一阵子你不是在看AOP么,正好也想做个优化,新项目就别打日志和运行时间了,你做个切面吧。”我推推厚厚的眼镜,说“这事没有问题,抓切入点后用环绕通知就可以的!”大哥听到之后点点头,又想了一下,笑着说,”干脆分离出来做成第三方插件吧,那样子其他项目也好用诶。“ 是个新挑战啊,值得研究研究。 ### 2 项目分离 既然大哥说要做成第三方插件,那么正好把之前埋的坑再填一个。 在我们实际开发的过程中,为了模块的复用,会将一个项目拆分成多个项目或者模块。肯定是有一个项目作为主入口,用依赖的方式引入服务和切面的,那么这又是如何起作用的呢?将切面、服务和主进程拆分成三个maven项目,为了更好地契合实际,这一次例子直接分成了三个项目,更方便读者理解。 #### 2.1 服务项目keen-service 第一个项目是服务项目: 在项目A中新建路径cn.jdl.ka.service下的KeenService文件: ```java package cn.jdl.ka.service; public class KeenService { public void square(int a) { System.out.println(a+"的平方是:"+ (int) Math.pow(a, 2)); } } ``` 对应的pom文件为: ```xml <groupId>cn.jdl.ka</groupId> <artifactId>keen-service</artifactId> <version>1.0.0-SNAPSHOT</version> ``` 直接使用mvn install安装到本地仓库,打成jar包,更方便主入口调用。 #### 2.2 主入口keen-main 第二个项目是主入口: 在项目B中新建路径cn.jdl.ka下的KeenMain文件: ```java package cn.jdl.ka; import cn.jdl.ka.service.KeenService; public class KeenMain { public static void main(String[] args) { new KeenService().square(3); } } ``` 直接运行会发现输出一行内容:3的平方是:9。 当然这不是我们的目的,为了更好地方便大家理解,主入口的配置会放在下面和切面项目的配置一起介绍。 #### 2.3 切面项目keen-aspect 第三个项目是切面项目: 在项目C中新建路径cn.jdl.ka.aspect下的KeenAspect文件: ```java package cn.jdl.ka.aspect; @Aspect public class KeenAspect { @Before("execution( * cn.jdl.ka.service.KeenService.*(..))") public void beforeKeenService(JoinPoint joinPoint) { System.out.println("前置通知>>开始运行方法!"); } } ``` 接下来就是重头戏,还是介绍两种方法,一种是编译期织入,一种是JVM加载期织入。因为两种织入时机对应的配置是不同的,所以接下来进一步细分。 #### 2.4 编译期织入 ##### 2.4.1 切面配置 切面项目的pom.xml配置为: ```xml <groupId>cn.jdl.ka</groupId> <artifactId>keen-aspect-compile</artifactId> <version>1.0.0-SNAPSHOT</version> <dependencies> <!--添加依赖--> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.8.13</version> </dependency> </dependencies> <build> <plugins> <!--aspectj编译--> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version>1.11</version> <configuration> <!--对应编译器版本--> <complianceLevel>1.8</complianceLevel> <!--对应java1.8--> <source>1.8</source> <!--对应class文件版本--> <target>1.8</target> <!--展示信息--> <showWeaveInfo>true</showWeaveInfo> <!--忽略警告--> <Xlint>ignore</Xlint> <!--设置通用编码--> <encoding>UTF-8</encoding> </configuration> <executions> <execution> <goals> <!--编织主类--> <goal>compile</goal> </goals> </execution> </executions> </plugin> </plugins> </build> ``` 在pom文件中使用插件进行编译,得到的同名class文件里一定要有这个方法! ```java public static KeenAnnoAspect aspectOf() ``` 将该项目用mvn install安装到本地仓库。 ##### 2.4.2 主入口配置 对应的切面项目pom文件为: ``` <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--服务依赖--> <dependency> <groupId>cn.jdl.ka</groupId> <artifactId>keen-service</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> <!--切面依赖,自带依赖aspectjrt-1.8.13--> <dependency> <groupId>cn.jdl.ka</groupId> <artifactId>keen-aspect-compile</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> </dependencies> <build> <plugins> <!-- AspectJ 编译插件 --> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version>1.11</version> <configuration> <!--服务所在依赖--> <weaveDependencies> <weaveDependency> <groupId>cn.jdl.ka</groupId> <artifactId>keen-service</artifactId> </weaveDependency> </weaveDependencies> <!--切面所在依赖--> <aspectLibraries> <aspectLibrary> <groupId>cn.jdl.ka</groupId> <artifactId>keen-aspect-compile</artifactId> </aspectLibrary> </aspectLibraries> <complianceLevel>1.8</complianceLevel> <source>1.8</source> <target>1.8</target> <showWeaveInfo>true</showWeaveInfo> <Xlint>ignore</Xlint> <encoding>UTF-8</encoding> <showWeaveInfo/> </configuration> <executions> <execution> <goals> <goal>compile</goal> </goals> </execution> </executions> </plugin> <!--运行插件--> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>1.4.0</version> <executions> <execution> <goals> <goal>java</goal> </goals> </execution> </executions> <configuration> <executable>java</executable> <arguments> <argument>-Dfile.encoding=utf-8</argument> <argument>-classpath</argument> <classpath/> <!-- 程序入口,主类名称 --> <argument>cn.jdl.ka.KeenMain</argument> </arguments> </configuration> </plugin> </plugins> </build> ``` 然后使用第一篇介绍的方法,直接运行 ``` mvn clean compile exec ``` 即可看到结果: ![](//img1.jcloudcs.com/developer.jdcloud.com/10484ba9-6ce6-4d4a-9070-e94800e1f84520211019140625.png) #### 2.5 JVM期织入 ##### 2.5.1 切面配置 因为是在JVM类加载期才织入,仅仅是新建一个aop.xml文件来指定依赖所在位置就可以了! resource/META-INF/aop.xml文件: ```xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd"> <aspectj> <aspects> <!-- 向编织器声明一个切面,只支持java文件,不支持aj文件 ,除非切换编译器--> <aspect name="cn.jdl.ka.aspect.KeenAspect"/> </aspects> </aspectj> ``` 切面项目的pom.xml配置为: ```xml <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <groupId>cn.jdl.ka</groupId> <artifactId>keen-aspect-ltw</artifactId> <version>1.0.0-SNAPSHOT</version> <dependencies> <!--添加依赖--> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.8.13</version> </dependency> </dependencies> ``` 仍然是注意要mvn install到本地仓库,主入口项目才能引用。 ##### 2.5.2主入口配置 对应的主入口项目pom文件为: ```xml <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--服务依赖--> <dependency> <groupId>cn.jdl.ka</groupId> <artifactId>keen-service</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> <!--切面依赖,自带依赖aspectjrt-1.8.13--> <dependency> <groupId>cn.jdl.ka</groupId> <artifactId>keen-aspect-ltw</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> <!--织入LTW的依赖包,至少需要导入一次,且版本应与aspectjrt一致--> <!--<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.13</version> </dependency>--> </dependencies> ``` 然后使用第一篇介绍的方法,在运行的时候加上对应的VM参数,即可得到同样的结果! ``` -javaagent:"D:\.m2\repository\org\aspectj\aspectjweaver\1.8.13\aspectjweaver-1.8.13.jar" ``` ### 3 基础知识 回顾第二篇中,注解那一块没有详细地讲解,就是留在这里,更细致且更具体地分析他。 #### 3.1 基础注解 接下来就是要简单说如何去自定义一个注解,之间拿来做例子的Keen注解是这样子定义的: ```java package cn.jdl.ka.component; import java.lang.annotation.*; @Inherited @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface Keen { //该注解实例是否禁用 boolean unable() default false; //字符串内容 String STRING() default ""; } ``` ###### 1)@Inherited的作用 @Inherited的原文机翻后是: 如果注释类型用于注释类以外的任何内容,则此元注释类型无效。还要注意,这个元注释只会导致注释从超类继承;已实现接口上的注释无效。 在我看来@Inherited在继承关系情况下,无非就是分为两种情况: - 类继承关系中,如果父类使用的Keen注解中是被@Inherited修饰的注解,那么子类会继承。 - 接口继承关系中,子接口或者子实现是不会继承父接口中的Keen注解。 ###### 2)@Retention的作用 通过枚举常数java.lang.annotation.RetentionPolicy指示要注释具有注释类型的注释的保留时间。 ![](//img1.jcloudcs.com/developer.jdcloud.com/b684ffb3-e37e-412e-aa4c-f589c31b1a3620211019141056.png) ###### 3)@Target的作用 通过枚举常数java.lang.annotation.ElementType表示可能适用的声明上下文和类型上下文。 ![](//img1.jcloudcs.com/developer.jdcloud.com/7a8dfbc5-47d2-4579-8131-05ba49eee85a20211019141140.png) #### 3.2 Aspectj 总的来说,在AspectJ中基于注解的切入点有以下六种类型: ![](//img1.jcloudcs.com/developer.jdcloud.com/bbb1f893-e39c-4703-b192-c8ea139e711020211019141213.png) 不同的情况应该选用不同的关键字。 所有传入的注解必须是由@Retention(RetentionPolicy.RUNTIME)特别标注了的才可以,否则不起效果。 ![](//img1.jcloudcs.com/developer.jdcloud.com/38b97fb1-9213-47c7-b868-5d718ac1d67d20211019141242.png) 接下来的仍然是以第二篇中出现过的例子做测试,为了本文的简洁性,不再附上了,如果不是很理解仍然可以拉取我附在文末的例子。 ##### 2.2.1 @this和@target 倘若Keen注解打在了类Bird上面,那么对于@this实际的语句就是: ```java @Pointcut("this(cn.jdl.ka.service.impl.Bird)") ``` 那么对于@target实际的语句就是: ```java @Pointcut("target(cn.jdl.ka.service.impl.Bird)") ``` 真正运行的效果已经在第二篇的1.2.6部分中点明了,故而不再复述。 ##### 3.2.2 @args 特别的,给鸟添加一个吃蛇的方法: ```java public class Bird implements Animal { public void move() { System.out.println("鸟在飞"); } public void eat(Snake snake){ System.out.println("鸟吃蛇:"+snake); } } ``` 此时将Bird的注解去掉,Keen注解打在了类Snake上面,可以抓到的切入点为: 1. eat方法的调用 1. eat方法的运行 1. 内部StringBuilder.append方法的调用 ##### 3.2.3 @within 倘若Keen注解打在了类KeenService上面,那么实际的语句就是: ```java @Pointcut("within(cn.jdl.ka.service.KeenService)") ``` 真正运行的效果已经在第二篇的1.2部分中点明了,故而不再复述。 可以取反来去除对某类的切入,较within更为灵活。 ##### 3.2.4 @withincode 这个注解比较特别,只有Keen注解打在了方法上面才起效果。不会对方法本身进行切入,而是对该方法的内部调用其他方法进行切入。 倘若注解打在了方法move上面,形如 : ```java @Keen public void move() { System.out.println("蛇在爬"); } ``` 那么实际的语句就是: ```java @Pointcut("withincode(* cn.jdl.ka.service.KeenService.move())") ``` 所切入的则是: ```java get(PrintStream java.lang.System.out) call(void java.io.PrintStream.println(String)) ``` 实际应用中很少,因为真正用的时候,更多的是为了排除对嵌套方法的切入,表达式可以写作: ```java @Pointcut(" within(cn.jdl.ka.service.impl.Snake) && !withincode(* cn.jdl.ka.service.impl.Snake.*(..))") ``` 但是用注解方式排除某方法的内部嵌套切入却是会报错的(visiting a false expression)。 ##### 3.2.5 @annotation 将书中对该注解的内容机翻出来便是: 包含指定注释的任何连接点。 1. 对于方法、构造函数和通知执行连接点,切入点与程序元素相同。 1. 对于字段访问和异常句柄连接点,切入点是要访问的字段或异常。 1. 对于初始化和预初始化连接点,切入点将使用与指定签名匹配的第一个被调用构造函数。 1. 对于静态初始化连接点,切入点是正在初始化的类型 至于如何使用,下文已经用了两个详细的例子细致地讲解了。 ### 4 自定义注解 经过上面的拆分,我们就可以明白是如何将项目拆分的,多个项目又是如何一起运行的,JK已经将切面做成第三方插件了。所以下面的就只需要详细说如何实现注解切面了,如果担心没办法复现,可以拉取我附在文末的项目。 现在我们再细化一下大哥留给我的需求: 1. 要自定义一个注解@Log,所有打上该注解的方法都都会用日志的方式打印入参和出参。经过前两篇文章的介绍,伙伴们已经知道了,在编译期织入和在JVM期织入的配置方式是不同的,所以是一定要分成两个module的。但是又有一个毋庸置疑的事实摆在我们面前,在真正使用的时候,肯定是用的同一个注解,所以我们要先将注解和API单独地抽调出来做为一个module。 1. 再自定义一个注解@RunTime,所有打上该注解的方法都会输出方法的运行时间。并且针对某一个路径内的所有公开方法只要运行都要输出运行时间,还不能和注解发生冲突。 #### 4.1 优雅地打日志 首先将注解和API都抽出成为一个单独的module。在这里多插一嘴,如何才能够优雅地打日志呢? ##### 4.1.1 基础知识 首先,Slf4j是一个日志框架,它是对所有日志框架制定的一种规范、标准、接口,但并不能独立使用。 其次,Log4j2是一个日志实现,它是一种记录日志的具体实现。Log4j2是对Log4j的改进,并提供了许多Logback可用的改进,同时解决了Logback体系结构中的一些固有问题。他自己也有配套的日志门面。 最后,用桥接器,将Log4j2日志门面桥接至Slf4j日志门面。 现如今,以Slf4j和Log4j2的使用组合最为常见,故而最推荐此法。 ##### 4.1.2 引入依赖和添加配置 pom.xml依赖: ```xml <properties> <log4j.version>2.14.0</log4j.version> <slf4j.version>1.7.25</slf4j.version> </properties> <dependencies> <!--log4j2 开始--> <!--slf4j依赖,是一种日志框架,单独的话缺少实现--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <!--桥接包log4j-slf4j-impl起到适配的作用,版本须对应log4j-core的版本--> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>${log4j.version}</version> </dependency> <!--log4j-core是log4j2的具体实现--> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>${log4j.version}</version> </dependency> <!--log4j2 结束--> </dependencies> ``` resources/log4j2.xml配置文件: 为了节省篇幅,在这里只是简单说一下配置控制台和文件,更多的配置请参考我的另一篇文章。 ##### 4.1.3 打印日志 接下来就要说一下如何应用去打印日志了。 平常的时候,我们都是这样子应用的: ```java public class KeenMain { private static Logger logger= LoggerFactory.getLogger(KeenMain.class); public static void main(String[] args) { logger.error("error"); logger.warn("warn"); logger.info("info"); logger.debug("debug"); logger.trace("trace"); } } ``` 但是在需要打印日志的地方之前是必须要有第二行语句的存在来构造一个静态常量的,未免有些冗余了,可以进一步简化和包装的。虽然这样包装会出现一点点小问题,就是没办法准确打印方法名和行数,但是进一步简化了。 cn.jdl.ka.rip.component.log.LoggingUtils.java文件: ```java package cn.jdl.ka.rip.component.log; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class LoggingUtils { private static Logger logger = LoggerFactory.getLogger(LoggingUtils.class); public static boolean isDebugEnabled() { return logger.isDebugEnabled(); } public static boolean isInfoEnabled() { return logger.isInfoEnabled(); } public static boolean isErrorEnabled() { return logger.isErrorEnabled(); } public static void debug(Class clazz, String msg, Object... args) { if (clazz != null) { logger = LoggerFactory.getLogger(clazz); } logger.debug(msg, args); } public static void info(Class clazz, String msg, Object... args) { if (clazz != null) { logger = LoggerFactory.getLogger(clazz); } logger.info(msg, args); } public static void error(Class clazz, String msg, Object... args) { if (clazz != null) { logger = LoggerFactory.getLogger(clazz); } logger.error(msg, args); } } ``` 于是只需要直接使用就可以了: ```java package cn.jdl.ka.rip; import cn.jdl.ka.rip.component.log.LoggingUtils; public class KeenMain { public static void main(String[] args) { LoggingUtils.info(KeenMain.class, "打印日志"); LoggingUtils.info(KeenMain.class, "【方法】{}", "main"); } } ``` 在传参的时候将想要打印的类名和信息一起传入进去就可以了,还支持传入多个参数来组装复杂信息。 #### 4.2 切面项目 ##### 4.2.1 新建注解 只需要针对方法级别,一定要有一个属性值来判断加上这个注解的有效性。 首先是打印日志的注解,Log: ```java package cn.jdl.ka.rip.component.log; import java.lang.annotation.*; @Inherited @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Log { /** * 默认开启日志 * @return */ boolean printLog() default true; } ``` 还有运行时长的注解RunTime: ```java package cn.jdl.ka.rip.component.runTime; import java.lang.annotation.*; @Inherited @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface RunTime { /** * 默认开启打印运行时间 * @return */ boolean printRunTime() default true; } ``` ##### 4.2.2 依赖和服务 引入注解所在的依赖,再引入aspectj的依赖即可。 ```xml <!--注解依赖--> <dependency> <groupId>cn.jdl.ka</groupId> <artifactId>keen-annotation</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> <!--aspectj依赖--> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.8.13</version> </dependency> ``` 服务类提供四个方法,add1为不使用注解;add2使用注解且入参非空,出参为空;add3使用注解且正好与第二个相反,入参为空,出参非空;print则是用来计算方法运行时间的 ```java package cn.jdl.ka.service; public class KeenService { public void add1(int a) { LoggingUtils.info(KeenService.class,"参数加一结果为:{}", a + 1); } @Log public void add2(int a) { LoggingUtils.info(KeenService.class,"参数加二结果为:{}", a + 2); } @Log public int add3() { LoggingUtils.info(KeenService.class,"反馈结果为:{}",3); return 3; } @RunTime public void print(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } LoggingUtils.info(KeenService.class,"打印字符串"); } } ``` 主入口则是简单些: ```java public static void main(String[] args) { KeenService keenService = new KeenService(); keenService.add1(3); keenService.add2(3); keenService.add3(); keenService.print(); } ``` ##### 4.2.3 日志的切面实现 在切面实现的部分则是需要多加注意,我着重考虑的有以下几点: 首先也是最重要的,就是要精确地找到切入点,匹配规则是所有携带了注解的公开方法,运行的时候都要切入。 ```java //针对日志注解切面 @Pointcut("execution(public * *(..)) && @annotation(cn.jdl.ka.rip.component.log.Log)") private void log() {} ``` 其次,要考虑使用哪种通知方法。用@Before、@AfterRunning和@AfterThrowing组合使用是可以实现相同效果的,本文使用环绕通知实现。 假设现在已经切入了目标方法,那么我们就要先获得注解的属性值,确定我们可以对该方法进行切入。 ```java // 获取切入的 Method和注解实例 MethodSignature joinPointObject = (MethodSignature) point.getSignature(); Method method = joinPointObject.getMethod(); Log logAnnotation = method.getAnnotation(cn.jdl.ka.rip.component.log.Log.class); // 获取参数,类名,方法名 Object[] args = point.getArgs(); Class declaringType = joinPointObject.getDeclaringType(); String methodName = method.getName(); if (logAnnotation != null && logAnnotation.printLog()) { //可以打印入参和出参 }else{ //不能打印,直接运行原方法 try { result = point.proceed(); } catch (Throwable throwable) { throw throwable; } } ``` 接着,如果参数是复杂些的对象的话,那么必然是需要经过序列化为字符串,才能用日志输出出来。 现在考虑一下这个情形,一个对象序列化需要300ms,输出级别为debug级别。然而当前日志输出的最小级别是info,没有办法输出内容,但是直接花费了不必要的时间去序列化,这是不合常理的。 所以需要在打印日志前先简单地判别一次,当前日志输出的级别是否允许输出该条日志,倘若允许,再序列化和打印不迟。 再考虑一下,倘若入参和出参某一个结果为空呢?仍是建议先判断再打印,倘若真的非空再序列化也不迟,如果是空,那么输出固定字符串也就可以了。 最后,如果程序运行出现异常,那么必然要打印日志,但是注意一定要抛出异常! ```java try { //运行 函数并打印日志 if (LoggingUtils.isInfoEnabled()) { LoggingUtils.info(declaringType, "【方法名为:】{},【入参:】{}", methodName, 0 == args.length ? "void" : JSON.toJSONString(args)); } result = point.proceed(); if (LoggingUtils.isInfoEnabled()) { LoggingUtils.info(declaringType, "【方法名为:】{},【出参:】{}", methodName, null == result ? "void" : JSON.toJSONString(result)); } } catch (Throwable throwable) { LoggingUtils.error(method.getDeclaringClass(), "【方法名为:】:{},【入参:】:{}, " + "发生系统异常,【异常信息为:】", new Object[]{methodName, 0 == args.length ? "void" : JSON.toJSONString(args), throwable}); throw throwable; } ``` ![](//img1.jcloudcs.com/developer.jdcloud.com/027a2b1a-3d66-4d65-8f38-7ddaf69a631520211019151556.png) ##### 4.2.4 运行时间的切面实现 首先也是最重要的,就是要精确地找到切入点,匹配规则是在cn.jdl.ka.service包下所有携带了注解的公开方法,运行的时候都要切入。 ```java //针对日志注解切面 @Pointcut("execution(public * cn.jdl.ka.service..*(..)) && @annotation(cn.jdl.ka.rip.component.runTime.RunTime)") private void runTime() {} ``` 第二步与其他相似,也是选择环绕通知,在里面获取待打印信息和判断是否存在注解。 ```java Object result = null; // 获取切入的 Method和注解实例 MethodSignature joinPointObject = (MethodSignature) point.getSignature(); Method method = joinPointObject.getMethod(); RunTime runTimeAnnotation = method.getAnnotation(RunTime.class); // 获取参数,类名,方法名 Object[] args = point.getArgs(); Class declaringType = joinPointObject.getDeclaringType(); String methodName = method.getName(); if (logAnnotation != null && logAnnotation.printLog()) { //可以输出运行时间 }else{ //不能打印,直接运行原方法 try { result = point.proceed(); } catch (Throwable throwable) { throw throwable; } } ``` 到了真正可以输出运行时间的时候,则是要注意了,需要多加考量。 ```java private Date date = null; ``` 运行完方法后需要将当前时间减去刚刚记录的时间,差值便是方法的运行时间了。 ```java try { date=new Date(); //运行 函数并打印日志 if (LoggingUtils.isInfoEnabled()) { LoggingUtils.info(declaringType, "【方法名为:】{},【开始运行时间为:】{}", new Object[]{methodName, new SimpleDateFormat("yyyy年MM月dd日E HH时mm分ss秒").format(date)}); } result = point.proceed(); if (LoggingUtils.isInfoEnabled()) { LoggingUtils.info(declaringType, "【方法名为:】{},【运行时长为:】{}", new Object[]{methodName, System.currentTimeMillis()-date.getTime()+ "ms"}); } } catch (Throwable throwable) { LoggingUtils.error(method.getDeclaringClass(), "【方法名为:】:{},【入参:】:{}, " + "发生系统异常,【异常信息为:】", new Object[]{methodName, 0 == args.length ? "void" : JSON.toJSONString(args), throwable}); throw throwable; } ``` ![](//img1.jcloudcs.com/developer.jdcloud.com/7f423a33-5f94-4d04-8766-ec248318f22b20211019151743.png) ##### 4.2.5 接口使用注解的切面实现 这一节是在之后才考虑可能用到的情形,是要求对某个接口的所有实现方法进行切入,但是不对实现类对象调用方法进行切入。 接口和实现方法内容如下: ```java package cn.jdl.ka.service; public interface KeenInterface { @Log public int add10(int a); } package cn.jdl.ka.service.impl; public class keenServiceImpl_1 implements KeenInterface { @Override public int add10(int a) { LoggingUtils.info(keenServiceImpl_1.class,"参数加十结果为:{}", a + 10); return 0; } } package cn.jdl.ka.service.impl; public class keenServiceImpl_2 implements KeenInterface { @Override public int add10(int a) { LoggingUtils.info(keenServiceImpl_2.class,"参数加十结果为:{}", a + 10); return 0; } } ``` 与上文4.2.3的区别就是切入点的选择,需要精确地找到调用切入点! ```java //针对日志注解切面 @Pointcut("call(* cn.jdl.ka.service.KeenInterface.*(..) ) && @annotation(cn.jdl.ka.rip.component.log.Log) ") private void log() {} ``` 通知没有变化,可以测试一下效果: ```java public static void main(String[] args) { // 实际方法调用 new keenServiceImpl_1().add10(3); new keenServiceImpl_2().add10(3); // 接口方法调用 KeenInterface keenInterface = new keenServiceImpl_1(); keenInterface.add10(3); keenInterface = new keenServiceImpl_2(); keenInterface.add10(3); } ``` ### 5 小结 读到这里,你已经完成了项目拆分,可以用Aspectj自研插件了。不仅理论上对注解切入点有了进一步的了解,还实现简单的项目,成功地打印入参出参和打印运行时间了。 但是,或许你会有一点点疑问,这就是AspectJ的全部魅力了么?当然不会的,AspectJ不仅可以在运行的时候织入通知,还可以在编译期织入哦!跟Spring AOP的同台竞技也很精彩的。敬请期待! #### 参考资料 - 项目链接:https://coding.jd.com/weijikuo/keenTest-aop.git - AspectJ In Ation 第二版 - AspectJ Cookbook中文版 - Log4j2 中文文档: https://www.docs4dev.com/docs/zh/log4j2/2.x/all/manual-customconfig.html - Java8 API:https://www.matools.com/api/java8 ------------ ###### 自猿其说Tech-JDL京东物流技术发展部 ###### 作者:销售发展技术部 魏继扩
原创文章,需联系作者,授权转载
上一篇:打日志的优雅身姿
下一篇:京东快递APP对Flutter 2.0空安全的适配
相关文章
Taro小程序跨端开发入门实战
Flutter For Web实践
配运基础数据缓存瘦身实践
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号