1.AOP是什么?

1.是一种编程范式,不是编程语言
2.解决特定问题,不能解决所有问题
3.是OOP(Object Oriented Programming)的补充,不是替代

五个注解:

  • 前置通知(Before):在目标方法被调用之前调用通知功能;
  • 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
  • 返回通知(After-returning):在目标方法成功执行之后调用通知;
  • 异常通知(After-throwing):在目标方法抛出异常后调用通知;
  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。

2.Aop使用前后对比

2.1 使用Aop前

所需要的依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-aop</artifactId>
  4. </dependency>

先看一下代码结构
image.png

  1. @Data
  2. public class Product {
  3. private Long id;
  4. private String name;
  5. }
  1. public class CurrentUserHolder {
  2. private static final ThreadLocal<String> holder = new ThreadLocal<>();
  3. public static String get() {
  4. return holder.get() == null ? "unknown" : holder.get();
  5. }
  6. public static void set(String user) {
  7. holder.set(user);
  8. }
  9. }
  1. @Component
  2. public class AuthService {
  3. public void checkAccess() {
  4. String user = CurrentUserHolder.get();
  5. if (!"admin".equals(user)) {
  6. throw new RuntimeException("operation not allow");
  7. }
  8. }
  9. }

现在的需求是这两个方法都需要经过校验,身份是admin才能执行,这种写法相当于硬编码,假设我300个方法都需要校验的情况,那我需要一条条的添加到方法中去。

  1. @Service
  2. @Slf4j
  3. public class ProductService {
  4. @Autowired
  5. AuthService authService;
  6. public void insert(Product product) {
  7. authService.checkAccess();
  8. log.info("insert product");
  9. }
  10. public void delete(Long id) {
  11. authService.checkAccess();
  12. log.info("delete product");
  13. }
  14. }

执行测试看一下结果:

  1. @SpringBootTest
  2. class AopApplicationTests {
  3. @Autowired
  4. ProductService productService;
  5. @Test
  6. void contextLoads() {
  7. }
  8. @Test
  9. public void anoInsertTest() {
  10. CurrentUserHolder.set("tom");
  11. productService.delete(1L);
  12. }
  13. @Test
  14. public void adminInsert() {
  15. CurrentUserHolder.set("admin");
  16. productService.delete(1L);
  17. }
  18. }

2.2使用Aop后

  1. @Retention(RetentionPolicy.RUNTIME)
  2. @Target(ElementType.METHOD)
  3. public @interface AdminOnly {
  4. }
  1. @Aspect
  2. @Component
  3. public class SecurityAspect {
  4. @Autowired
  5. AuthService authService;
  6. // 拦截标注AdminOnly的方法
  7. @Pointcut("@annotation(AdminOnly)")
  8. public void adminOnly(){
  9. }
  10. // 在执行前插入这行代码
  11. @Before("adminOnly()")
  12. public void check(){
  13. authService.checkAccess();
  14. }
  15. }

去掉这种硬编码的方式加上注解。

  1. @Service
  2. @Slf4j
  3. public class ProductService {
  4. @AdminOnly
  5. public void insert(Product product) {
  6. log.info("insert product");
  7. }
  8. @AdminOnly
  9. public void delete(Long id) {
  10. log.info("delete product");
  11. }
  12. }

总结:执行后的结果是一致的,很显然使用aop幸福感更高

3.切面表达式

image.png

  1. 任意公共方法的执行:
  2. execution(public * *(..))
  3. 任何一个以“set”开始的方法的执行:
  4. execution(* set*(..))
  5. AccountService 接口的任意方法的执行:
  6. execution(* com.xyz.service.AccountService.*(..))
  7. 定义在service包里的任意方法的执行:
  8. execution(* com.xyz.service.*.*(..))
  9. 定义在service包和所有子包里的任意类的任意方法的执行:
  10. execution(* com.xyz.service..*.*(..))
  11. 定义在pointcutexp包和所有子包里的JoinPointObjP2类的任意方法的执行:
  12. execution(* com.test.spring.aop.pointcutexp..JoinPointObjP2.*(..))")
  13. ***> 最靠近(..)的为方法名,靠近.*(..))的为类名或者接口名,如上例的JoinPointObjP2.*(..))
  14. pointcutexp包里的任意类.
  15. within(com.test.spring.aop.pointcutexp.*)
  16. pointcutexp包和所有子包里的任意类.
  17. within(com.test.spring.aop.pointcutexp..*)
  18. 实现了MyInterface接口的所有类,如果MyInterface不是接口,限定MyInterface单个类.
  19. this(com.test.spring.aop.pointcutexp.MyInterface)
  20. ***> 当一个实现了接口的类被AOP的时候,用getBean方法必须cast为接口类型,不能为该类的类型.
  21. 带有@MyTypeAnnotation标注的所有类的任意方法.
  22. @within(com.elong.annotation.MyTypeAnnotation)
  23. @target(com.elong.annotation.MyTypeAnnotation)
  24. 带有@MyTypeAnnotation标注的任意方法.
  25. @annotation(com.elong.annotation.MyTypeAnnotation)
  26. ***> @within@target针对类的注解,@annotation是针对方法的注解
  27. 参数带有@MyMethodAnnotation标注的方法.
  28. @args(com.elong.annotation.MyMethodAnnotation)
  29. 参数为String类型(运行是决定)的方法.
  30. args(String)

