欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

SpringBoot 面向切面编程AOP

程序员文章站 2022-06-05 13:57:05
...

AOP全称Aspect Oriented Programming,面向切面,AOP主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。其与设计模式完成的任务差不多,是提供另一种角度来思考程序的结构,来弥补面向对象编程的不足。

原来在SpringMVC上使用AOP时是采用XML配置的方式,在SpringBoot中则是采用注解的形式,下面就了解一**解实现AOP。

一、AOP主要注解

@Aspect

使用@Aspect注解将一个java类定义为切面类。

@Pointcut

使用@Pointcut定义一个切入点,可以是一个规则表达式,也可以是一个注解等。

@Before

使用@Before在切入点开始处切入内容。

@After

使用@After在切入点结尾处切入内容。

@AfterReturning

使用@AfterReturning在切入点return内容之后切入内容(可以用来对处理返回值做一些加工处理)。

@Around

使用@Around在切入点前后切入内容,并自己控制何时执行切入点自身的内容,又称为环绕通知。

@AfterThrowing

使用@AfterThrowing用来处理当切入内容部分抛出异常之后的处理逻辑。

二、代码实现

SpringBoot的基本配置这里就不写了,仅写一些主要代码。

@SpringBootApplication
@Controller
@ComponentScan(basePackages={"com.test.spring.boot"})   // 自定义自动扫描
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

这里需要注意@ComponentScan中填写的包路径要包含AOP注解类,否则无法生效。

/**
 * 测试SpringBoot实现AOP
 * @author TJ
 */
@Aspect
@Component
public class TestAopAspect {

    /**
     *  public * com.test.spring..controller.*.*(..)) 
     *  
     *  中间两个 .. 代表所有子目录,后面括号里的两个 .. 代表所有参数
     */
    @Pointcut("execution(public * com.test.spring..controller.*.*(..))")
    public void logPointCut() {
    }

    /**
     * 请求处理之前进行一些操作
     * @param joinPoint
     * @throws Throwable
     */
    @Before("logPointCut()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        System.out.println("请求地址:" + request.getRequestURI().toString());
        System.out.println("HTTP METHOD : " + request.getMethod());
        System.out.println("IP : " + request.getRemoteAddr());
        System.out.println("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        System.out.println("参数 : " + Arrays.toString(joinPoint.getArgs()));
    }

    /**
     * 在切入点结尾处进行拦截处理
     * @param joinPoint
     */
    @After("execution(public * com.test.spring..controller.*.*(..))")
    public void doAfter(JoinPoint joinPoint) {
        System.out.println("@After: 处理完成,这里进行一些后续处理");
    }

    /**
     * 在切入点return之后拦截处理
     * @param joinPoint
     */
    @AfterReturning("logPointCut()")
    public void doAfterReturning(JoinPoint joinPoint){
        System.out.println("@AfterReturning:在切入点return之后拦截处理");
    }

    /** 
     * 后置返回通知 
     * 这里需要注意的是: 
     * 如果参数中的第一个参数为JoinPoint,则第二个参数为返回值的信息 
     * 如果参数中的第一个参数不为JoinPoint,则第一个参数为returning中对应的参数 
     * returning 限定了只有目标方法返回值与通知方法相应参数类型时才能执行后置返回通知,否则不执行,对于returning对应的通知方法参数为Object类型将匹配任何目标返回值 
     * @param joinPoint 
     * @param keys 
     */  
    @AfterReturning(value = "execution(public * com.test.spring..controller.*.*(..))",returning = "keys")  
    public void doAfterReturningAdvice1(JoinPoint joinPoint,Object keys){  
          System.out.println("第一个后置返回通知的返回值:"+keys);  
    }  

    @AfterReturning(value = "execution(* com.test.spring..controller..*.*(..))",returning = "keys",argNames = "keys")  
    public void doAfterReturningAdvice2(String keys){  
          System.out.println("第二个后置返回通知的返回值:"+keys);  
    }
}

HelloController

@RestController
public class HelloController {

