溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

怎么用自定義注解

發(fā)布時間:2021-10-25 10:40:17 來源:億速云 閱讀:129 作者:iii 欄目:編程語言

本篇內(nèi)容介紹了“怎么用自定義注解”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

基本知識

在Java中,注解分為兩種,元注解和自定義注解。

很多人誤以為自定義注解就是開發(fā)者自己定義的,而其它框架提供的不算,但是其實上面我們提到的那幾個注解其實都是自定義注解。

關(guān)于"元"這個描述,在編程世界里面有都很多,比如"元注解"、"元數(shù)據(jù)"、"元類"、"元表"等等,這里的"元"其實都是從meta翻譯過來的。

一般我們把元注解理解為描述注解的注解,元數(shù)據(jù)理解為描述數(shù)據(jù)的數(shù)據(jù),元類理解為描述類的類…

所以,在Java中,除了有限的幾個固定的"描述注解的注解"以外,所有的注解都是自定義注解。

在JDK中提供了4個標(biāo)準(zhǔn)的用來對注解類型進(jìn)行注解的注解類(元注解),他們分別是:

@Target  @Retention  @Documented  @Inherited

除了以上這四個,所有的其他注解全部都是自定義注解。

這里不準(zhǔn)備深入介紹以上四個元注解的作用,大家可以自行學(xué)習(xí)。

本文即將提到的幾個例子,都是作者在日常工作中真實使用到的場景,這例子有一個共同點,那就是都用到了Spring的AOP技術(shù)。

什么是AOP以及他的用法相信很多人都知道,這里也就不展開介紹了。

使用自定義注解做日志記錄

不知道大家有沒有遇到過類似的訴求,就是希望在一個方法的入口處或者出口處做統(tǒng)一的日志處理,比如記錄一下入?yún)?、出參、記錄下方法?zhí)行的時間等。

如果在每一個方法中自己寫這樣的代碼的話,一方面會有很多代碼重復(fù),另外也容易被遺漏。

這種場景,就可以使用自定義注解+切面實現(xiàn)這個功能。

假設(shè)我們想要在一些web請求的方法上,記錄下本次操作具體做了什么事情,比如新增了一條記錄或者刪除了一條記錄等。

首先我們自定義一個注解:

/**   * Operate Log 的自定義注解   */  @Target(ElementType.METHOD)  @Retention(RetentionPolicy.RUNTIME)  public @interface OpLog {      /**      * 業(yè)務(wù)類型,如新增、刪除、修改       * @return       */      public OpType opType();      /**       * 業(yè)務(wù)對象名稱,如訂單、庫存、價格       * @return      */      public String opItem();      /**       * 業(yè)務(wù)對象編號表達(dá)式,描述了如何獲取訂單號的表達(dá)式       * @return       */      public String opItemIdExpression();  }

因為我們不僅要在日志中記錄本次操作了什么,還需要知道被操作的對象的具體的唯一性標(biāo)識,如訂單號信息。

但是每一個接口方法的參數(shù)類型肯定是不一樣的,很難有一個統(tǒng)一的標(biāo)準(zhǔn),那么我們就可以借助Spel表達(dá)式,即在表達(dá)式中指明如何獲取對應(yīng)的對象的唯一性標(biāo)識。

有了上面的注解,接下來就可以寫切面了。主要代碼如下:

