前言
在开发过程中Spring AOP可以说是经常使用的一个技术了,他能够让我们不污染业务的情况下进行日志、处理结果及一些入参预处理等。像Spring提供的事务操作,也是基于Spring AOP实现的。
AOP
AOP即面向切面编程,该思想主要体现于“切面”这一 理念。通俗来讲理念主要是将业务处理过程流中的某一过程、步骤或阶段,当成像乐高积木一样,可按需对某一模块进行切入所需的其他模块。当然,在这一理念中目前来讲粒度只能达到方法级的切入。
在这篇文章中,仅讲解Spring AOP的使用。
Spring AOP的使用
准备
需要引入spring aop以及aspectj,以下为aspectj所需依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.13</version>
</dependency>
切点 Pointcut
使用AOP切入时,最重要的就是告知切入的位置,其次是要将那些操作切入其中。而切入的位置,也就是切点,需要通过 @Pointcut 来进行定义,其中存在两个参数:
value:切点表达式,支持 ||(or) 和 &&(and) argNames:参数表达式,必须和注释的方法中入参一致 在切点表达式中,存在以下指示符:
execution
匹配执行方法的访问限制符、返回类型等方法信息,将符合的方法作为切点。其中表达式规则如下
execution( 访问修饰符 返回类型全类名.方法(入参类型) ) — 支持通配符
eg:
可用到时再进行查阅
以上表达式基本囊括了绝大部分常见的场景,而部分功能也可以通过其他方式实现
within
匹配指定类型中的方法执行,只有实现该方法的才会被匹配成功。其中类型表达式规则
within( 全类名 ) — 支持通配符
eg:
表达式 匹配目标
this
匹配当前AOP代理对象类型的执行方法,说白了就是匹配类型中重写/实现的方法,父类中未重写或实现的方法则不会被匹配(可以称为作用域为当前对象,非super的方法)。其中表达式规则如下
this( 全类名 )
eg:
target
this( 全类名 )
eg:
args
根据参数类型匹配方法入参(注意这里匹配不是匹配方法签名的类型,而是匹配传入的类型,例如String也可以通过Object匹配)。表达式规则如下
args(全类名) — 支持通 .. 匹配多参数
eg:
注意,由于args匹配是动态切入点,开销比较大,非必要的情况下建议尽量少用(待确认)
@within、@target、@args、@annotation
@within、@target、@args同within、target、args中匹配注解的简略写法,但该种只能匹配基于注解,无法匹配类型
@annotation匹配持有指定类型注解的方法
eg:
bean
该类型由Spring ASP拓展,AspectJ并无该类型。用于匹配Spring容器中指定名称的Bean方法
bean(bean名称) — 支持通 * 通配符
eg:
引用切点
通常使用时,会搭配该方式实现对切点的定义,如:
@Before("execution(* (@org.springframework.stereotype.Controller *).*(..))")
public void classAnnotationAop() {
System.out.println("class annotation");
}
也可以写成
@Pointcut("execution(* (@org.springframework.stereotype.Controller *).*(..))")
public void classMethod() {}
@Before("classMethod()")
public void classAnnotationAop() {
System.out.println("class annotation");
}
其中@Before直接引用了方法classMethod()上定义的切点表达式
通知 Advise
通知类型
存在一下几种通知类型:
Before 前置通知,于目标方法执行前执行 After 后置通知,在目标方法执行后执行,但在@AfterReturn、@AfterThrowing之前执行 AfterReturn 返回后通知,在方法成功执行并返回结果时执行 AfterThrowing 异常处理通知,在方法执行过程中抛出异常时执行 Around 环绕通知,最特殊的通知方式,可以实现上面几种通知相同的功能,但该通知不同于@Before,需要自己手动通过ProceedingJoinPoint.proceed进行调用切入方法 执行顺序如图
通知入参
在任何的通知中,都支持JoinPoint类型的入参,可以通过该 入参获取当前被通知方法的目标对象、代理对象、方法参数等数据。该类型提供了以下方法
public interface JoinPoint {
String toString(); // 连接点的相关信息,如 execution(int xyz.me4cxy.aop.advise.AdviseTarget.test(int))
String toShortString(); // 连接点的简短相关信息,如 execution(AdviseTarget.test(..))
String toLongString(); // 连接点的详细相关信息,如 execution(public int xyz.me4cxy.aop.advise.AdviseTarget.test(int))
Object getThis(); // 获取AOP代理对象
Object getTarget(); // 获取目标对象
Object[] getArgs(); // 获取当前执行方法入参
Signature getSignature(); // 获取当前执行方法签名
SourceLocation getSourceLocation(); // 获取连接点方法在类文件中的位置
String getKind(); // 获取连接点类型,如 method-execution
JoinPoint.StaticPart getStaticPart(); // 获取连接点静态部分属性
}
同时,在@Around类型的通知中,也可以用ProceedingJoinPoint类型,该类型提供了proceed方法用于执行被代理的目标方法。
public interface ProceedingJoinPoint {
Object proceed() throws Throwable; // 执行目标方法,并传入调用时的参数
Object proceed(Object[] var1) throws Throwable; // 执行目标方法,并将用户自定义的参数传入
}
注意,如果需要接收JoinPoint类型的参数,那么该参数必须至于方法第一个参数,否则会出现异常
Caused by: java.lang.IllegalArgumentException: error at ::0 formal unbound in pointcut
如果是@AfterReturn或@AfterThrowing类型的通知,还可以分别通过注解参数returning和throwing指定返回值和异常传入的参数名称,如:
@AfterThrowing(value = "test()", throwing = "e")
public void afterThrowing(Exception e) {
System.out.println("afterThrowing");
System.out.println(e.getMessage());
}
@AfterReturning(value = "test()", returning = "result")
public void afterReturning(int result) {
System.out.println("afterReturning");
System.out.println(result);
}
以上的方式都是内置好会自动匹配传入的。如果我们需要根据我们匹配规则,传入我们需要的方法入参、注解时,可以上面讲述过的指示符来传入,除了execution和bean这两个指示符不能传递参数给通知方法外,其他的都可以将匹配的参数或对象作为方法入参传入。
eg:
传入匹配的入参
@Before("execution(* xyz.me4cxy.aop.advise.AdviseTarget.*(*)) && args(param)")
public void before(JoinPoint joinPoint, int param) {
System.out.println("before");
System.out.println(param);
}
上面例子中通过execution指示符匹配AdviseTarget中到仅有一个参数的方法,然后会通过before方法中的args参数的类型进行再次匹配执行方法的入参类型,如果两者相同就会进行切入并将参数作为入参传入param中,例如
@Component
public class AdviseTarget {
public Object test(Object i) {
if (i.equals(1))
System.out.println("执行方法");
else
throw new RuntimeException("异常");
return i;
}
}
当执行test方法时,因为入参为Object类型,那么在匹配时因为before中param参数的类型是int,不符合所以将不会被切入。但是我们把test的入参修改为int/Integer,调用并传入 1 时,执行结果如下
before 1 执行方法
传入匹配的注解
@Component
public class AnnotationTarget {
@RequestMapping({"/", "/test"})
public void test(String param) {
System.out.println("test");
}
}
@Pointcut("execution(* xyz.me4cxy.aop.anno..*.*(*))")
public void exec() {}
@Pointcut("@annotation(requestMapping)")
public void requestMapping(RequestMapping requestMapping) {}
@Pointcut("args(str)")
public void stringParam(String str) {}
@Before("exec() && requestMapping(anno) && stringParam(str)")
public void before(RequestMapping anno, String str) {
System.out.println(ArrayUtil.arrayToString(anno.value()));
System.out.println(str);
}
@Autowired
private AnnotationTarget target;
@Test
public void test() {
target.test("123");
}
上面例子利用了“类型自匹配”(非专业名称,纯属个人方便表达使用),通过入参类型来限制匹配的方法类型。同时也可以完成入参的控制。而上例执行结果为
/ /test 123 test
注册 Register
在这里,我使用Java配置的方式来实现AOP的注册,实际上Java配置的方式非常简单,只要在类上添加@Aspect和@Component即可(记得使用@EnableAspectJAutoProxy开启代理)。
在使用Spring AOP时,在配合@Autowired注入bean时可能会出现异常
Bean named ‘‘ is expected to be of type ‘‘ but was actually of type ‘com.sun.proxy.$Proxy**’
原因和Spring AOP代理不无关系,在【设计模式】代理模式中说过,SpringAOP是使用JDK代理和Cglib两种方式实现的,而且不难发现异常中com.sun.proxy.$Proxy**类正是JDK代理生成的代理类。
一般来说,SpringAOP在代理时,如果代理目标类实现了接口,那么SpringAOP将使用JDK代理来生成代理对象,也就是这样原因导致使用@Autowired注入会出现bean类型不一致的问题。
解决这个问题实际上很简单,修改@EnableAspectJAutoProxy注解中的参数proxyTargetClass为true即可。
最后
感谢你看到这里,看完有什么的不懂的可以在评论区问我,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!