Spring AOP作为spring的核心之一,是非常好用的。我们可以将其用在很多地方,例如记录日志,权限验证,数据校验,异常处理等等。自己经常会用到,但是总是过了一段时间就忘记了,这里记录下如何集成及基本操作,例如基本配置,切点定义,通知定义等等。
1、maven依赖
<!--spring aop-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.6</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.1</version>
</dependency>
其中spring-aop是进行spring切面编程必备的,aspectweaver和aspectjrt是为了支持AspectJ风格编程(例如@Aspect注解,@PointCut注解等等)的必须的。cglib-nodep暂时不太确定是干嘛的,应该是为了支持静态代理需要的吧,我没有引入也可以跑。如果有知道的朋友,麻烦告知下。
2、aop配置
<!--激活自动代理-->
<aop:aspectj-autoproxy/>
这里主要介绍基于注解的配置方式,也就是AspectJ风格的配置方式,基于xml的配置方式太过于繁琐了,因此略过。要启用AspectJ风格的配置方式,需要在xml文件里配置自动代理,如上所示。
3、PointCut
PointCut说白了就是一组连接点的集合,它用简单的表达式包含了一组我们关心的连接点,我们往往需要在这些连接点上进行相同的操作。比如,我们会在所有的数据库操作前打印SQL参数,记录日志等等。在Spring aop中,PointCut只能关联执行的方法,而且bean必须是spring的bean,否则是不会起作用的,这点要牢记。
先来看一看一个最简单的PointCut定义:
@Pointcut("execution(String testBeforeAdvice(..))")//PointCut定义
public void pt2() {}//PointCut名称
pt2定义了一组连接点(在spring aop中是一组方法),方法名称是testBeforeAdvice,返回String类型,参数任意。在后面的通知部分,我们就可以使用pt2来代表定义的这个PointCut。
以execution类型切点为例,一个完整的PointCut语法结构如下:
语法结构:execution(【修饰符】 返回值类型 方法名(参数) 【异常模式】
其中【】部分是可选的。下面列出一些例子,自己可以依葫芦画瓢:
execution(public *.*(..)) 所有的public方法。
execution(* hello(..)) 所有的hello()方法
execution(String hello(..)) 所有返回值为String的hello方法。
execution(* hello(String)) 所有参数为String类型的hello()
execution(* hello(String..)) 至少有一个参数,且第一个参数类型为String的hello方法
execution(* com.aspect..*(..)) 所有com.aspect包,以及子孙包下的所有方法
execution(* com..*.*Dao.find*(..)) com包下的所有一Dao结尾的类的一find开头的方法
如果能理解..和*两个通配符,基本上就没啥问题了。..放在包定义里面,表示所有子孙包,放在参数里面,表示参数不固定。*号表示通配符,通配任意字符串。
PointCut类型有很多,最常用的就是我们刚才用到的execution类型PointCut,它表示方法执行。其他部分PointCut类型如下所示:
//@annotation类型切点,含有该annotation的方法
@Pointcut("@annotation(com.gameloft9.demo.annotaion.METHODDESC)")
public void pt3() {}
//target类型切点
//实现IHelloService接口的目标对象的任意连接点 (在Spring AOP中只是方法执行):
@Pointcut("target(com.gameloft9.demo.service.api.IHelloService)")
public void pt4() {}
//within类型切点
//在service包中的任意连接点(在Spring AOP中只是方法执行):
@Pointcut("within(com.gameloft9.demo.service.impl.*)")
public void pt5() {}
如果我们想组合不同类型的PointCut怎么办,很简单,直接使用&&链接起来就好了:
//组合切点,可以用&&,||和!逻辑连接符链接不同的切点
@Pointcut("pt3()&&pt4()")
public void pt6(){}
是不是很简单。
3、advice
通知(advice)必须和一组PointCut关联,它会在关联的方法执行前,后或者抛出异常时执行。不同的执行时间点,决定了通知的类型。目前有的通知类型有@Before,@After,@AfterReturning,@AfterThrowing,@Around这几种,下面是一些简单的例子:
/**
* 前置通知,目标方法执行前会走到这里
* 注意:通知和方法只能一对一,不能放多个通知
* */
@Before("pt1()")
public void beforeAdvice(){
log.info("进入前置通知了");
}
/**
* 后置通知,不管是正常返回,还是异常返回,均会走到这里
* */
@After("pt2()")
public void afterAdvice(){
log.info("进入后置通知");
}
/**
* 正常返回通知
* */
@AfterReturning(pointcut = "pt3()" ,returning = "res")
public void afterReturningAdvice(Object res){
log.info("进入返回通知,返回结果{}",res);
}
/**
* 异常通知,如果目标方法抛出异常,会进入到这里
* */
@AfterThrowing(pointcut = "pt4()",throwing = "ex")
public void afterThrowingAdvice(Throwable ex){
log.error("进入异常通知",ex);
}
除了@Around通知,其他几个通知都很简单,都是在方法执行的前后做一些操作,不涉及参数处理。如果我们要处理参数,而且在方法前后都要做一些额外处理,那么@Around就很有用了。
/**
* 环绕通知切点
* */
@Pointcut("execution(public String testAroundAdvice(String,javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse))")
public void ptTestAroundAdvice() {}
/**
* 环绕通知,执行前进入,执行后返回。
* 注意:
* 1、参数获取的话,所有参数都必须列进来,要跟方法的参数顺序一致,名字可以不一致。
* 2、pjp参数必须放在第一位。
* */
@Around("ptTestAroundAdvice()&&args(number,request,response)")
public String testAroundAdvice(ProceedingJoinPoint pjp, String number, HttpServletRequest request, HttpServletResponse response){
log.info("进入环绕通知,请求参数,number:{}",number);
String res = "";
try{
res = (String)pjp.proceed();
}catch(Throwable e){
log.error("处理异常",e);
}
log.info("处理结果{}",res);
return res;
}
在定义@Around通知时,通过args将切点方法的参数名称定义好,然后通知处理方法里面就可以截获方法的参数并处理了。这里要注意的是ProceedingJoinPoint必须放在第一位,我们要通过它来调用目标方法并获取返回结果的。
pjp除了prceed()方法外,还有一个prceed(Object[] params)方法,通过这个方法可以替换原有参数:
/**
*环绕通知替换传入参数
* */
@Around("ptTestAroundAdviceParam()&&args(number,request,response)")
public String testAroundAdviceParam(ProceedingJoinPoint pjp, String number, HttpServletRequest request, HttpServletResponse response){
log.info("进入环绕通知,请求参数,number:{}",number);
String replace = "5";
Object[] param = {replace,request,response};//替换掉参数
log.info("替换number为:{}",replace);
String res = "";
try{
res = (String)pjp.proceed(param);
}catch(Throwable e){
log.error("处理异常",e);
}
log.info("处理结果{}",res);
return res;
}
4、引入
所谓引入就是为目标对象添加新的方法,而这个方法本来是属于另外的类。如果熟悉js的话,这个有点类似于apply操作。在spring aop中,我们通过@DeclareParents这个注解来实现。例如:
@Aspect
@Service
@Slf4j
public class IntroductionDemoAop {
// ““+” 号,表示只要是Person及其子类都可以添加新的方法
@DeclareParents(value = "com.gameloft9.demo.aspect.introduction.Person+", defaultImpl = PersonRun.class)
public Run personRun;
}
通过上述操作,我们为Person及其子类劫持了Run接口,那么之后尽管Person及其子类没有实现Runj接口,我们也可以使用Run接口的方法。默认的实现都交给了PersunRun这个类。
public interface Run {
public void run();
}
@Slf4j
public class PersonRun implements Run {
public void run(){
log.info("Person is running");
}
}
public class Person {
}
@Service
@Slf4j
@Data
public class ChinesePerson extends Person {
public void sayName(String name){
log.info("我的名字是{}",name);
}
}
调用代码示例:
//测试引入
Run run = (Run)chinesePerson;
run.run();//新方法
chinesePerson.sayName("gameloft9");
5、运行截图
好了,以上就是spring aop的基本点,更多内容请参考官方文档:
https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop-introductions
源码地址:https://download.csdn.net/download/gameloft9/10343785