您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
AspectJ浅析系列四静态横切
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
AspectJ浅析系列四静态横切
自猿其说Tech
2021-12-24
IP归属:未知
40560浏览
计算机编程
### 1 前言 哈咯,大家好啊!还是我,JK!今天要介绍的是静态横切的部分内容,静态横切的作用就是修改程序结构的。 它有多种表现形式:引入成员、将新类型声明为父类型、附加注释、编译时错误和警告以及异常软化。将这些再归类一下,可以分为三类:织入成员,织入声明和异常软化。 这一篇我们先介绍一下传统语法和`@AspectJ`的语法的不同,再说一下织入声明和异常软化两部分。织入成员部分因为JK我一不小心唠叨了些,就很繁杂,我再精简精简,嘻嘻。 由于静态横切很大程度上依赖于编译时行为,故而并非所有功能都可以用`@AspectJ`语法实现,所以在本文中会以传统语法的应用为主体,以`@AspectJ`语法为辅助。 因为本篇文章更多的是在编译过程中起作用,我会尽量用直观的效果呈现给大家,大家可以直接拉取我附在文末的项目,就会更加了解了。 ### 2 传统语法和@AspectJ 在聊静态横切之前,有必要提前介绍一点儿传统语法的内容。下图中左侧是传统语法,右侧是`@AspectJ`语法。 ![](//img1.jcloudcs.com/developer.jdcloud.com/edd4be75-68e2-4ac9-9794-1a60c13b5f8b20211224111115.png) 相较于传统语法,`@AspectJ`语法支持诸如切面、切入点、通知、修改实现或继承结构和织入声明等大部分重要的切面关键字。但是像引入成员,处理异常和切面私有等功能都不支持。 ![](//img1.jcloudcs.com/developer.jdcloud.com/8fe47088-28cf-4050-9e2a-80db3cfd0e4e20211224111127.png) 接下来我们从易学性、冗长性、功能性、工具友好性和易用性等方面进行比较,帮助你根据具体需要选择正确的语法。 #### 2.1 易学性 人们一直认为@AspectJ语法更容易,因为它是“纯Java”。但事实上,我们仍然需要学习`pointcut`和`advice`语义,因为Java编译器不会对其进行修改。从咱们程序员的角度来看,`@AspectJ`只是因为有对注解这个概念更为了解,所以更倾向于使用而已。 #### 2.2 冗长性 传统语法是最简洁的,因为它专门设计用于表示AOP结构。此外,传统语法可以使用引用import语句的类型,从而缩短切入点表达式。如果计划大量使用切面时,可以考虑用传统语法的。 与之相比,`@AspectJ`语法必须始终使用完全限定的类型,这可能导致读取切入点表达式的困难。如果是少量的应用,选择哪个并无太大的影响。 #### 2.3 功能性 由于传统语法需要使用特殊编译器,它将程序元素从一些Java规则中解放出来(如允许ITD和异常软化),并促进了高级应用程序的AOP。 如果咱们只需要动态横切,那么你是不会觉得`@AspectJ`比传统语法功能弱很多。但是如果必须使用静态横切,那么对于那一部分的功能则是必须使用传统语法了。 #### 2.4 工具友好性 大多数Java工具都不熟悉传统语法,这会给这些工具带来困难。与之相反,`@AspectJ`语法的友好性更强,可以被视为常规Java类。由于切面会编译为符合标准的字节码,因此它们的语法在工具友好性方面没有区别。 在`Eclipse`中,可以获得更多的支持,例如横切参考视图,查看交叉引用视图并可视化程序元素和切面之间的关系等。 `IntellijIDEA`只是将切面作为Java代码处理,只不过`IntellijIDEA`就没办法查看交叉引用视图之类的支持了,而且社区版是不支持`*.aj`类型的文件提示的,需要升级到付费的专业版才可以。 #### 2.5 易用性 1. 如果应用了`Spring`框架,那么可以使用`Spring`的基于`Proxy`的`AOP`,它是支持@AspectJ语法,而无需构建时或加载时编织。Spring使用切面中的信息围绕bean创建代理来实现横切功能。如果使用`Eclipse`还可以用一个名为`SpringIDE`的插件来查看交叉引用视图。 1. 如果Spring基于代理的AOP满足不了需求时,可以继续使用@AspectJ语法,在编译期或者类加载期进行织入,只需要使用`Maven`的编译插件或者是`Spring-driven ITD`即可。 1. 如果要应用到静态切面时,必要时可以使用传统语法编写一些切面。 ### 3 织入声明WTD 织入声明(weave-time declaration)原文是: ![](//img1.jcloudcs.com/developer.jdcloud.com/d0d51d69-df01-4539-b5ce-6d54381ea1e720211224111231.png) 主要是修改类编译阶段的行为,有两种类型: 1. 编译时添加警告; 1. 编译时添加错误; 倘若现在有一个方法,是不安全的,那么在调用该方法时就应该进行提示。 ```java package cn.jdl.ka.service; public class KeenService { public void unSave(){ System.out.println("该方法不安全"); } } ``` #### 3.1 编译时警告 要求字符串属性同时是静态和终态的,以此保证数值不会发生变化。 注解内数值是支持切入点通配符语法的,但是要注意在这里没办法使用`this`和`target`的关键字,可以使用`within`关键字替代。 字符串的数值是编织器在检测到匹配连接点时发出的消息。 倘若某个程序调用了`unSave`方法,那么我要求在调用时就发出警告来提醒调用者,这个方法是不安全的! ```java @Aspect public class KeenAspectWTD { @DeclareWarning(value="call( * cn.jdl.ka.service.KeenService.unSave())") static final String warningCallPrint = "警告!你所调用的unsave方法是不安全的!"; } ``` 如果使用传统语法则是: ``` declare warning : call( * cn.jdl.ka.service.KeenService.unSave()) : "警告!你所调用的unsave方法是不安全的!"; ``` 这样子其他类调用后,在编译过程中就会出现警告。 ![](//img1.jcloudcs.com/developer.jdcloud.com/521c2fc7-dd25-4fa9-8538-4a22daad195620211224111601.png) 当然,如果调用方忽略了该警告,可以正常地运行程序,只不过控制台打印出来后就会是这个样子: ![](//img1.jcloudcs.com/developer.jdcloud.com/5038b8dd-c104-4d70-a0b0-3e3458f4f43a20211224111615.png) ##### 3.2 编译时错误 与上面的警告相似的,使用该关键字可以让不安全的方法在其他人调用的时候,于编译期直接报错。 ```java @Aspect public class KeenAspectWTD { @DeclareError(value = "call( * cn.jdl.ka.service.KeenService.unSave())") static final String errorCallPrint = "错误!你所调用的unsave方法是不安全的!"; } ``` 如果使用传统语法则是: ``` declare error : call( * cn.jdl.ka.service.KeenService.unSave()) : "错误!你所调用的unsave方法是不安全的!"; ``` ![](//img1.jcloudcs.com/developer.jdcloud.com/ca478f57-d4e9-440b-b5c7-c38742ca347520211224111853.png) 与上文相同的是,如果调用者主观地忽略了该错误,那么程序仍然是可以正常运行的。因为虽然会抛出错误,但是真正的class已经编译成功了。 ![](//img1.jcloudcs.com/developer.jdcloud.com/13c5b669-d41d-436a-8b98-ee409ba1148a20211224111916.png) #### 3.3 对比 发现编译时警告和编译时错误就仅仅是输出的内容不一样,难道功能的设计之初,就只是为了区分编译时的状态消息?! 后来,在打包父级项目的时候,我发现了不同之处。 ![](//img1.jcloudcs.com/developer.jdcloud.com/bbe3c679-f07f-4215-80bc-fe9a89fb561b20211224111937.png) 一个父级项目包括多个子模块,在其中`keen-wtd`模块中编译时发生错误,会直接抛出该错误并且会停止编译接下来的所有子模块。 而之前的子模块也有发生了编译异常的,但是却不会跳过其他子模块。 #### 4 异常软化ExceptionSoft ##### 4.1 异常分类 说起异常,那是个大家族,族谱如下: ![](//img1.jcloudcs.com/developer.jdcloud.com/16eae51a-9345-48e8-93b3-e4bfa1df97f020211224112307.png) 通常,Java的异常(包括`Exception`和`Error`)可以分为检查异常和未检查的异常。 ##### 4.1.1 已检查异常 什么是已检查异常呢? 所有除了`RuntimeException`和`Error`的所有异常类型。 这些是编译器要求你必须处理的异常,调用方必须通过捕获异常或声明可以抛出异常来处理异常。对已检查异常`(checked exception)`的几种处理方式: 1. 通过`throws exception`抛出,一直可以抛到java虚拟机来处理。 1. 用`try...catch...`语句块包围捕获 ##### 4.1.2 未检查异常 什么是未检查异常? 直接或间接地扩展了`RuntimeException`或者`Error`的异常。 调用方不需要显式地处理,异常会自动传播到调用堆栈。对未检查的异常`(unchecked exception)`的几种处理方式: 1. 通过`throws exception`抛出,一直可以抛到java虚拟机来处理。 1. 用`try...catch...`语句块包围捕获。 1. 不处理,出现了就终止程序 #### 4.2 AspectJ的异常处理 `AspectJ`对异常的处理可以根据时机分为两种: 1. 在运行时的行为,就是之前介绍的`AfterThrowing`后置异常通知 1. 在编译时的行为,就是接下来要介绍的异常软化`ExceptionSoft`。 异常软化,允许在某些连接点将除了`RuntimeException`及其子类之外的所有指定抛出类型视为未检查,等到真正需要抛出该类型时就抛出软异常来代替。 异常软化处理的是模块化异常的横切关注点。他也是避免将异常处理问题与核心逻辑纠缠在一起的一种快速方法。 例如,可以软化在某个方法调用时抛出的自定义异常(注意这种异常不能是`RuntimeException`及其子类,但可以是`Error`类型),以避免在每个级别处理他。在某些情况下,这可能是一种有用的策略。 注意,如果一个方法抛出多个选中的异常,则必须逐个软化每个异常。 ##### 4.2.1 运行时处理 我们知道,通知仅能够更改运行时的行为。如果是运行时发生了一个异常,那么我们完全可以使用`AfterThrowing`或者`Around`通知来捕捉切入点发生的异常。 回顾一下之前`AfterThrowing`的使用方式: 倘若有某个服务方法在调用的时候会抛出一个异常。 ```java package cn.jdl.ka.service; public class KeenService { public void throwBaseException() throws BaseException { System.out.println("该方法会抛出自定义异常"); throw new BaseException("抛出自定义异常"); } } ``` 因为是在运行时处理的,所以在编译期是不能理解的,在主函数中调用该方法必须要用`try...catch...`语句块包围后才能运行。 ```java public class KeenMain { public static void main(String[] args) { KeenService keenService=new KeenService(); try{ keenService.throwBaseException(); }catch (Exception e){ e.printStackTrace(); } } } ``` 现在我们使用通知切入到了该方法,将之前自定义的一个异常抛出来。 ```java package cn.jdl.ka.exeception; public class BaseException extends Exception { public BaseException(String errMessage) { super(errMessage); } public BaseException(String errMessage, Throwable e) { super(errMessage, e); } } package cn.jdl.ka.aspect; @Aspect public class KeenDeclarSoftAspect2 { @AfterThrowing(value="execution(* cn.jdl.ka.service.KeenService.throwBaseException())",throwing="ex") public void softException(BaseException ex) { System.out.println("切入点发生自定义异常"); } } ``` 在真正编译出来的时候是这个样子: ```java package cn.jdl.ka.service; import cn.jdl.ka.aspect.KeenDeclarSoftAspect2; import cn.jdl.ka.exeception.BaseException; public class KeenService { public void throwBaseException() throws BaseException { try { System.out.println("该方法会抛出自定义异常"); throw new BaseException("抛出自定义异常"); } catch (BaseException var2) { KeenDeclarSoftAspect2.aspectOf().softException(var2); throw var2; } } } ``` 使用插件进行编译运行,会在控制台打印如下内容: ![](//img1.jcloudcs.com/developer.jdcloud.com/9742b666-189f-4870-9942-3622c64cb4c220211224112701.png) 这是在运行时发生异常的例子。异常能否在编译时就进行处理呢?`AspectJ`提供了一种手段。 ##### 4.2.2 编译时处理 `AspectJ`编译器能够将连接点抛出的异常作为编译期间的运行时异常进行处理,而普通Java编译器并不能理解。 异常软化有两部分功能: 1. 在特定连接点上引发的异常,将其转化成为未捕捉的异常。 1. 抛出软异常来代替软化的已检查异常 异常软化的真正价值在于它的编译时行为。如果不能提供这一功能,那么该功能中几乎就是无用的。 由于功能定位的不同,故而只能使用传统语法: ``` declare soft : <ExceptionTypePattern> : <pointcut> ``` 仍然是针对上面所举出的例子,如果想要软化`throwBaseException`方法抛出的异常,所使用的切面是这样子: ```java package cn.jdl.ka.aspect; public aspect KeenDeclarSoftAspect { pointcut executionBasePointCut() : execution(* cn.jdl.ka.service.KeenService.throwBaseException()); declare soft : BaseException : executionBasePointCut(); } ``` 此时主函数不变,可以直接编译出来,结果如下: ```java package cn.jdl.ka.service; import cn.jdl.ka.exeception.BaseException; import org.aspectj.lang.SoftException; public class KeenService { public void throwBaseException() throws BaseException { try { System.out.println("该方法会抛出自定义异常"); throw new BaseException("抛出自定义异常"); } catch (BaseException var2) { if (var2 instanceof RuntimeException) { throw var2; } else { throw new SoftException(var2); } } } } ``` 简单比对之后就会发现,如果使用异常软化来处理,在编译的结果中就会多加上一个判断`BaseException`异常类型是否是`RuntimeException`及其子类,而不是直接调用切面的通知方法。 既然都在编译期软化了该异常,那么在调用时英国就不需要`try...catch...`语句块来捕捉了。虽然运行时肯定会抛异常导致程序运行失败,但是至少可以成功编译才对。继而可以推演到,即使我不用`try...catch...`语句块捕捉该语句,那么在其之前的语句应该可以正常运行才对。 将主函数修改成为: ```java package cn.jdl.ka; import cn.jdl.ka.service.KeenService; public class KeenMain { public static void main(String[] args) { KeenService keenService=new KeenService(); System.out.println("此处调用其他方法"); // 我理解的应该是编译会成功,运行到这里才会失败 keenService.throwBaseException(); } } ``` 使用插件进行编译,结果让我有些皱眉,插件报错竟然说编译不过去。 ![](//img1.jcloudcs.com/developer.jdcloud.com/df0e53e4-fbed-4f2d-8d87-45cae5a63e8d20211224120240.png) 重新查阅资料后发现,是切入点不对,我应该切入的是该方法的调用切入点,而不是运行切入点,所以修改切入点为: ```java package cn.jdl.ka.aspect; import cn.jdl.ka.exeception.BaseException; public aspect KeenDeclarSoftAspect { pointcut callBasePointCut() : call(* cn.jdl.ka.service.KeenService.throwBaseException()); declare soft : BaseException : callBasePointCut(); } ``` 重新使用插件进行编译,编译成功的结果着实让我松了一口气。编译的结果是在调用处自动添加了一层`try...catch`的语句块。 ```java package cn.jdl.ka; import cn.jdl.ka.exeception.BaseException; import cn.jdl.ka.service.KeenService; import org.aspectj.lang.SoftException; public class KeenMain { public static void main(String[] args) { KeenService keenService = new KeenService(); System.out.println("此处调用其他方法"); KeenService var10000 = keenService; try { var10000.throwBaseException(); } catch (BaseException var3) { if (var3 instanceof RuntimeException) { throw var3; } else { throw new SoftException(var3); } } } } ``` 使用`exec`插件运行后,得到结果为: ![](//img1.jcloudcs.com/developer.jdcloud.com/dc26ab63-f682-4df1-97a9-b0b5c033abe320211224120401.png) 如同预期一般,是一直到抛出自定义异常前都能正常运行,直至出现自定义异常后因为没有处理导致了程序运行失败。 ### 5 小结和感谢 本文是先介绍一下传统语法和`@AspectJ`的语法的不同,接着就五种特性情况对比了两个语法形式,更方便了大家在适当的时候去选择使用哪个语法。接着就静态横切中的织入声明和异常软化两部分进行了简单的分析,其间提及了异常的几个分类。 只是本文还是没有将静态横切完全说完,本来就是三足鼎立的局面,现在还差一个织入成员的大佬没有出现呢。所以还是那句老话哦,敬请期待~ 项目链接:https://coding.jd.com/weijikuo/keenTest-aop.git ##### 参考资料 AspectJ In Ation 第二版 AspectJ Cookbook中文版 [java异常](https://blog.csdn.net/lx520aa/article/details/77817159 ) ##### 同系列以往文章 1. AspectJ浅析系列(一)-初识https://developer.jdcloud.com/article/2132 1. AspectJ浅析系列(二)-切入点和通知https://developer.jdcloud.com/article/2137 1. AspectJ浅析系列(三)-自定义注解https://developer.jdcloud.com/article/2195 ------------ ###### 自猿其说Tech-京东物流技术发展部 ###### 作者:魏继扩
原创文章,需联系作者,授权转载
上一篇:以结果为导向 勇于担当铸就反爬防刷神盾的女汉子——访京东零售技术与数据中心潘姣
下一篇:京东晒出技术年度成绩单 “数实融合”用工匠精神助力实体经济发展
相关文章
Taro小程序跨端开发入门实战
Flutter For Web实践
配运基础数据缓存瘦身实践
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
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
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号