springboot中的AOP

  • AOP分享,springboot中的aop

    • springboot中引入aop

      <!--aop-->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-aop</artifactId>
      </dependency>
      复制代码

    关于

    spring:
      aop:
        auto: true #启动aop配置
    复制代码

    @EnableAspectJAutoProxy

    这个两个配置都是启动aop的,但是其实在springboot中aop已经被默认开启了,所以不需要写这两个配置

在springboot中默认使用的cglib代理而spring中使用的jdk的动态代理,那么在springboot中如果想要使用jdk的动态代理怎么办呢?此时可以在配置文件中进行配置spring.aop.proxy-target-class=false,但是如@EnableAspectJAutoProxy一类的注解已经不能改变springboot的代理模式了,因为这类注解本身就是启用的jdk的动态代理,然而他们并没有起作用。

  • 名词解释

    • 通知(有的地方叫增强)(Advice):需要完成的工作叫做通知,就是你写的业务逻辑中需要比如事务、日志等先定义好,然后需要的地方再去用

    • 连接点(Join point):是spring中允许使用通知的地方,基本上每个方法前后抛异常时都可以是连接点

    • 切点(Poincut) :其实就是筛选出的连接点,一个类中的所有方法都是连接点,但又不全需要,会筛选出某些作为连接点做为切点。如果说通知定义了切面的动作或者执行时机的话,切点则定义了执行的地点

    • 切面(Aspect):其实就是通知和切点的结合,通知和切点共同定义了切面的全部内容,它是干什么的,什么时候在哪执行

    • 引入(Introduction):在不改变一个现有类代码的情况下,为该类添加属性和方法,可以在无需修改现有类的前提下,让它们具有新的行为和状态。其实就是把切面(也就是新方法属性:通知定义的)用到目标类中去

    • 目标(target):被通知的对象。也就是需要加入额外代码的对象,也就是真正的业务逻辑被组织织入切面。

    • 织入(Weaving):把切面加入程序代码的过程。切面在指定的连接点被织入到目标对象中,在目标对象的生命周期里有多个点可以进行织入:

      编译期:切面在目标类编译时被织入,这种方式需要特殊的编译器

      类加载期:切面在目标类加载到JVM时被织入,这种方式需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码

      运行期:切面在应用运行的某个时刻被织入,一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象,Spring AOP就是以这种方式织入切面的。

  • 创建切面

    在springboot中使用@Aspect将一个类标注为切面类,但是只是使用@Aspect并不能直接使用,还需要使用@Component注解将这个类注入

  • 切点

    • 写法

      1. 可以在每个通知的注解上写

        @Before(value = "@annotation(TestAnnotation)")
        复制代码
      2. 可以创建一个方法在方法上使用@Pointcut标记切点位置

        @Pointcut("@annotation(TestAnnotation)")
        public void pointCut(){
        }
        复制代码
    • 切入点指示符

      1. exeution

        execution(modifier-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
        复制代码
        • modifier-pattern:指定方法的修饰符,支持通配符,该部分可以省略

        • ret-type-pattern:指定返回值类型,支持通配符,可以使用“*”来通配所有的返回值类型

        • declaring-type-pattern:指定方法所属的类,支持通配符,该部分可以省略

        • name-pattern:指定匹配的方法名,支持通配符,可以使用“*”来通配所有的方法名

        • param-pattern:指定方法的形参列表,支持两个通配符,“”和“..”,其中“”代表一个任意类型的参数,而“..”代表0个或多个任意类型的参数。

        • throw-pattern:指定方法声明抛出的异常,支持通配符,该部分可以省略

          例子:

          execution(public * * (..))//匹配所有public方法

          execution(* set*(..))//匹配以set开始的方法

          execution(* com.abc.service.AdviceManager.* (..))//匹配AdviceManager中任意方法

          execution(* com.abc.service.. (..))//匹配com.abc.servcie包中任意类的任意方法

      2. within

        只能匹配类内的所有方法

        //匹配com.zejian.dao包及其子包中所有类中的所有方法 within(com.zejian.dao..*)

        //匹配实现了DaoUser接口的所有子类的方法

        within(com.zejian.dao.DaoUser+)

        除了within之外还有@within其是within的注解使用方式

      3. 注解方式

        @Pointcut("@annotation(TestAnnotation)")

      4. args和@args

        args配置的是有此参数的所有方法,而@args匹配的是参数的类上有此注解的所有方法

      5. 其他

        除了上面的指示符之外还有this、bean、target等this指向的是目标类,而target只想的代理类,bean是指能匹配通过的bean

  • 通知

    • 通知的类型及其参数

      1. 环绕通知:ProceedingJoinPoint

      2. 前置通知:JoinPoint

      3. 后置通知:JoinPoint

      4. 返回通知:JoinPoint、returning

        returning:限定了只有目标方法返回值与通知方法相应参数类型时才能执行后置返回通知,否则不执行,对于returning对应的通知方法参数为Object类型将匹配任何目标返回值

      5. 异常通知:JoinPoint、throwing

        throwing:限定了只有目标方法抛出的异常与通知方法相应参数异常类型时才能执行后置异常通知,否则不执行,对于throwing对应的通知方法参数为Throwable类型将匹配任何异常。

    • 通知的执行顺序

      ①不自定义情况下,无异常的aop执行流程:环绕前置==》前置==》程序执行==》环绕后置==》后置==》返回
      ②不自定义情况下,有异常的aop执行流程:环绕前置==》前置==》程序执行==》环绕后置==》后置==》异常返回
      复制代码
    • JoinPoint 和ProceedingJoinPoint

    • public interface JoinPoint {  
         String toString();         //连接点所在位置的相关信息  
         
         String toShortString();     //连接点所在位置的简短相关信息 
         
         String toLongString();     //连接点所在位置的全部相关信息  
         
         Object getThis();         //返回AOP代理对象,也就是com.sun.proxy.$Proxy18
         
         Object getTarget();       //返回目标对象或者是接口(也就是定义方法的接口或类,为什么会是接口呢?这主要是在目标对象本身是动态代理的情况下,例如Mapper。所以返回的是定义方法的对象如aoptest.daoimpl.GoodDaoImpl或com.b.base.BaseMapper<T, E, PK>)
         
         Object[] getArgs();       //返回被通知方法参数列表  
         
         Signature getSignature();  //返回当前连接点签名  其getName()方法返回方法的FQN,如void aoptest.dao.GoodDao.delete()或com.b.base.BaseMapper.insert(T)(需要注意的是,很多时候我们定义了子类继承父类的时候,我们希望拿到基于子类的FQN,这直接可拿不到,要依赖于AopUtils.getTargetClass(point.getTarget())
         
         SourceLocation getSourceLocation();//返回连接点方法所在类文件中的位置  
         
         String getKind();        //连接点类型  
         
         StaticPart getStaticPart(); //返回连接点静态部分  
        }  
      复制代码

      ProceedingJoinPoint继承了JoinPoint ,除此之外添加了proceed、proceed(Object[] args)两个方法

    • 用环绕通知替代前置通知、返回通知、异常通知、后置通知

      @Around(value ="pointCut()")
      public Object aroundCut(ProceedingJoinPoint proceedingJoinPoint) {
          logger.info("前置通知");
          Object proceed = null;
          try {
              proceed = proceedingJoinPoint.proceed();
              System.out.print(proceed);
              logger.info("后置通知");
          } catch (Throwable throwable) {
              throwable.printStackTrace();
              logger.info("异常通知");
          }finally {
              logger.info("返回通知");
          }
          return proceed;
      }
      复制代码
    • 前置通知和返回通知改变参数和返回值

         //前置通知,在改变参数的时候,不能改变基本类型的参数,如果想要改变基本类型的参数,需要创     建一个封装类
         @Before("pointCut()")
          public void beforeCut(JoinPoint joinPoint){
              Object[] args = joinPoint.getArgs();
              for (Object o: args){
                  System.out.println(o);
                  if (o instanceof Person){
                      Person person = (Person) o;
                      person.setName("zhangsan");
                      System.out.println(person);
                  }
                  logger.info(o.toString());
              }
          }
          //后置通知
          @AfterReturning(value = "pointCut()",returning = "keys")
          public void returningCut(JoinPoint joinPoint,Object keys){
                if (keys instanceof RetKit){
                    RetKit retKit = (RetKit) keys;
                    retKit.data(222);
                }
      
          }
      复制代码
    • 在环绕通知中改变参数和返回值

         @Around(value ="pointCut()")
          public Object aroundCut(ProceedingJoinPoint proceedingJoinPoint)  {
              logger.info("前置通知");
              Object[] args = proceedingJoinPoint.getArgs();
              int i =0;
              for (Object arg:args){
                  if (arg instanceof Integer){
                      args[i]=2;
                  }
                  i++;
              }
              Object proceed = null;
              try {
                  proceed = proceedingJoinPoint.proceed(args);
                  logger.info("后置通知");
              } catch (Throwable throwable) {
                  throwable.printStackTrace();
                  logger.info("异常通知");
              }finally {
                  RetKit retKit = (RetKit) proceed;
                  retKit.setData("修改结果");
                  logger.info(proceed.toString());
                  logger.info("返回通知");
              }
              return proceed;
          }
      复制代码
    • 在通知中可以使用request和response

      @Before("pointCut()")
      public void beforeCut(JoinPoint joinPoint){
          ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
          HttpServletRequest request = attributes.getRequest();
          HttpServletResponse response = attributes.getResponse();
          //url
          logger.info("url={}",request.getRequestURI());
          //method
          logger.info("method={}", request.getMethod());
          //ip
          logger.info("ip={}", request.getRemoteAddr());
          //类方法
          logger.info("classMethod={}", joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
          //参数
          Enumeration<String> paramter = request.getParameterNames();
          while (paramter.hasMoreElements()) {
              String str = (String) paramter.nextElement();
              logger.info(str + "={}", request.getParameter(str));
          }
           //重定向或者转发
          try {
              response.sendRedirect("/person/error");
            request.getRequestDispatcher("/person/error").forward(request,response);
          } catch (IOException e) {
              e.printStackTrace();
          }catch (ServletException e) {
              e.printStackTrace();
          }
      }
      复制代码
    • 异常通知及扩展

      AOP的AfterThrowing处理虽然可以对目标方法的异常进行处理,但这种处理与直接使用catch捕捉不同,catch捕捉意味着完全处理该异常,如果catch块中没有重新抛出新的异常,则该方法可能正常结束;而AfterThrowing处理虽然处理了该异常,但它不能完全处理异常,该异常依然会传播到上一级调用者,即JVM。

      • 处理异常的方式
        1. java的处理异常方式

          1. try-catch
          2. throws
        2. spring中的处理异常的方式

          1. 使用@ExceptionHandler
          2. 使用@ControllerAdvice+@ExceptionHandler
          3. 实现HandlerExceptionResolver接口
            @Slf4j
            @Component
            public class GlobalExpetion implements HandlerExceptionResolver {
                @Override
                public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
               log.info("系统发生异常");
                // 统一处理异常
                ModelAndView modelAndView = new ModelAndView();
                modelAndView.addObject("message", "系统发生异常,请稍后重试");
                modelAndView.setViewName("/500");
                return modelAndView;
            }
            }
          复制代码
  • 切面的作用域,优先级和嵌套原则

    • 切面的作用域

      public, protected, default 作用域的方法都可以被拦截

    • 优先级

      可以使用@order指定切面的优先级。@order中的value值默认是int的最大值即2的31次方-1.事务切面优先级:默认为最低优先级

    • 嵌套原则

      a.调用方法不满足拦截规则,调用本类中其他满足拦截条件的方法,这个时候不会启动aop的拦截方法 b.调用方法不满足拦截规则,调用其他类中满足拦截条件的方法,这个时候会启动aop拦截方法 c.调用满足拦截的方法a再调用满足拦截的方法b(分为在本类内和在类外) 在类外的会启动aop的拦截,拦截的顺序是先进入a方法的后置通知然后进入b方法后置,走完b方法的通知之后再进入a方法的后置通知,然后走完a方法的通知 ,如果是在本类内调用的只会执行一次aop

      • 嵌套原则的形成因素

        因为aop拦截的并不是真正的目标类而是注入ioc容器的代理类,但是在java中如果调用方法则是使用this.method()这种形式去调用,此时this所指向的并不是代理类而是类的本身。所以这个时候aop并不能拦截到方法。

      • 解决方法

        1. 自己注入到自己

          @Component
          public class TestAopService {
              @Resource
              private TestAopService testAopService;
              public TestAopService() {
              }
              @TestAnnotation
              public void say1(){
                  testAopService.say2();
                  System.out.println("1");
              }
              @TestAnnotation
              public void say2(){
                  System.out.println("1");
              }
          }
          复制代码
        2. 使用AopContext.currentProxy()

          @Component
          public class TestAopService {
              public TestAopService() {
              }
          
              @TestAnnotation
              public void say1(){
                  ((TestAopService)AopContext.currentProxy()).say2();
                  System.out.println("1");
              }
              @TestAnnotation
              public void say2(){
                  System.out.println("1");
              }
          }
          复制代码

          但是要注意的是用AopContext.currentProxy()必须要搭配注解@EnableAspectJAutoProxy(exposeProxy = true)使用,因为AopContext扫描bean的功能默认是关闭的,必须要手动设置为true才可以

        3. 使用ApplicationContext查找bean

          @Component
          public class TestAopService {
              @Autowired
              private ApplicationContext applicationContext;
              public TestAopService() {
              }
          
              @TestAnnotation
              public void say1(){
                  TestAopService bean = applicationContext.getBean(TestAopService.class);
                  bean.say2();
                  System.out.println("1");
              }
              @TestAnnotation
              public void say2(){
                  System.out.println("1");
              }
          }
          复制代码

猜你喜欢

转载自juejin.im/post/5dd73de6e51d45236665e696
今日推荐