Springboot项目快速实现Aop功能

前言

这篇文章的有几个关键点,第一,关于AOP的一些基础理论知识,在正式使用AOP前需要了解;第二,Springboot项目中怎么快速集成Aop功能的;第三,AOP的一些使用小技巧和注意事项。

依赖引入

Springboot引入AOP依赖包后,一般来说是不需要再做其他配置了,在比较低的版本或者有其他配置影响了AOP的相关功能,导致aop功能不生效,可以试试在启动类上增加@EnableAspectJAutoProxy来启用;


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

代码实现

1、自定义注解@TestAop


@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAop {
}

2、ExampleAop .java


@Component
@Aspect
@Slf4j
public class ExampleAop {

    //切入点:增强标有@TestAop注解的方法
    @Pointcut(value = "@annotation(TestAop)")
    //切入点签名
    public void pointcut() {
        System.out.println("pointCut签名。。。");
    }

    //前置通知
    @Before("pointcut()")
    public void deBefore(JoinPoint joinPoint) throws Throwable {
        log.info("前置通知被执行");
        //可以joinpoint中得到命中方法的相关信息,利用这些信息可以做一些额外的业务操作;
    }

    //返回通知
    @AfterReturning(returning = "ret", pointcut = "pointcut()")
    public void doAfterReturning(Object ret) throws Throwable {
        log.info("返回通知被执行");
        //可以joinpoint中得到命中方法的相关信息,利用这些信息可以做一些额外的业务操作;
    }

    //异常通知
    @AfterThrowing(throwing = "ex", pointcut = "pointcut()")
    public void throwss(JoinPoint jp, Exception ex) {
        log.info("异常通知被执行");
        //可以joinpoint中得到命中方法的相关信息,利用这些信息可以做一些额外的业务操作;
        //可以从ex中获取到具体的异常信息
    }

    //后置通知
    @After("pointcut()")
    public void after(JoinPoint jp) {
        log.info("后置通知被执行");
        //可以joinpoint中得到命中方法的相关信息,利用这些信息可以做一些额外的业务操作;
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("------环绕通知 start");
        String methodName = proceedingJoinPoint.getSignature().getName();
        String className = proceedingJoinPoint.getTarget().getClass().getName();
        Object[] args = proceedingJoinPoint.getArgs();
        String argsName = null;
        StringBuilder sb = new StringBuilder();
        if (args != null && args.length > 0) {
            for (int i = 0; i < args.length; i++) {
                if (args[i] != null) {
                    sb.append(";").append(args[i].getClass().getName());
                }
            }
            if (sb.toString().length() > 0) {
                argsName = sb.toString().substring(1);
            }
        }
        log.info("命中类:{},方法{},参数{};", className, methodName, argsName);
        Object proceed = proceedingJoinPoint.proceed();
        log.info("------环绕通知 end");
        return proceed;
    }

}

核心注解和类

1、Aspect,表示当前类是一个切面类,简单理解就是切入点和通知的抽象封装,表述的是切入点和通知方法之间的对应关系;

2、@Before,前置通知,用于方法上,被@Before注解标记的方法会在被切入方法执行之前执行;

3、@After,后置通知,用于方法上,被@After注解标记的方法会在被切入方法执行之后执行;

4、@AfterReturning,返回通知,用于方法上,被@AfterReturning注解标记的方法会在被切入方法返回结果之后执行;

6、@AfterThrowing:异常通知,用于方法上,被@AfterThrowing注解标记的方法会在被切入方法抛出异常之后执行,一般用于有目的的获取异常信息;

7、@Aroud:环绕通知,用于方法上,被@Around注解标记的方法会在被切入方法执行前后执行;

8、@Pointcut,切入点,标记在方法上,用于定义切入点,所谓的切入点是指对哪些连接点处进行切入增强的定义,在Spring中具体就是指对哪些方法进行切入增强的定义;被@Pointcut注解表示切入点的表达式有多种,最常用的是两种,execution表达式和注解;

