AspectJ是JAVA面向切面编程AOP的软件包,用来解决日志记录、性能监测、权限认证、访问控制等非核心逻辑通用功能模块的复用。
- 目录结构
- XML文件结构
- AspectJ组件
- 切入点表达式
- 通知参数获取
- 演示项目代码
-
知识结构图解
1 XML配置文件结构
与Spring框架的bean定义XML文件一样,可以定义BEAN对象或切面配置,切面配置节点
,BEAN对象节点
,这里重点研究切面配置节点。
AspectJ重要概念:切面(aspect)、切入点(pointcut)、通知(before、after、afterReturning、afterThrowing、around)等。
# 配置aspectj自动代理,可选,一般与aop:config节点二选一
#切面配置
# 配置切入点,可选,0个或多个
# 配置只有一个切入点和一个通知的切面,可选
# 配置1个切面,可选,0个或多个
# 配置切入点,可选,0个或多个
# 前置通知,可选,0个或多个
# 后置通知,可选,0个或多个
# 方法返回后通知,可选,0个或多个
# 方法抛出异常后通知,可选,0个或多个
#定义BEAN对象,可选,0个或多个
# 构造函数参数,可选,0个或多个
# 定义对象属性,可选,0个或多个
切面(Aspect),切面由切入点和通知组成,一个配置文件可以定义若干个切面。
切入点(Pointcut),一个切面定义0或多个切入点。
通知(Advice),一个切面定义1个或多个通知,常用通知类型,before-前置通知,after-后置通知,around-环绕通知,afterReturning-方法返回后通知,afterThrowing-异常抛出后通知。
2 AspectJ组件
切面、切入点与通知,都支持XML或注解方式配置。
2.1 切面-Aspect
切面对应XML节点
,对应注解@Aspect
。个人理解,切面设置对象是实现某个功能模块的类,如日志记录、权限验证等。
- 切面接口
public @interface Aspect{
String value() default ""
}
通过代码,可以看到@Aspect注解有一个属性value,可以保存类似XML节点的id等内容。注解范例如下:
@Aspect(value="mylog")
public class Logging{
...
}
- XML范例
XML方式
有id与ref两个属性,ref属性需要指定被引用的BEAN对象,也就是通知方法所在的BEAN对象的id。例如,自己实现一个记录日志的切面对象,bean对象的id为logging,这里的引用属性设置为logging。
注意:后续在该切面内定义的所有通知,method属性是被引用对象的方法名称。
2.2 切入点-Pointcut
2.2.1 命名方式
根据切入点是否有名称分成匿名切入点与命名切入点。区别:命名方式支持在其他位置引用,匿名方式不可以。
2.2.1.1 命名切入点
- XML配置
给
的id属性赋值,值可以根据程序需求与环境自定义,在通知定义中引用,上述例子id赋值为setValue,在后续通知
中用pointcut-ref
中引用。
- 注解配置
@Aspect
public class Logging {
@Pointcut(value="execution(public * com.toipr..SimpleCalculator.*(int, int))")
private void calculator(){
}
@Before(value="calculator()")
public void beforeAdvice(JoinPoint jp){
Object target = jp.getTarget();
System.out.println("target object class is " + target.getClass().getName());
Object[] args = jp.getArgs();
if(args!=null && args.length>0){
System.out.println("going to setup profile. name=" + args[0].toString());
}
}
}
上述例子,申明私有方法calculator(用@Pointcut注解),这个就是切入点的命名,在前置通知@Before注解中引用(@Before(value="calculator()")
),更复杂的命名与引用形式后续总结。
2.2.1.2 匿名切入点
public class Logging{
@Before(value="execution(public * com.toipr..UserInfo.set*(*)))")
public void beforeAdvice(JoinPoint jp){
Object target = jp.getTarget();
System.out.println("target object : " + target.toString());
}
}
上述例子,直接在通知方法beforeAdvice
上使用注解@Pointcut申明切入点,属于匿名切入点,在其他通知中无法复用。
2.2.2 配置方式
支持XML或注解两种配置方式。
2.2.2.1 XML配置
节点用来定义一个切入点,XML节点有两个属性,id为切入点唯一标识,expression为切入点表达式。切入点XML节点示例:
2.2.2.2 注解配置
@Pointcut
,包含两个属性value与argNames,value为切入点表达式,argNames为参数。
- 切入点注解接口
public @interface Pointcut{
/* 返回切入点表达式, 如: execution=(public * com.toipr..UserInfo.set(*)) */
String value() default ""
/* 返回切入点参数,多个参数用半角逗号隔开,例如:a,b */
String argNames() default ""
}
- 注解方式示例代码
public class Logging{
@Before(value="execution(public * com.toipr..UserInfo.set*(*)))")
public void beforeAdvice(JoinPoint jp){
Object target = jp.getTarget();
System.out.println("target object : " + target.toString());
}
}
2.3 通知-Advice
典型的切面通知有before-前置通知、after-后置通知、afterReturning-方法返回后通知、afterThrowing-抛出异常后通知、around-环绕通知等,配置方式与参数大同小异。
-
通知时机示意图
2.3.1 before-前置通知
前置通知,顾名思义,就是在方法主体代码执行前接收调用通知。前置通知的注解为@Before
,XML节点标签为
。
- 注解定义
public @interface Before{
/*通知表达式*/
String value() default ""
/*通知参数命名,多个参数用半角分号隔开*/
String argNames() default ""
}
- 注解配置
public class Logging{
@Before(value="execution(public * com.toipr..UserInfo.set*(*)))")
public void beforeAdvice(JoinPoint jp){
Object target = jp.getTarget();
System.out.println("target object : " + target.toString());
}
}
- XML配置
标签名称
,节点有method(必选)、pointcut、pointcut-ref与arg-names四个属性。参考样例如下:
2.3.2 after后置通知
与before-前置通知基本一致。
2.3.3 afterReturning返回后通知
与@Before
、@After
不一样,@AfterReturning
通知有一个重要的returning配置项,在方法调用返回后触发通知。
- 注解定义
public @interface AfterReturning {
String value() default ""
String argNames() default ""
/* 切入点表达式,设置后覆盖value属性 */
String pointcut() default ""
/* 返回值的参数命名 */
String returning() default ""
}
- 注解配置
@Aspect
public class Logging{
@AfterReturning(pointcut="calculator()", returning = "retVal")
public void afterReturningAdvice(Object retVal){
if(retVal==null){
return;
}
System.out.println("Returning value=" + retVal.toString());
}
}
注意:returning属性配置返回值参数名称,对应函数的参数名称必须一致,并且,有的函数没有返回值。
- XML配置
注意:与注解形式相比,XML多了pointcut-ref与pointcut两个属性,pointcut-ref用于引用命名切入点,pointcut用于申明专用的匿名切入点。
2.3.4 AfterThrowing异常后通知
捕获异常后通知@AfterThrowing
,顾名思义,在捕获方法调用抛出异常后触发通知。
- 注解定义
public @interface AfterThrowing {
String value() default ""
String argNames() default ""
/* 切入点表达式,设置后覆盖value属性 */
String pointcut() default ""
/* 抛出异常的参数命名 */
String throwing() default ""
}
- 注解配置
@Aspect
public class Logging{
@AfterThrowing(pointcut="calculator()", throwing = "ex")
public void afterThrowingAdvice(IllegalArgumentException ex){
System.out.println("There has been an exceptin:" + ex.toString());
}
}
- XML配置
2.3.5 around环绕通知
环绕通知@Around
,顾名思义,在一个方法的执行前与执行后触发通知,注解接口定义与@Before一致,XML节点标签
3 切入点表达式
切入点表达式知识点包括语法与切入点指示符两个部分。
3.1 表达式语法
3.1.1 表达式结构
注解? 修饰符? 返回值类型? 类型申明? 方法匹配(参数列表) 异常列表?
- 注解,可选,用于匹配方法上申明的注解,如@Documented等。注意:注解类型必须是全限定名称。
- 修饰符,可选,如public / protected / private等,public代表匹配所有公有方法,用通配符*表示不做任何限定。
- 返回值类型,必选,可填写如何类型,如java.lang.String,用通配符*表示返回任意类型。
- 类型申明,可选,可填写任何类型,匹配类/接口的全路径,用通配符*或..模式匹配。例如:com.toipr.*与com.toipr..等。
- 方法匹配,必选,可以用方法全名、精确匹配,也可以用*通配符模糊匹配,只有“*"匹配任意方法,用“前缀+*”匹配名称以特定文本前缀开头的方法,用“*+后缀”匹配名称以特定文本后缀结尾的方法。例如:find*表示以前缀“find”开头的方法。
- 参数列表,必选,“()”匹配无任何参数的方法,“(*)”匹配只有一个参数、类型任意的方法,“(..)”匹配任意多个参数的方法,“(type, ..)”表示第一个参数为type、后接多个参数的方法,“(.., type)”匹配最后一个参数类型为type、前面有任意多个参数的方法,参数类型type可以是java.lang.String,也可以自由指定。注意:参数类型名称必须为全限定名称。
- 异常列表,可选,以"throws 异常全限定名称(1个或多个)"表示,多个异常用半角分号隔开,如throws java.lang.ClassCastException。注意:异常名称必须是全限定名称。
3.1.2 通配符
表达式支持三个常用通配符:*、..和+。
- 星号通配符*
匹配任意数量的字符。
- 加号通配符+
匹配某个类型的子类型,一般作为后缀放到接口或类名称后边,如com.toipr..Calculator+.div(..)匹配com.toipr包下任意层级、从Calculator派生的子类的div方法。
- 双点通配符..
匹配任意数量字符的重复。最常用的两个作用:1) 类/接口匹配,表示匹配任意层级包名称; 2) 方法参数匹配,表示匹配任意多个参数。
3.1.3 逻辑运算符
AspectJ表达式与JAVA一样,使用&&、||、与!三个逻辑运算符,使用括号对表达式分组。&&
表示逻辑与,||
表示逻辑或,!
表示逻辑非,也可以用and / or / not替换,尤其在XML Schema配置中,避免使用&等转义字符。
3.2 切入点指示符
@Pointcut
切入点指示符用于匹配切入的对象与方法,有execution / within / target / this / args / get / set / call / handler等数十个切入点指示符,其中,使用最多最重要的是execution / within / args / target指示符。
序号 | 指示符 | 用途 |
---|---|---|
1 | execution | 用于匹配方法的执行连接点。 |
2 | args | 用于指定参数列表与类型来限定匹配方法的执行连接点。 |
3 | target | 用于匹配当前目标对象类型的执行连接点。 |
4 | within | 用于限定在某个包内或某个类型下所有方法的执行连接点。 |
3.2.1 execution指示符
使用execution("expression")的形式配置,匹配切面连接的方法。应用示例如下表:
序号 | 模式 | 模式含义 |
---|---|---|
1 | public * com.toipr..UserInfo.*(..) | 匹配com.toipr包下任意层级的类UserInfo的任意公有方法 |
2 | public * *(..) | 匹配任意公有方法 |
3 | public * com.toipr..*.*(..) | 匹配com.toipr包下的任意公有方法 |
4 | public * com.toipr..UserInfo.setName(*) | 匹配com.toipr包下任意层级UserInfo类的的公有方法setName,并且,方法只有一个参数、类型任意。 |
5 | public * com.toipr..UserInfo.setName(java.lang.String) | 匹配com.toipr包下任意层级UserInfo类的的公有方法setName,并且,方法只有一个参数、类型为java.lang.String。 |
6 | @java.lang.Deprecated * *(..) | 匹配所有持有@Deprecated注解的方法 |
7 | @java.lang.Deprecated @java.lang.Documented * *(..) | 匹配同时持有@Deprecated和@Documented注解的所有方法。 |
8 | @(java.lang.Deprecated || java.lang.Documented) * *(..) | 匹配持有@Deprecated或@Documented注解的所有方法 |
9 | public java.lang.String com.toipr..UserInfo.get*() | 匹配com.toipr包下任意层级UserInfo类、以get开头的无参数公有方法,且方法返回值类型为字符串java.lang.String类型 |
10 | * com.toipr..UserInfo+.*(java.lang.String, ..) | 匹配com.toipr包下任意层级、派生自UserInfo的第一个参数为java.lang.String类型的任意方法 |
11 | @RequestMapping * com.toipr..*.*(..,java.util.Map |
匹配在com.toipr包下,持有@RequestMapping注解,且最后一个参数是Map |
12 | public * com.toipr..Calculator.*(..) throws java.lang.Exception | 匹配com.toipr包下任意层级Calculator类/接口的、申明抛出java.lang.Exception异常的任意公有方法。 |
3.2.2 args指示符
参数指示符,匹配方法的参数类型是否一致,不一致则排除,使用args(参数类型列表)的形式文本串表达。
- 参数类型数量:参数类型列表可以有一个或多个,多个类型用半角分号隔开。
- 参数类型名称:必须使用全限定名称,如字符串类型String必须用java.lang.String表示。
应用示例
args(java.lang.String):匹配只有一个字符串String类型的方法。
args(java.lang.String, int) : 配置有两个参数,第一个参数为字符串类型、第二个参数为整型的方法。
args(java.lang.String, ..) : 匹配第一个参数为String类型、后接任意多参数的方法。
args(.., java.lang.String) : 匹配最后一个参数为String类型、前面有任意多参数的方法。
3.2.3 target指示符
目标对象指示符target,使用目标对象的类型匹配,采用"target(类型限定表达式)"。注意:类型限定表达式不支持通配符,类型名称必须是全限定名称。
- 应用示例
target(com.toipr.aop.bean.Calculator):匹配com.toipr.aop.bean.Calculator的任意方法。
3.2.4 within指示符
包或类型限定指示符within,采用"within(类型限定表达式)",匹配切面连接的包路径或类。与target指示符不一样,这里的表达式支持通配符。
- 应用示例
within(com.toipr.aop..*):匹配com.toipr.aop包下任意类方法。
within(com.toipr.aop..Calculator+):匹配com.toipr.aop包下,任意从Calculator派生的类方法。
4 通知参数JoinPoint
搞清楚如何配置切入点和通知后,我最关心的是,我如何获取切入点方法的参数问题,无论在日志记录、权限验证等功能,都很可能使用到切入点方法参数。
切面对象的通知函数可以把JoinPoint接口作为方法的第一个参数,调用接口方法的getTarget方法获取方法所在类的实例,调用getArgs获取该方法的调用入参列表。
4.1 接口定义
连接点相关接口有三个JoinPoint、ProceedingJoinPoint与StaticPart。
- JoinPoint连接点
public interface JoinPoint {
/* 获取切面对象,直接用this更高效安全 */
Object getThis();
/* 获取被切入方法所在对象类的实例 */
Object getTarget();
/* 获取被切入方法的调用入参列表 */
Object[] getArgs();
/* 获取静态方法辅助信息 */
StaticPart getStaticPart();
/* 获取方法签名 */
Signature getSignature();
/* 获取源代码位置信息,不一定能正常获取 */
SourceLocation getSourceLocation();
public interface StaticPart {
/* 获取静态方法的ID */
int getId();
/* 获取静态调用的类型 */
String getKind();
/* 获取方法签名 */
Signature getSignature();
/* 获取源代码位置信息 */
SourceLocation getSourceLocation();
}
static String METHOD_EXECUTION = "method-execution";
static String METHOD_CALL = "method-call";
static String CONSTRUCTOR_EXECUTION = "constructor-execution";
static String CONSTRUCTOR_CALL = "constructor-call";
static String FIELD_GET = "field-get";
static String FIELD_SET = "field-set";
static String STATICINITIALIZATION = "staticinitialization";
static String PREINITIALIZATION = "preinitialization";
static String INITIALIZATION = "initialization";
static String EXCEPTION_HANDLER = "exception-handler";
static String SYNCHRONIZATION_LOCK = "lock";
static String SYNCHRONIZATION_UNLOCK = "unlock";
static String ADVICE_EXECUTION = "adviceexecution";
}
通过JoinPoint连接点的接口定义来看,最常用的应该就是getTarget与getArgs方法,getSourceLocation在某些功能比较有用。注意:getThis函数获取切面代理对象,一般可以在通知函数中用this替代,更高效安全。
- ProceedingJoinPoint
用于环绕通知的处理,继承JoinPoint接口,增加了proceed两个重要方法,用于执行被切入方法。
public interface ProceedingJoinPoint extends JoinPoint {
/* 执行被切入方法 */
Object proceed() throws Throwable;
Object proceed(Object[] args) throws Throwable;
/* 没用过 */
void set$AroundClosure(AroundClosure arc);
void stack$AroundClosure(AroundClosure arc);
}
4.2 使用示例
JoinPoint连接点
@Aspect
public class Logging {
@Before(value="calculator()")
public void beforeAdvice(JoinPoint jp){
Object target = jp.getTarget(); //获取被切入方法所在的目标对象实例
System.out.println("target object class is " + target.getClass().getName());
Object[] args = jp.getArgs(); //获取被切入方法的调用入参列表
if(args!=null && args.length>0){
System.out.println("going to setup profile. name=" + args[0].toString());
}
}
}
- 目标对象。调用JoinPoint连接点的
getTarget
方法,可以获得被切入方法所在对象类的实例,后续可以采用类型转换、反射技术等方法访问该对象的属性与方法。 - 入参列表。调用JointPoint连接点的
getArgs
方法,可以获得被切入方法的调用入参列表,可以实现参数检查、日志记录等功能。
ProceedingJoinPoint,用于处理环绕通知
@Aspect
public class Logging {
@Around(value="calculator()")
public Object aroundAdvice(ProceedingJoinPoint jp){
Object retVal = null;
Date tmBeg = new Date();
try {
retVal = jp.proceed();
System.out.println("aroundAdvice result=" + retVal.toString());
}catch(Throwable ex){
ex.printStackTrace();
}
Date tmEnd = new Date();
System.out.println("Time elapsed :" + (tmEnd.getTime() - tmBeg.getTime()));
return retVal;
}
}
这个例子简单的演示性能监测功能,在方法执行前记录开始时间,方法执行结束后记录结束时间,可以记录一些执行时间异常的调用,分析相关参数及时了解性能差异原因。注意:该方法一定要将proceed方法的直接结果返回,否则会产生异常。
5 测试项目源代码
第一步 创建MAVEN项目
用MAVEN模板quickstart创建一个简单项目。
- 项目名称,自己随意取
com.toipr.aop
AopStudy
1.0.0.1
- 依赖配置
org.springframework
spring-context
5.2.4.RELEASE
org.springframework
spring-aop
5.2.4.RELEASE
org.aspectj
aspectjrt
1.9.5
org.aspectj
aspectjweaver
1.9.5
第二步 添加com.toipr.aop.bean.SimpleCalculator类
package com.toipr.aop.bean;
public class SimpleCalculator implements Calculator {
public int add(int a, int b){
return (a + b);
}
public long div(int a, int b){
return (a / b);
}
}
第三步 添加切面对象com.toipr.log.Logging类,切面通知的响应函数在这里实现
package com.toipr.aop.log;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import java.util.Date;
public class Logging {
public void beforeAdvice(JoinPoint jp){
System.out.println("-----beforeAdvice-----");
Object target = jp.getTarget(); //获取被切入方法所在目标对象
Object[] args = jp.getArgs(); //获取被切入方法调用入参函数
if(args!=null && args.length==2){
System.out.println("a=" + args[0].toString() + "\tb=" + args[1].toString());
}
}
public void afterAdvice(){
System.out.println("-----afterAdvice-----");
}
public void afterReturningAdvice(Object retVal){
System.out.println("-----afterReturningAdvice-----");
if(retVal==null){
return;
}
System.out.println("Returning value=" + retVal.toString());
}
public void afterThrowingAdvice(Throwable ex){
System.out.println("-----afterThrowingAdvice-----");
System.out.println("There has been an exceptin:" + ex.toString());
}
public Object aroundAdvice(ProceedingJoinPoint jp) throws Throwable {
System.out.println("-----enter aroundAdvice-----");
Date tmBeg = new Date();
Object retVal = jp.proceed();
if(retVal!=null) {
System.out.println("aroundAdvice result=" + retVal.toString());
}
Date tmEnd = new Date();
System.out.println("Time elapsed :" + (tmEnd.getTime() - tmBeg.getTime()));
System.out.println("-----leave aroundAdvice-----");
return retVal;
}
}
第四步 配置beans.xml
第五步 集成调试,打完收工
package com.toipr.aop;
import com.toipr.aop.bean.Calculator;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* AOP AspectJ知识点练习
* @author 明月照我行@
*/
public class App
{
public static void main( String[] args ) throws Exception
{
ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/beans.xml");
Calculator calc = (Calculator)ctx.getBean("simpleCalculator");
System.out.println("result=" + calc.add(5, 8)); //正常流程
Thread.sleep(1000);
calc.div(5, 0); //除0错,异常流程
Thread.sleep(1000);
}
}
- 执行输出结果
-----beforeAdvice-----
a=5 b=8
-----enter aroundAdvice-----
aroundAdvice result=13
Time elapsed :0
-----leave aroundAdvice-----
-----afterReturningAdvice-----
Returning value=13
-----afterAdvice-----
result=13
Disconnected from the target VM, address: '127.0.0.1:53952', transport: 'socket'
-----beforeAdvice-----
a=5 b=0
-----enter aroundAdvice-----
-----afterThrowingAdvice-----
There has been an exceptin:java.lang.ArithmeticException: / by zero
-----afterAdvice-----
Exception in thread "main" java.lang.ArithmeticException: / by zero
6 结尾
断断续续,学习好多同行总结的文章,我在学习的基础上重新梳理,再次分享给后来学习者。