您好,登錄后才能下訂單哦!
本篇內容介紹了“怎么用自定義注解”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
基本知識
在Java中,注解分為兩種,元注解和自定義注解。
很多人誤以為自定義注解就是開發者自己定義的,而其它框架提供的不算,但是其實上面我們提到的那幾個注解其實都是自定義注解。
關于"元"這個描述,在編程世界里面有都很多,比如"元注解"、"元數據"、"元類"、"元表"等等,這里的"元"其實都是從meta翻譯過來的。
一般我們把元注解理解為描述注解的注解,元數據理解為描述數據的數據,元類理解為描述類的類…
所以,在Java中,除了有限的幾個固定的"描述注解的注解"以外,所有的注解都是自定義注解。
在JDK中提供了4個標準的用來對注解類型進行注解的注解類(元注解),他們分別是:
@Target @Retention @Documented @Inherited
除了以上這四個,所有的其他注解全部都是自定義注解。
這里不準備深入介紹以上四個元注解的作用,大家可以自行學習。
本文即將提到的幾個例子,都是作者在日常工作中真實使用到的場景,這例子有一個共同點,那就是都用到了Spring的AOP技術。
什么是AOP以及他的用法相信很多人都知道,這里也就不展開介紹了。
使用自定義注解做日志記錄
不知道大家有沒有遇到過類似的訴求,就是希望在一個方法的入口處或者出口處做統一的日志處理,比如記錄一下入參、出參、記錄下方法執行的時間等。
如果在每一個方法中自己寫這樣的代碼的話,一方面會有很多代碼重復,另外也容易被遺漏。
這種場景,就可以使用自定義注解+切面實現這個功能。
假設我們想要在一些web請求的方法上,記錄下本次操作具體做了什么事情,比如新增了一條記錄或者刪除了一條記錄等。
首先我們自定義一個注解:
/** * Operate Log 的自定義注解 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface OpLog { /** * 業務類型,如新增、刪除、修改 * @return */ public OpType opType(); /** * 業務對象名稱,如訂單、庫存、價格 * @return */ public String opItem(); /** * 業務對象編號表達式,描述了如何獲取訂單號的表達式 * @return */ public String opItemIdExpression(); }
因為我們不僅要在日志中記錄本次操作了什么,還需要知道被操作的對象的具體的唯一性標識,如訂單號信息。
但是每一個接口方法的參數類型肯定是不一樣的,很難有一個統一的標準,那么我們就可以借助Spel表達式,即在表達式中指明如何獲取對應的對象的唯一性標識。
有了上面的注解,接下來就可以寫切面了。主要代碼如下:
/** * OpLog的切面處理類,用于通過注解獲取日志信息,進行日志記錄 * @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 { // 目標方法執行 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(); // 獲取參數值 Object[] args = pjp.getArgs(); // 獲取運行時參數的名稱 LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer(); String[] parameterNames = discoverer.getParameterNames(method); // 將參數綁定到context中 if (parameterNames != null) { for (int i = 0; i < parameterNames.length; i++) { context.setVariable(parameterNames[i], args[i]); } } // 將方法的resp當做變量放到context中,變量名稱為該類名轉化為小寫字母開頭的駝峰形式 if (response != null) { context.setVariable( CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, response.getClass().getSimpleName()), response); } // 解析表達式,獲取結果 String itemId = String.valueOf(expression.getValue(context)); // 執行日志記錄 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注解來指定對標注了OpLog的方法設置切面。
2、使用Spel的相關方法,通過指定的表示,從對應的參數中獲取到目標對象的唯一性標識。
3、再方法執行成功后,輸出日志。
有了以上的切面及注解后,我們只需要在對應的方法上增加注解標注即可,如:
@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 { }
上面這種是入參的參數列表中已經有了被操作的對象的唯一性標識,直接使用#id指定即可。
如果被操作的對象的唯一性標識不在入參列表中,那么可能是入參的對象中的某一個屬性,用法如下:
@RequestMapping(method = {RequestMethod.GET, RequestMethod.POST}) @OpLog(opType = OpType.QUERY, opItem = "order", opItemIdExpression = "#orderVo.id") public @ResponseBody HashMap update(OrderVO orderVo) throws Exception { }
以上,即可從入參的OrderVO對象的id屬性的值獲取。
如果我們要記錄的唯一性標識,在入參中沒有的話,應該怎么辦呢?最典型的就是插入方法,插入成功之前,根本不知道主鍵ID是什么,這種怎么辦呢?
我們上面的切面中,做了一件事情,就是我們把方法的返回值也會使用表達式進行一次解析,如果可以解析得到具體的值,也是可以。如以下寫法:
@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); }
以上,就是一個簡單的使用自定義注解+切面進行日志記錄的場景。下面我們再來看一個如何使用注解做方法參數的校驗。
使用自定義注解做前置檢查
當我們對外部提供接口的時候,會對其中的部分參數有一定的要求,比如某些參數值不能為空等。大多數情況下我們都需要自己主動進行校驗,判斷對方傳入的值是否合理。
這里推薦一個使用HibernateValidator + 自定義注解 + AOP實現參數校驗的方式。
首先我們會有一個具體的入參類,定義如下:
public class User { private String idempotentNo; @NotNull( message = "userName can't be null" ) private String userName; }
以上,對userName參數注明不能為null。
然后再使用Hibernate Validator定義一個工具類,用于做參數校驗。
/** * 參數校驗工具 * @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進行校驗,一旦失敗,就會拋出ValidationException。
接下來定義一個注解:
/** * facade接口注解, 用于統一對facade進行參數校驗及異常捕獲 * <pre> * 注意,使用該注解需要注意,該方法的返回值必須是BaseResponse的子類 * </pre> */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Facade { }
這個注解里面沒有任何參數,只用于標注那些方法要進行參數校驗。
接下來定義切面:
/** * Facade的切面處理類,統一統計進行參數校驗及異常捕獲 * @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(); //循環遍歷所有參數,進行參數校驗 for (Object parameter : args) { try { BeanValidator.validateObject(parameter); } catch (ValidationException e) { return getFailedResponse(returnType, e); } } try { // 目標方法執行 Object response = pjp.proceed(); return response; } catch (Throwable throwable) { return getFailedResponse(returnType, throwable); } } /** * 定義并返回一個通用的失敗響應 */ private Object getFailedResponse(Class returnType, Throwable throwable) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //如果返回值的類型為BaseResponse 的子類,則創建一個通用的失敗響應 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; } }
以上代碼,和前面的切面有點類似,主要是定義了一個切面,會對所有標注@Facade的方法進行統一處理,即在開始方法調用前進行參數校驗,一旦校驗失敗,則返回一個固定的失敗的Response。
特別需要注意的是,這里之所以可以返回一個固定的BaseResponse,是因為我們會要求我們的所有對外提供的接口的response必須繼承BaseResponse類,這個類里面會定義一些默認的參數,如錯誤碼等。
之后,只需要對需要參數校驗的方法增加對應注解即可:
@Facade public TestResponse query(User user) { }
這樣,有了以上注解和切面,我們就可以對所有的對外方法做統一的控制了。
其實,以上這個facadeAspect我省略了很多東西,我們真正使用的那個切面,不僅僅做了參數檢查,還可以做很多其他事情。比如異常的統一處理、錯誤碼的統一轉換、記錄方法執行時長、記錄方法的入參出參等等。
總之,使用切面+自定義注解,我們可以統一做很多事情。除了以上的這幾個場景,我們還有很多相似的用法,比如:
統一的緩存處理。如某些操作需要在操作前查緩存、操作后更新緩存。這種就可以通過自定義注解+切面的方式統一處理。
代碼其實都差不多,思路也比較簡單,就是通過自定義注解來標注需要被切面處理的累或者方法,然后在切面中對方法的執行過程進行干預,比如在執行前或者執行后做一些特殊的操作。
使用這種方式可以大大減少重復代碼,大大提升代碼的優雅性,方便我們使用。
“怎么用自定義注解”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。