spring的切面编程


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中的方法只需要方法签名,而不需要在方法体内编写实际代码。切点可以用两种方式指定

    1. 通过指定切面类路径,代码如下。可以指定被切入类中的某个方法还是全部方法,以及方法的参数类型

      @Pointcut("execution(public public int com.aop.service.MyService.*(String, String))")
         public void getMethods() {
          }
    2. 通过注解指定切面方法,代码如下

      @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的一个接口,如下

  1. 在需要获取的类中 实现BeanNameAware
  2. 通过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..*.*(..))  

Author: 顺坚
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 顺坚 !
评论
 Previous
序列化与反序列化 序列化与反序列化
在Java开发中常会听到序列化与反序列化,特别是Web应用开发时,网络之间需要传输对象用到序列化的频率非常频繁。在此总结一下序列化的原理,在Java中实现序列化的常用方法是实现Serializable接口。 序列化:把Java对象转换为字
2020-10-03
Next 
Go语言的协程 Go语言的协程
在接触到Go语言时,了解到协程的概念。协程,又称微线程,纤程。英文名Coroutine,作为传统线程模型的进化版,虽说协程这个概念几十年前就有了,但是协程只是在近年才开始兴起,Go 、Kotlin、Python , 都是支持协程的。特别是G
2020-09-13
  TOC