/**   * OpLog的切面處理類,用于通過注解獲取日志信息,進(jìn)行日志記錄   * @author Hollis   */ @Aspect  @Component  public class OpLogAspect {      private static final Logger LOGGER = LoggerFactory.getLogger(OpLogAspect.class);      @Autowired      HttpServletRequest request;      @Around("@annotation(com.hollis.annotation.OpLog)")      public Object log(ProceedingJoinPoint pjp) throws Exception {          Method method = ((MethodSignature)pjp.getSignature()).getMethod();          OpLog opLog = method.getAnnotation(OpLog.class);          Object response = null;          try {              // 目標(biāo)方法執(zhí)行              response = pjp.proceed();          } catch (Throwable throwable) {              throw new Exception(throwable);          }           if (StringUtils.isNotEmpty(opLog.opItemIdExpression())) {              SpelExpressionParser parser = new SpelExpressionParser();              Expression expression = parser.parseExpression(opLog.opItemIdExpression());              EvaluationContext context = new StandardEvaluationContext();              // 獲取參數(shù)值              Object[] args = pjp.getArgs();              // 獲取運行時參數(shù)的名稱              LocalVariableTableParameterNameDiscoverer discoverer                  = new LocalVariableTableParameterNameDiscoverer();              String[] parameterNames = discoverer.getParameterNames(method);              // 將參數(shù)綁定到context中              if (parameterNames != null) {                  for (int i = 0; i < parameterNames.length; i++) {                      context.setVariable(parameterNames[i], args[i]);                  }              }              // 將方法的resp當(dāng)做變量放到context中,變量名稱為該類名轉(zhuǎn)化為小寫字母開頭的駝峰形式              if (response != null) {                  context.setVariable(                      CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, response.getClass().getSimpleName()),                      response);              }              // 解析表達(dá)式,獲取結(jié)果              String itemId = String.valueOf(expression.getValue(context));              // 執(zhí)行日志記錄             handle(opLog.opType(), opLog.opItem(), itemId);          }          return response;     }      private void handle(OpType opType,  String opItem, String opItemId) {        // 通過日志打印輸出        LOGGER.info("opType = " + opType.name() +",opItem = " +opItem + ",opItemId = " +opItemId);      }  }

以上切面中,有幾個點需要大家注意的:

  1、使用@Around注解來指定對標(biāo)注了OpLog的方法設(shè)置切面。

  2、使用Spel的相關(guān)方法,通過指定的表示,從對應(yīng)的參數(shù)中獲取到目標(biāo)對象的唯一性標(biāo)識。

  3、再方法執(zhí)行成功后,輸出日志。

有了以上的切面及注解后,我們只需要在對應(yīng)的方法上增加注解標(biāo)注即可,如:

@RequestMapping(method = {RequestMethod.GET, RequestMethod.POST})  @OpLog(opType = OpType.QUERY, opItem = "order", opItemIdExpression = "#id")  public @ResponseBody  HashMap view(@RequestParam(name = "id") String id)      throws Exception {  }

上面這種是入?yún)⒌膮?shù)列表中已經(jīng)有了被操作的對象的唯一性標(biāo)識,直接使用#id指定即可。

如果被操作的對象的唯一性標(biāo)識不在入?yún)⒘斜碇校敲纯赡苁侨雲(yún)⒌膶ο笾械哪骋粋€屬性,用法如下:

@RequestMapping(method = {RequestMethod.GET, RequestMethod.POST})  @OpLog(opType = OpType.QUERY, opItem = "order", opItemIdExpression = "#orderVo.id")  public @ResponseBody  HashMap update(OrderVO orderVo)      throws Exception {  }

以上,即可從入?yún)⒌腛rderVO對象的id屬性的值獲取。

如果我們要記錄的唯一性標(biāo)識,在入?yún)⒅袥]有的話,應(yīng)該怎么辦呢?最典型的就是插入方法,插入成功之前,根本不知道主鍵ID是什么,這種怎么辦呢?

我們上面的切面中,做了一件事情,就是我們把方法的返回值也會使用表達(dá)式進(jìn)行一次解析,如果可以解析得到具體的值,也是可以。如以下寫法:

@RequestMapping(method = {RequestMethod.GET, RequestMethod.POST})  @OpLog(opType = OpType.QUERY, opItem = "order", opItemIdExpression = "#insertResult.id")  public @ResponseBody  InsertResult insert(OrderVO orderVo)      throws Exception {      return orderDao.insert(orderVo);  }

以上,就是一個簡單的使用自定義注解+切面進(jìn)行日志記錄的場景。下面我們再來看一個如何使用注解做方法參數(shù)的校驗。

使用自定義注解做前置檢查

當(dāng)我們對外部提供接口的時候,會對其中的部分參數(shù)有一定的要求,比如某些參數(shù)值不能為空等。大多數(shù)情況下我們都需要自己主動進(jìn)行校驗,判斷對方傳入的值是否合理。

這里推薦一個使用HibernateValidator + 自定義注解 + AOP實現(xiàn)參數(shù)校驗的方式。

首先我們會有一個具體的入?yún)㈩?,定義如下:

public class User {      private String idempotentNo;      @NotNull(          message = "userName can't be null"      )      private String userName;  }

以上,對userName參數(shù)注明不能為null。

然后再使用Hibernate Validator定義一個工具類,用于做參數(shù)校驗。

/**   * 參數(shù)校驗工具   * @author Hollis   */  public class BeanValidator {      private static Validator validator = Validation.byProvider(HibernateValidator.class).configure().failFast(true)          .buildValidatorFactory().getValidator();      /**       * @param object object       * @param groups groups       */      public static void validateObject(Object object, Class<?>... groups) throws ValidationException {          Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);         if (constraintViolations.stream().findFirst().isPresent()) {              throw new ValidationException(constraintViolations.stream().findFirst().get().getMessage());          }      }  }