3.1within表达式

  1. @Component
  2. @Aspect
  3. @Slf4j
  4. public class PkgTypeAspectConfig {
  5. // 匹配类
  6. // @Pointcut("within(com.example.aop.service.AuthService)")
  7. // 匹配包名下所有的类
  8. @Pointcut("within(com.example.aop.service.*)")
  9. public void matchType() {
  10. }
  11. @Before("matchType()")
  12. public void before() {
  13. log.info("PkgTypeAspectConfig");
  14. }
  15. }

3.2对象匹配

this匹配实现接口下的所有类

  1. public interface Work {
  2. void working();
  3. }
  1. @Service
  2. public class MondayWorkServiceImpl implements Work {
  3. @Override
  4. public void working() {
  5. }
  6. }
  1. @Component
  2. @Aspect
  3. @Slf4j
  4. public class ObjectiveAspectConfig {
  5. // 匹配Work接口下的所有类
  6. @Pointcut("this(com.example.aop.service.Work)")
  7. public void matchCondition() {
  8. }
  9. @Before("matchCondition()")
  10. public void before() {
  11. log.info("matchCondition");
  12. }
  13. }

在没有引入Introduction的情况下this和target结果是一样的

  1. @Component
  2. @Aspect
  3. @Slf4j
  4. public class ObjectiveAspectConfig {
  5. // 匹配Work接口下的所有类
  6. @Pointcut("target(com.example.aop.service.Work)")
  7. public void matchCondition() {}
  8. @Before("matchCondition()")
  9. public void before() {
  10. log.info("matchCondition");
  11. }
  12. }

匹配bean注意类名开头小写,我学习过程中因为没有小写导致排查了一小时

  1. @Component
  2. @Aspect
  3. @Slf4j
  4. public class ObjectiveAspectConfig {
  5. // bean名称
  6. @Pointcut("bean(mondayWorkServiceImpl))")
  7. public void matchCondition() {}
  8. @Before("matchCondition()")
  9. public void before() {
  10. log.info("matchCondition");
  11. }
  12. }

3.3参数匹配

  1. @Aspect
  2. @Component
  3. public class ArgsAspectConfig {
  4. // @Pointcut("args(Long,String) && within(com.example.aop.service.*)") 匹配Long,String
  5. // @Pointcut("args(Long,..) && within(com.example.aop.service.*)") 匹配Long,和任意类型
  6. // @Pointcut("args(..) && within(com.example.aop.service.*)") 匹配任意类型
  7. // 匹配long类型
  8. @Pointcut("args(*) && within(com.example.aop.service.*)")
  9. public void matchArgs() {
  10. }
  11. @Before("matchArgs()")
  12. public void before() {
  13. System.out.println("###before");
  14. }
  15. }

3.4注解方式匹配

  1. @Component
  2. @Aspect
  3. @Slf4j
  4. public class AnoAspectConfig {
  5. @Pointcut("@annotation(com.example.aop.security.AdminOnly)")
  6. public void matchAno(){}
  7. @Before("matchAno()")
  8. public void before(){
  9. System.out.println("AnoAspectConfig");
  10. }
  11. }
  1. @Retention(RetentionPolicy.RUNTIME)
  2. @Target(ElementType.METHOD)
  3. public @interface AdminOnly {
  4. }

添加注解到方法上即可实现拦截

  1. @AdminOnly
  2. public void delete(Long id) {
  3. log.info("delete product");
  4. }

可被继承的注解

  1. @Retention(RetentionPolicy.RUNTIME)
  2. @Target(ElementType.TYPE)
  3. @Inherited //支持继承
  4. public @interface NeedSecured {
  5. }

父类

  1. @Service
  2. @Slf4j
  3. @NeedSecured
  4. public class ProductService {
  5. @AdminOnly
  6. public void insert(Product product) {
  7. log.info("insert product");
  8. }
  9. @AdminOnly
  10. public void delete(Long id) {
  11. log.info("delete product");
  12. }
  13. public void withinTest(){
  14. log.info("withinTest");
  15. }
  16. }

子类继承父类NeedSecured注解也被继承,所以都会匹配成功

  1. @Component
  2. public class SubProductService extends ProductService {
  3. public void demo() {
  4. System.out.println("SubProductService");
  5. }
  6. }

在spring context的环境下,二者没有区别target和within

  1. @Component
  2. @Aspect
  3. @Slf4j
  4. public class AnoAspectConfig {
  5. @Pointcut("@target(com.example.aop.security.NeedSecured)")
  6. public void matchAno(){}
  7. @Before("matchAno()")
  8. public void before(){
  9. System.out.println("AnoAspectConfig");
  10. }
  11. }

在类上面添加NeedSecured注解调用改类就会被匹配

  1. @NeedSecured
  2. public class Product {
  3. }
  1. @Component
  2. @Aspect
  3. @Slf4j
  4. public class AnoAspectConfig {
  5. @Pointcut("@args(com.example.aop.security.NeedSecured)")
  6. public void matchAno(){}
  7. @Before("matchAno()")
  8. public void before(){
  9. System.out.println("AnoAspectConfig");
  10. }
  11. }

3.5 execution匹配(重点掌握)

  1. @Aspect
  2. @Component
  3. public class ExecutionAspectConfig {
  4. // @Pointcut("execution(public String com.imooc.service..*(..))") 匹配service包含子包且只匹配无返回值
  5. // @Pointcut("execution(public String com.imooc.service..*(..))") 匹配service包含子包且只匹配String的返回值
  6. // @Pointcut("execution(public * com.imooc.service..*(..))") 匹配service包含子包任意返回值
  7. // @Pointcut("execution(public * com.imooc.service..*())") 匹配service包含子包无参数
  8. // @Pointcut("execution(public * com.imooc.service..*(Long))") 匹配service包含子包且拦截参数的Long类型
  9. // @Pointcut("execution(public * com.imooc.service.*(..) throws java.lang.IllegalAccessException)") 只匹配抛出IllegalAccessException的类
  10. // 匹配service下的包不包含子包
  11. @Pointcut("execution(public * com.imooc.service.*(..))")
  12. public void matchCondition() {
  13. }
  14. @Before("matchCondition()")
  15. public void before() {
  16. System.out.println("");
  17. System.out.println("###before");
  18. }
  19. }

还能这样写匹配Note开头的所有类除NoteWeiXinCodeController

  1. @Pointcut("execution(public * com.jideos.jnotes.controller.Note*.*(..))" +
  2. "&& !execution(public * com.jideos.jnotes.controller.NoteWeiXinCodeController*.*(..))")
  3. public void verify() {
  4. }

3.6 advice注解

Around注解可以用来在调用一个具体方法前和调用后来完成一些具体的任务,如下就用获取方法执行时间

  1. @Aspect
  2. @Component
  3. @Slf4j
  4. public class TimeInterceptor {
  5. // 一分钟,即60000ms
  6. private static final long ONE_MINUTE = 60000;
  7. // service层的统计耗时切面,类型必须为final String类型的,注解里要使用的变量只能是静态常量类型的
  8. public static final String POINT = "execution (* com.example.aop.*.*(..))";
  9. // 统计方法执行耗时Around环绕通知
  10. @Around(POINT)
  11. public Object timeAround(ProceedingJoinPoint joinPoint) {
  12. // 定义返回对象、得到方法需要的参数
  13. Object obj = null;
  14. Object[] args = joinPoint.getArgs();
  15. long startTime = System.currentTimeMillis();
  16. try {
  17. obj = joinPoint.proceed(args);
  18. } catch (Throwable e) {
  19. log.error("统计某方法执行耗时环绕通知出错", e);
  20. }
  21. // 获取执行的方法名
  22. long endTime = System.currentTimeMillis();
  23. MethodSignature signature = (MethodSignature) joinPoint.getSignature();
  24. String methodName = signature.getDeclaringTypeName() + "." + signature.getName();
  25. // 打印耗时的信息
  26. this.printExecTime(methodName, startTime, endTime);
  27. return obj;
  28. }
  29. // 打印方法执行耗时的信息,如果超过了一定的时间,才打印
  30. private void printExecTime(String methodName, long startTime, long endTime) {
  31. long diffTime = endTime - startTime;
  32. if (diffTime > ONE_MINUTE) {
  33. log.warn("-----" + methodName + " 方法执行耗时:" + diffTime + " ms");
  34. }
  35. }
  36. }