9、Jointpoint,连接点,所谓的连接点是指被aop切面切入的位置点,在Spring中具体就是指被切入的方法;

10、PointCut,

11、Advice,通知,所谓的通知是指对定义好的切入点进行拦截后,要具体做哪些操作的定义;在Spring中就是指被@Before、@After、@AfterReturning、@AfterThrowing、@Around注解标记的方法;

标记切入点的常用方式

1、execution表达式

表达式请法:访问修饰符 返回值 包名.包名...类名.方法(参数列表)

示例1:表示匹配所有com.fanfu包以及子包下的所有类中以add开头的方法,返回值、参数不限;


@Pointcut("execution(* com.fanfu..*.*.add*(..))")

示例2:表示匹配所有com.fanfu包以及子包下的所有类中以add开头,参数类型是String的方法,返回值不限;


@Pointcut("execution(* com.fanfu..*.*.add*(String))")

示例3:表示匹配com.fanfu包下任意类、任意方法、任意参数;


@Pointcut("execution(* com.fanfu..*.*.*(String))")
execution()为表达式的主体;
第一个*表示返回值类型为任意,即不限制返回值类型;
包后的*表示当前包,包后面连续两个..表示当前包以及子包;
(..)表示任意参数;
最后的*.*(..)表示匹配任意类、任意方法、任意参数;

2、注解

注解语法:@annotation(自定义的注解)

示例:表示匹配所有标记@TestAop注解的方法;


@Pointcut("@annotation(com.fanfu.config.TestAop)")

Spring Aop的小技巧

每一个@Pointcut可以使用execution或注解来定义切入点,多个切点之间还可以使用逻辑运算符,如&&、||、!运算;

1、point1()&&point2()表示命中point1和point2的所有切入点的交集;如示例:com.fanfu包以及下属所有子包的所有类中,方法名是以add开头,参数类型是String的所有方法,与com.fanfu.service包以及下属所有子包的所有类中,不限方法名和参数类型的所有方法取交集,即com.fanfu.service包以及下属所有子包的所有类中,方法或是add1或add2的方法,在调用前后都会执行环绕通知around()方法内的逻辑;


@Component
@Aspect
@Slf4j
public class ExampleAop {
    @Pointcut("execution(* com.fanfu..*.*.add*(String))")
    public void point1() {
    }
    @Pointcut("execution(* com.fanfu.service..*.*(..))")
    public void point2() {
    }
    @Pointcut("point1()&&point2()")
    public void point() {
    }
    @Around("point()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("------around start");
        Object proceed = proceedingJoinPoint.proceed();
        log.info("------around end");
        return proceed;
    }
}

2、point1()&&point2()表示命中point1和point2的所有切入点的并集;如示例:com.fanfu.service包以及下属所有子包的所有类中,方法名是add1,参数类型是String的所有方法,与com.fanfu.controller包以及下属所有子包的所有类中,方法名是add2,参数类型是String的所有方法取并集,即com.fanfu.service或com.fanfu.controller的包以及下属所有子包的所有类中,方法或是add1或add2的方法,在调用前后都会执行环绕通知around()方法内的逻辑;


@Component
@Aspect
@Slf4j
public class ExampleAop {
    @Pointcut("execution(* com.fanfu.service..*.add*(String))")
    public void point1() {
    }
    @Pointcut("execution(* com.fanfu.controller..*.*.add*(String))")
    public void point2() {
    }
    @Pointcut("point1()||point2()")
    public void point() {
    }
    @Around("point()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("------around start");
        Object proceed = proceedingJoinPoint.proceed();
        log.info("------around end");
        return proceed;
    }
}

3、!point()表示命中point()的所有切入点取反,如示例:com.fanfu.service包及下属所有子包的所有类中,不是以add开头的方法,在调用前后都会执行环绕通知around()方法内的逻辑;


@Component
@Aspect
@Slf4j
public class ExampleAop {
    @Pointcut("execution(* com.fanfu.service..*.add*(String))")
    public void point() {
    }
    @Around("!point()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("------around start");
        Object proceed = proceedingJoinPoint.proceed();
        log.info("------around end");
        return proceed;
    }
}

Spring Aop注意事项

1、与定义的切点匹配方法,如果在当前调用链中,方法在当前类是首次匹配则会命中,即执行相关的通知,如果当前的调用链没有结束,又在当前方法里调用了当前类的与其他切入点匹配方法,则不会再命中,即其他与切入点匹配的方法执行的时候不会再触发相关的通知;

如下示例:

当请求http://localhost:8080/example时,ExampleController中的example方法被触发,ExampleController#example()又调用了ExampleService#test(),在ExampleService#test()内部,又顺序调用了ExampleService#test1()和ExampleService#test2();在ExampleAop中,按照execution中的配置,是可以匹配到test()、test1()、test2(),实际是命中的方法只有test();


@Component
@Aspect
@Slf4j
public class ExampleAop {
    @Pointcut("execution(* com.fanfu.service.impl.ExampleServiceImpl.test*(..))")
    public void point2() {
        log.info("切入点匹配");
    }
    @Around("point()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("------around start");
        String methodName = proceedingJoinPoint.getSignature().getName();
        String className = proceedingJoinPoint.getTarget().getClass().getName();
        Object[] args = proceedingJoinPoint.getArgs();
        String argsName=null;
        StringBuilder sb=new StringBuilder();
        if (args!=null&&args.length>0) {
            for (int i = 0; i < args.length; i++) {
                if (args[i] != null) {
                    sb.append(";").append(args[i].getClass().getName());
                }
            }
            if (sb.toString().length()>0) {
                argsName=sb.toString().substring(1);
            }
        }
        log.info("命中类:{},方法{},参数{};",className,methodName,argsName);
        Object proceed = proceedingJoinPoint.proceed();
        log.info("------around end");
        return proceed;
    }
}

@Service
@Slf4j
public class ExampleServiceImpl implements IExampleService {

    @Override
    public String test(String msg) {
        log.info("test方法被执行");
        this.test1(msg);
        this.test2(msg);
        return msg;
    }

    public String test1(String msg) {
        log.info("test1方法被执行");
        return "msg1";
    }

    public String test2(String msg) {
        log.info("test2方法被执行");
        return "msg2";
    }
}

public interface IExampleService {
    public String test(String msg);
    public String test1(String msg);
    public String test2(String msg);
}

@RestController
@Slf4j
public class ExampleController {
    @Autowired
    private IExampleService exampleService;
    @GetMapping("/example")
    public String example() {
        log.info("example start");
        exampleService.test(null);
        log.info("example end");
        return String.valueOf(System.currentTimeMillis());
    }
}

2、对于上面的问题,如果把execution表达换成注解,会不会结果不一样?再把ExampleAop中的@Pointcut改成注解形式,再在ExampleService#test1()、ExampleService#test2()、ExampleService#test()添加注解@TestAop,验证结果依然是一样的,只有test()会命中,其他不会!所以要注意呀。


@Component
@Aspect
@Slf4j
public class ExampleAop {
    @Pointcut("@annotation(TestAop)")
    public void point() {
    }
    @Around("point()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("------around start");
        String methodName = proceedingJoinPoint.getSignature().getName();
        String className = proceedingJoinPoint.getTarget().getClass().getName();
        Object[] args = proceedingJoinPoint.getArgs();
        String argsName = null;
        StringBuilder sb = new StringBuilder();
        if (args != null && args.length > 0) {
            for (int i = 0; i < args.length; i++) {
                if (args[i] != null) {
                    sb.append(";").append(args[i].getClass().getName());
                }
            }
            if (sb.toString().length() > 0) {
                argsName = sb.toString().substring(1);
            }
        }
        log.info("命中类:{},方法{},参数{};", className, methodName, argsName);
        Object proceed = proceedingJoinPoint.proceed();
        log.info("------around end");
        return proceed;
    }
}

