Spring框架的AOP机制(切面编程)可以让开发者把业务流程中的通用功能抽取出来,单独编写功能代码。在业务流程执行过程中,Spring框架会根据业务流程要求,自动把独立编写的功能代码切入到流程的合适位置。从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。在Spring AOP中业务逻辑仅仅只关注业务本身,将日志记录、性能统计、安全控制、事务处理、异常处理等代码从业务逻辑代码中划分出来,从而在改变这些行为的时候不影响业务逻辑的代码。
需求的思考
最近在项目中常会用到一些通用性的处理,比较常见的就是接入了很多上游的业务数据并通过MQ同步到系统中来,这样就会导致有很多接收MQ消息的处理类,一般在这种处理类中,需要对消息进行业务处理,对于处理失败的消息需要落地到一个记录表然后做重试,通过记录的beanName,获取到实例重新调用业务方法,伪代码形式如下
@Service("testService")
class ReciveMessage{
@MqListener("topic1")
public void doMessage(String msg){
try{
this.dealMsg(msg);
}catch(Exception e){
this.saveMsg(msg);
}
}
private void dealMsg(String msg){
// 处理消息业务代码
}
private void saveMsg(String msg){
// 把消息存到数据库,通过定时任务重试
MqMessage mqMsg = new MqMessage();
mqMsg.setBeanName("testService");
mqMsg.setMethodName("dealMsg");
mqMsg.setMessage(msg);
messageDao.insertMsg(mqMsg);
}
}
这样在处理消息的类里面加上saveMsg方法,这样虽然能达到目的,但是代码侵入性很强。如果要接入很多MQ处理类的话,就需要加入很多相似性很高的代码,不利于代码的扩展和维护。于是就思考着能不能使用注解去标识这个方法,如果加了这个注解,那么这个方法如果执行抛异常了就调用saveMsg方法,这样代码侵入性就非常低。就像热插拔一样,需要处理异常加个注解,不需要处理异常删除注解就可以了,改造后伪代码如下
@Service("testService")
class ReciveMessage{
@MqException
@MqListener("topic1")
public void doMessage(String msg){
this.dealMsg(msg);
}
private void dealMsg(String msg){
// 处理消息业务代码
}
}
切面编程
通过spring的aop可实现切面编程,增强原来的方法,在方法的前后异常做一些其他事情。它有几个重要的注解,在编写切面类时需要用到,作用如下
@Aspect:把当前类标识为一个切面
@Pointcut:Pointcut也叫切点,是织入Advice的触发条件。每个Pointcut的定义包括2部分,一是表达式,二是方法签名。方法签名必须是public及void型。可以将Pointcut中的方法看作是一个被Advice引用的助记符,因为表达式不直观,因此我们可以通过方法签名的方式为此表达式命名。因此Pointcut中的方法只需要方法签名,而不需要在方法体内编写实际代码。切点可以用两种方式指定
通过指定切面类路径,代码如下。可以指定被切入类中的某个方法还是全部方法,以及方法的参数类型
@Pointcut("execution(public public int com.aop.service.MyService.*(String, String))") public void getMethods() { }
通过注解指定切面方法,代码如下
@Pointcut("@annotation(com.aop.annotate.MyException)") public void annotationPointcut() { }
@Around:环绕增强,目标方法执行前后分别执行一些代码
@AfterReturning:返回增强,目标方法正常执行完毕时执行
@Before:前置增强,目标方法执行之前执行
@After:后置增强,不管是抛出异常或者正常退出都会执行
@AfterThrowing:异常抛出增强,目标方法发生异常的时候执行
引入依赖
在spring boot项目中引入如下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
在非spring 项目中引入如下依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.3</version>
</dependency>
编写切面类
首先定义一个注解
package com.aop.annotate;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Auther: Lushunjian
* @Date: 2020/9/22 20:41
* @Description:
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyException {
String value() default "";
}
然后定义切面类
package com.aop.service;
import com.alibaba.fastjson.JSON;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
/**
* @Auther: Lushunjian
* @Date: 2020/9/22 20:42
* @Description:
*/
@Aspect
@Component
public class ValidException {
/**
* 通过注解
*/
@Pointcut("@annotation(com.aop.annotate.MyException)")
public void annotationPointcut() {
}
/**
* 通过路径
*/
@Pointcut("execution(public public int com.aop.service.MyService.*(String, String))")
public void getMethods() {
}
@Before("annotationPointcut()")
public void beforePointcut(JoinPoint joinPoint) {
}
@Around("annotationPointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed();
}
/**
* 在切入点return内容之后切入内容(可以用来对处理返回值做一些加工处理)
* @param joinPoint
*/
@AfterReturning(value = "annotationPointcut()",returning="result")
public void doAfterReturning(JoinPoint joinPoint,Object result) {
}
//@AfterThrowing: 异常通知
@AfterThrowing(value="annotationPointcut()",throwing="e")
public void afterReturningMethod(JoinPoint joinPoint,Exception e){
String methodName = joinPoint.getSignature().getName();
//System.out.println("The method name:"+methodName+ " ends and args="+ JSON.toJSONString(args));
Object object = joinPoint.getThis();
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
//方法所属类的类名
System.out.println("---className:"+ methodSignature.getDeclaringTypeName());
//打印方法名
System.out.println("---method:"+methodName);
//参数名称
String[] names=((MethodSignature) joinPoint.getSignature()).getParameterNames();
// 参数值
Object[] args = joinPoint.getArgs();
//当前调用方法对象
System.out.println("---this:"+object);
System.out.println("---target:"+joinPoint.getTarget());
MyServicePro servicePro = (MyServicePro)object;
System.out.println("---SpringBean Name:"+servicePro.getBeanName());
for(Object obj : args){
System.out.println("---参数Type:"+obj.getClass().getTypeName()+"---参数Value:"+JSON.toJSONString(obj));
}
}
}
由于我需要获取到实例在spring容器中的beanName,因此需要实现spring的一个接口,如下
- 在需要获取的类中 实现BeanNameAware
- 通过getBeanName方法获取id或者name的值
测试类
package com.aop.service;
import com.aop.annotate.MyException;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @Auther: Lushunjian
* @Date: 2020/9/22 21:44
* @Description:
*/
@Service("testMyService")
public class MyService extends MyServicePro{
@MyException
public int myTest(String msg, List<String> list, String s){
if("123".equals(s)){
throw new RuntimeException("测试异常");
}
return 0;
}
//一般情况下@Component、@Service注解未指定bean name的时候,默认是以类名称的的首字母小写作为bean name
}
测试结果
这样我就用一个注解实现了通用代码的抽离
关于切点的配置
切入点方法不用写代码,返回类型为void,有两种方式可以用于表达切点的位置 execution和@annotation,execution:用于匹配表达式,@annotation:用于匹配注解的全路径
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
- 修饰符匹配(modifier-pattern?)
- 返回值匹配(ret-type-pattern)可以为*表示任何返回值,全路径的类名等
- 类路径匹配(declaring-type-pattern?)
- 方法名匹配(name-pattern)可以指定方法名 或者 代表所有, set 代表以set开头的所有方法
- 参数匹配((param-pattern))可以指定具体的参数类型,多个参数间用“,”隔开,各个参数也可以用“”来表示匹配任意类型的参数,如(String)表示匹配一个String参数的方法;(,String) 表示匹配有两个参数的方法,第一个参数可以是任意类型,而第二个参数是String类型;可以用(..)表示零个或多个任意参数
- 异常类型匹配(throws-pattern?) 其中后面跟 着“?”的是可选项
//表示匹配所有方法
1)execution(* *(..))
//表示匹配com. example.controller中所有的public方法
2)execution(public * com. example.controller.*(..))
//表示匹配com. example.controller包及其子包下的所有方法
3)execution(* com. example.controller..*.*(..))