springboot中的AOP

只想搞技術的菜雞發表於2019-11-22
  • 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中的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");
              }
          }
          複製程式碼

相關文章