@Service
@Slf4j
public class ExampleServiceImpl implements IExampleService {

    @Override
    @TestAop
    public String test(String msg) {
        log.info("test方法被执行");
        this.test1(msg);
        this.test2(msg);
        return msg;
    }
    @Override
    @TestAop
    public String test1(String msg) {
        log.info("test1方法被执行");
        return "msg1";
    }
    @Override
    @TestAop
    public String test2(String msg) {
        log.info("test2方法被执行");
        return "msg2";
    }
   
}

3、那什么情况下,ExampleService#test1()、ExampleService#test2()、ExampleService#test()会同时命中呢?让从ExampleController#example()到ExampleService#test1()、ExampleService#test2()、ExampleService#test()分别在不同的调用链上,那么就可以同时命中了;


@RestController
@Slf4j
public class ExampleController {
    @Autowired
    private IExampleService exampleService;
    @GetMapping("/example")
    public String example() {
        log.info("example start");
        exampleService.test(null);
        exampleService.test1(null);
        exampleService.test2(null);
        log.info("example end");
        return String.valueOf(System.currentTimeMillis());
    }
}

总结

Springboot集成Aop并不难,难的是如何在实际业务中用好。如果想要用好AOP,那么理解Spring AOP的工作原理就十分有必要了,比如明明按照@Pointcut中定义已经匹配到了具体的切入点,但实际上并不一定会命中,即触发相关的通知。Spring AOP的工作原理,这个坑后面肯定会补上的。

  • 6
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
springboot学习笔记 spring基础 Spring概述 Spring的简史 xml配置 注解配置 java配置 Spring概述 Spring的模块 核心容器CoreContainer Spring-Core Spring-Beans Spring-Context Spring-Context-Support Spring-Expression AOP Spring-AOP Spring-Aspects Messaging Spring-Messaging WEB Spring-Web Spring-Webmvc Spring-WebSocket Spring-Webmvc-Portlet 数据访问/集成(DataAccess/Intefration) Spring-JDBC Spring-TX Spring-ORM Spring-OXM Spring-JMS Spring的生态 Spring Boot Spring XD Spring Cloud Spring Data Spring Integration Spring Batch Spring Security Spring HATEOAS Spring Social Spring AMQP Spring Mobile Spring for Android Spring Web Flow Spring Web Services Spring LDAP Spring Session Spring项目快速搭建 Maven简介 Maven安装 Maven的pom.xml dependencies dependency 变量定义 编译插件 Spring项目的搭建 Spring Tool Suite https://spring.io/tools/sts/all IntelliJ IDEA NetBeans https://netbeans.org/downloads/ Spring基础配置 依赖注入 声明Bean的注解 @Component组件,没有明确的角色 @Service在业务逻辑层(service层) @Repository在数据访问层(dao层) @Controller在展现层(MVC→SpringMVC) 注入Bean的注解 @Autowired:Spring提供的注解 @Inject:JSR-330提供的注解 @Resource:JSR-250提供的注解 Java配置 @Configuration声明当前类是一个配置类 @Bean注解在方法上,声明当前方法的返回值为一个Bean AOP @Aspect 声明是一个切面 拦截规则@After @Before @Around PointCut JoinPoint Spring常用配置 Bean的Scope Singleton Prototype Request Session GlobalSession SpringEL和资源调用 注入普通字符 注入操作系统属性 注入表达式云算结果 注入其他Bean的属性 注入文件内容 注入网址内容 注入属性文件 Bean的初始化和销毁 Java配置方式 注解方式 Profile @Profile 通过设定jvm的spring.profiles.active参数 web项目设置在Servlet的context parameter中 事件Application Event 自定义事件,集成ApplicationEvent 定义事件监听器,实现ApplicationListener 使用容器发布事件 Spring高级话题 Spring Aware BeanNameAware BeanFactoryAware

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凡夫贩夫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值