    @RequestMapping("/hello")
    @ResponseBody
    public String hello() {
        return "Hello test";
    }
}

访问http://localhost:8081/hello,得到打印结果:

![这里写图片描述](https://img-blog.csdn.net/20171116181243078?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvQTYzMjE4OTAwNw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)

三、通知参数

任何通知方法可以将第一个参数定义为org.aspectj.lang.JoinPoint类型(环绕通知需要定义第一个参数为ProceedingJoinPoint类型,它是 JoinPoint 的一个子类)。JoinPoint接口提供了一系列有用的方法,比如 getArgs()(返回方法参数)、getThis()(返回代理对象)、getTarget()(返回目标)、getSignature()(返回正在被通知的方法相关信息)和 toString()(打印出正在被通知的方法的有用信息)

有时候我们定义切面的时候,切面中需要使用到目标对象的某个参数,如何使切面能得到目标对象的参数。

使用args来绑定。如果在一个args表达式中应该使用类型名字的地方 使用一个参数名字,那么当通知执行的时候对应的参数值将会被传递进来。下面看一下例子:

@Before("execution(* findById*(..)) &&" + "args(id,..)")
public void testBefore(Long id){
    System.err.println ("切面before执行了。。。。id==" + id);
}
@Before("logPointCut()")  
public void doBeforeAdvice(JoinPoint joinPoint){  
    System.out.println("我是前置通知!!!");  
    // 获取目标方法的参数信息  
    Object[] obj = joinPoint.getArgs();  
    // AOP代理类的信息  
    joinPoint.getThis();  
    // 代理的目标对象  
    joinPoint.getTarget();  
    // 用的最多 通知的签名  
    Signature signature = joinPoint.getSignature();  
    // 代理的是哪一个方法  
    System.out.println(signature.getName());  
    // AOP代理类的名字  
    System.out.println(signature.getDeclaringTypeName());  
    // AOP代理类的类(class)信息  
    signature.getDeclaringType();  
    // 获取RequestAttributes  
    RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();  
    // 从获取RequestAttributes中获取HttpServletRequest的信息  
    HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);  
    // 如果要获取Session信息的话,可以这样写:  
    //HttpSession session = (HttpSession) requestAttributes.resolveReference(RequestAttributes.REFERENCE_SESSION);  
    Enumeration<String> enumeration = request.getParameterNames();  
    Map<String,String> parameterMap = Maps.newHashMap();  
    while (enumeration.hasMoreElements()){  
        String parameter = enumeration.nextElement();  
        parameterMap.put(parameter,request.getParameter(parameter));  
    }  
    String str = JSON.toJSONString(parameterMap);  
    if(obj.length > 0) {  
        System.out.println("请求的参数信息为:"+str);  
    }  
}

注意:这里用到了JoinPoint和RequestContextHolder。通过JoinPoint可以获得通知的签名信息,如目标方法名、目标方法参数信息等。通过RequestContextHolder来获取请求信息,Session信息。

四、切入点表达式

从上面的例子可以知道,在定义一个切入点时需要一个包含名字和任意参数的签名,还有一个切入点表达式,即:public * com.test.spring..controller.*.*(..)

切入点表达式的格式:execution([可见性] 返回类型 [声明类型].方法名(参数) [异常])

其中【】中的为可选,其他的还支持通配符的使用:
*:匹配所有字符
..:一般用于匹配多个包,多个参数
+:表示类及其子类
运算符有:&&、||、!

切入点表达式关键词

execution:用于匹配子表达式

//匹配com.cjm.model包及其子包中所有类中的所有方法,返回类型任意,方法参数任意
@Pointcut("execution(* com.cjm.model..*.*(..))")
public void before(){
}

within:用于匹配连接点所在的Java类或者包

//匹配Person类中的所有方法
@Pointcut("within(com.cjm.model.Person)")
public void before(){
}

//匹配com.cjm包及其子包中所有类中的所有方法
@Pointcut("within(com.cjm..*)")
public void before(){
}

this:用于向通知方法中传入代理对象的引用

@Before("before() && this(proxy)")
public void beforeAdvide(JoinPoint point, Object proxy){
//处理逻辑
}

target:用于向通知方法中传入目标对象的引用

@Before("before() && target(target)
public void beforeAdvide(JoinPoint point, Object proxy){
     //处理逻辑
}

args:用于将参数传入到通知方法中

@Before("before() && args(age,username)")
public void beforeAdvide(JoinPoint point, int age, String username){
      //处理逻辑
}

@within :用于匹配在类一级使用了参数确定的注解的类,其所有方法都将被匹配

//所有被@AdviceAnnotation标注的类都将匹配
@Pointcut("@within(com.cjm.annotation.AdviceAnnotation)")
public void before(){
}

@target :和@within的功能类似,但必须要指定注解接口的保留策略为RUNTIME

@Pointcut("@target(com.cjm.annotation.AdviceAnnotation)")
public void before(){
}

@args :传入连接点的对象对应的Java类必须被@args指定的Annotation注解标注

@Before("@args(com.cjm.annotation.AdviceAnnotation)")
public void beforeAdvide(JoinPoint point){
    //处理逻辑
}

@annotation :匹配连接点被它参数指定的Annotation注解的方法。也就是说,所有被指定注解标注的方法都将匹配

@Pointcut("@annotation(com.cjm.annotation.AdviceAnnotation)")
public void before(){
}

bean:通过受管Bean的名字来限定连接点所在的Bean。该关键词是Spring2.5新增的

@Pointcut("bean(person)")
public void before(){
}