以上代碼,會對一個bean進(jìn)行校驗,一旦失敗,就會拋出ValidationException。

接下來定義一個注解:

/**   * facade接口注解, 用于統(tǒng)一對facade進(jìn)行參數(shù)校驗及異常捕獲   * <pre>  *      注意,使用該注解需要注意,該方法的返回值必須是BaseResponse的子類   * </pre>   */  @Target(ElementType.METHOD)  @Retention(RetentionPolicy.RUNTIME)  public @interface Facade {  }

這個注解里面沒有任何參數(shù),只用于標(biāo)注那些方法要進(jìn)行參數(shù)校驗。

接下來定義切面:

/**   * Facade的切面處理類,統(tǒng)一統(tǒng)計進(jìn)行參數(shù)校驗及異常捕獲   * @author Hollis   */  @Aspect  @Component  public class FacadeAspect {      private static final Logger LOGGER = LoggerFactory.getLogger(FacadeAspect.class);      @Autowired      HttpServletRequest request;      @Around("@annotation(com.hollis.annotation.Facade)")      public Object facade(ProceedingJoinPoint pjp) throws Exception {          Method method = ((MethodSignature)pjp.getSignature()).getMethod();          Object[] args = pjp.getArgs();          Class returnType = ((MethodSignature)pjp.getSignature()).getMethod().getReturnType();          //循環(huán)遍歷所有參數(shù),進(jìn)行參數(shù)校驗          for (Object parameter : args) {              try {                  BeanValidator.validateObject(parameter);              } catch (ValidationException e) {                  return getFailedResponse(returnType, e);              }          }          try {              // 目標(biāo)方法執(zhí)行              Object response = pjp.proceed();              return response;          } catch (Throwable throwable) {              return getFailedResponse(returnType, throwable);          }      }      /**       * 定義并返回一個通用的失敗響應(yīng)       */      private Object getFailedResponse(Class returnType, Throwable throwable)          throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {          //如果返回值的類型為BaseResponse 的子類,則創(chuàng)建一個通用的失敗響應(yīng)          if (returnType.getDeclaredConstructor().newInstance() instanceof BaseResponse) {              BaseResponse response = (BaseResponse)returnType.getDeclaredConstructor().newInstance();              response.setSuccess(false);              response.setResponseMessage(throwable.toString());              response.setResponseCode(GlobalConstant.BIZ_ERROR);              return response;          }          LOGGER.error(              "failed to getFailedResponse , returnType (" + returnType + ") is not instanceof BaseResponse");          return null;      }  }

以上代碼,和前面的切面有點類似,主要是定義了一個切面,會對所有標(biāo)注@Facade的方法進(jìn)行統(tǒng)一處理,即在開始方法調(diào)用前進(jìn)行參數(shù)校驗,一旦校驗失敗,則返回一個固定的失敗的Response。

特別需要注意的是,這里之所以可以返回一個固定的BaseResponse,是因為我們會要求我們的所有對外提供的接口的response必須繼承BaseResponse類,這個類里面會定義一些默認(rèn)的參數(shù),如錯誤碼等。

之后,只需要對需要參數(shù)校驗的方法增加對應(yīng)注解即可:

@Facade  public TestResponse query(User user) {  }

這樣,有了以上注解和切面,我們就可以對所有的對外方法做統(tǒng)一的控制了。

其實,以上這個facadeAspect我省略了很多東西,我們真正使用的那個切面,不僅僅做了參數(shù)檢查,還可以做很多其他事情。比如異常的統(tǒng)一處理、錯誤碼的統(tǒng)一轉(zhuǎn)換、記錄方法執(zhí)行時長、記錄方法的入?yún)⒊鰠⒌鹊取?/p>

總之,使用切面+自定義注解,我們可以統(tǒng)一做很多事情。除了以上的這幾個場景,我們還有很多相似的用法,比如:

統(tǒng)一的緩存處理。如某些操作需要在操作前查緩存、操作后更新緩存。這種就可以通過自定義注解+切面的方式統(tǒng)一處理。

代碼其實都差不多,思路也比較簡單,就是通過自定義注解來標(biāo)注需要被切面處理的累或者方法,然后在切面中對方法的執(zhí)行過程進(jìn)行干預(yù),比如在執(zhí)行前或者執(zhí)行后做一些特殊的操作。

使用這種方式可以大大減少重復(fù)代碼,大大提升代碼的優(yōu)雅性,方便我們使用。

“怎么用自定義注解”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI