天天都在使用的 Java 注解,你真的了解它嗎?
本文轉載自微信公眾號「Java極客技術」,作者鴨血粉絲 。轉載本文請聯系Java極客技術公眾號。
Hello,大家好,我是阿粉,Java 的注解相信大家天天都在用,但是關于注解的原理,大家都了解嗎?這篇文章通過意見簡單的示例給大家演示一下注解的使用和原理。
Java 元注解
注解(Annotation)是一種可以放在 Java 類上,方法上,屬性上,參數前面的一種特殊的注釋,用來注釋注解的注解叫做元注解。元注解我們平常不會編寫,只需要添加到我們自己編寫的注解上即可,。
Java 自帶的常用的元注解有@Target,@Retention,@Documented,@Inherited 分別有如下含義
- @Target:標記這個注解使用的地方,取值范圍在枚舉 java.lang.annotation.ElementType:TYPE,FIELD,METHOD,PARAMETER,CONSTRUCTOR,LOCAL_VARIABLE,ANNOTATION_TYPE,PACKAGE,TYPE_PARAMETER,TYPE_USE。
- @Retention :標識這個注解的生命周期,取值范圍在枚舉 java.lang.annotation.RetentionPolicy,SOURCE,CLASS,RUNTIME,一般定義的注解都是在運行時使用,所有要用 @Retention(RetentionPolicy.RUNTIME);
- @Documented:表示注解是否包含到文檔中。
- @Inherited :使用@Inherited定義子類是否可繼承父類定義的Annotation。@Inherited僅針對@Target(ElementType.TYPE)類型的annotation有效,并且僅針對class的繼承,對interface的繼承無效。
定義注解
上面介紹了幾個元注解,下面我們定義一個日志注解來演示一下,我們通過定義一個名為OperationLog 的注解來記錄一些通用的操作日志,比如記錄什么時候什么人查詢的哪個表的數據或者新增了什么數據。編寫注解我們用的是 @interface 關鍵字,相關代碼如下:
- package com.api.annotation;
- import java.lang.annotation.*;
- /**
- * <br>
- * <b>Function:</b><br>
- * <b>Author:</b>@author 子悠<br>
- * <b>Date:</b>2020-11-17 22:10<br>
- * <b>Desc:</b>用于記錄操作日志<br>
- */
- @Target({ElementType.METHOD})
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- public @interface OperationLog {
- /**
- * 操作類型
- *
- * @return
- */
- String type() default OperationType.SELECT;
- /**
- * 操作說明
- *
- * @return
- */
- String desc() default "";
- /**
- * 請求路徑
- *
- * @return
- */
- String path() default "";
- /**
- * 是否記錄日志,默認是
- *
- * @return
- */
- boolean write() default true;
- /**
- * 是否需要登錄信息
- *
- * @return
- */
- boolean auth() default true;
- /**
- * 當 type 為 save 時必須
- *
- * @return
- */
- String primaryKey() default "";
- /**
- * 對應 service 的 Class
- *
- * @return
- */
- Class<?> defaultServiceClass() default Object.class;
- }
說明
上面的注解,我們增加了@Target({ElementType.METHOD}) , @Retention(RetentionPolicy.RUNTIME), @Documented 三個元注解,表示我們這個注解是使用在方法上的,并且生命周期是運行時,而且可以記錄到文檔中。然后我們可以看到定義注解采用的u是@interface 關鍵字,并且我們給這個注解定義了幾個屬性,同時設置了默認值。主要注意的是平時我們編寫的注解一般必須設置@Target和@Retention,而且 @Retention一般設置為RUNTIME,這是因為我們自定義的注解通常要求在運行期讀取,另外一般情況下,不必寫@Inherited。
使用
上面的動作只是把注解定義出來了,但是光光定義出來是沒有用的,必須有一個地方讀取解析,才能提現出注解的價值,我們就采用 Spring 的 AOP 攔截這個注解,將所有攜帶這個注解的方法所進行的操作都記錄下來。
- package com.api.config;
- import lombok.extern.slf4j.Slf4j;
- import org.aspectj.lang.ProceedingJoinPoint;
- import org.aspectj.lang.annotation.Around;
- import org.aspectj.lang.annotation.Aspect;
- import org.aspectj.lang.annotation.Pointcut;
- import org.aspectj.lang.reflect.MethodSignature;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.core.annotation.Order;
- import org.springframework.stereotype.Component;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.PostMapping;
- import org.springframework.web.bind.annotation.RequestMapping;
- import javax.servlet.http.HttpServletRequest;
- import java.lang.reflect.Field;
- import java.lang.reflect.Method;
- import java.util.*;
- /**
- * <br>
- * <b>Function:</b><br>
- * <b>Author:</b>@author 子悠<br>
- * <b>Date:</b>2020-11-17 14:40<br>
- * <b>Desc:</b>aspect for operation log<br>
- */
- @Aspect
- @Component
- @Order(-5)
- @Slf4j
- public class LogAspect {
- /**
- * Pointcut for methods which need to record operate log
- */
- @Pointcut("within(com.xx.yy.controller..*) && @annotation(com.api.annotation.OperationLog)")
- public void logAspect() {
- }
- /**
- * record log for Admin and DSP
- *
- * @param joinPoint parameter
- * @return result
- * @throws Throwable
- */
- @Around("logAspect()")
- public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
- Object proceed = null;
- String classType = joinPoint.getTarget().getClass().getName();
- Class<?> targetCls = Class.forName(classType);
- MethodSignature ms = (MethodSignature) joinPoint.getSignature();
- Method targetMethod = targetCls.getDeclaredMethod(ms.getName(), ms.getParameterTypes());
- OperationLog operation = targetMethod.getAnnotation(OperationLog.class);
- if (null != operation && operation.write()) {
- SysMenuOpLogEntity opLogEntity = new SysMenuOpLogEntity();
- StringBuilder change = new StringBuilder();
- if (StrUtil.isNotBlank(operation.type())) {
- switch (operation.type()) {
- case OperationType.ADD:
- proceed = joinPoint.proceed();
- String addString = genAddData(targetCls, operation.defaultServiceClass(), joinPoint.getArgs());
- opLogEntity.setAfterJson(addString);
- change.append(OperationType.ADD);
- break;
- case OperationType.DELETE:
- String deleteString = autoQueryDeletedData(targetCls, operation.primaryKey(), operation.defaultServiceClass(), joinPoint.getArgs());
- opLogEntity.setBeforeJson(deleteString);
- change.append(OperationType.DELETE);
- proceed = joinPoint.proceed();
- break;
- case OperationType.EDIT:
- change.append(OperationType.EDIT);
- setOpLogEntity(opLogEntity, targetCls, operation.primaryKey(), operation.defaultServiceClass(), joinPoint.getArgs());
- proceed = joinPoint.proceed();
- break;
- case OperationType.SELECT:
- opLogEntity.setBeforeJson(getQueryString(targetCls, operation.defaultServiceClass(), joinPoint.getArgs()));
- change.append(operation.type());
- proceed = joinPoint.proceed();
- break;
- case OperationType.SAVE:
- savedDataOpLog(opLogEntity, targetCls, operation.primaryKey(), operation.defaultServiceClass(), joinPoint.getArgs());
- change.append(operation.type());
- proceed = joinPoint.proceed();
- break;
- case OperationType.EXPORT:
- case OperationType.DOWNLOAD:
- change.append(operation.type());
- proceed = joinPoint.proceed();
- break;
- default:
- }
- opLogEntity.setExecType(operation.type());
- }
- StringBuilder changing = new StringBuilder();
- if (StrUtil.isNotBlank(opLogEntity.getExecType())) {
- if (operation.auth()) {
- LoginUserVO loginUser = getLoginUser();
- if (null != loginUser) {
- opLogEntity.setUserId(loginUser.getUserId());
- opLogEntity.setUserName(loginUser.getUserName());
- changing.append(loginUser.getUserName()).append("-");
- } else {
- log.error("用戶未登錄");
- }
- }
- opLogEntity.setCreateTime(DateUtils.getCurDate());
- opLogEntity.setRemark(getOperateMenuName(targetMethod, operation.desc()));
- opLogEntity.setPath(getPath(targetMethod, targetMethod.getName()));
- opLogEntity.setChanging(changing.append(change).toString());
- menuOpLogService.save(opLogEntity);
- }
- }
- return proceed;
- }
- /**
- * query data by userId
- *
- * @param targetCls class
- * @param defaultServiceClass default service class
- * @return
- * @throws Exception
- */
- private String queryByCurrentUserId(Class<?> targetCls, Class<?> defaultServiceClass) throws Exception {
- BaseService baseService = getBaseService(targetCls, defaultServiceClass);
- LoginUserVO loginUser = dspBaseService.getLoginUser();
- if (null != loginUser) {
- Object o = baseService.queryId(loginUser.getUserId());
- return JsonUtils.obj2Json(o);
- }
- return null;
- }
- /**
- * return query parameter
- *
- * @param targetCls class
- * @param args parameter
- * @param defaultServiceClass default service class
- * @return
- * @throws Exception
- */
- private String getQueryString(Class<?> targetCls, Class<?> defaultServiceClass, Object[] args) {
- if (args.length > 0) {
- Class<?> entityClz = getEntityClz(targetCls, defaultServiceClass);
- for (Object arg : args) {
- if (arg.getClass().equals(entityClz) || arg instanceof BaseModel) {
- return JsonUtils.obj2Json(arg);
- }
- }
- }
- return null;
- }
- /**
- * save record log while OperatorType is SAVE
- *
- * @param opLogEntity entity
- * @param targetCls class
- * @param primaryKey primaryKey
- * @param defaultServiceClass default service class
- * @param args parameter
- * @throws Exception
- */
- private void savedDataOpLog(SysMenuOpLogEntity opLogEntity, Class<?> targetCls, String primaryKey, Class<?> defaultServiceClass, Object[] args) throws Exception {
- Class<?> entityClz = getEntityClz(targetCls, defaultServiceClass);
- BaseService baseService = getBaseService(targetCls, defaultServiceClass);
- for (Object arg : args) {
- if (arg.getClass().equals(entityClz)) {
- if (StrUtil.isNotBlank(primaryKey)) {
- Field declaredField = entityClz.getDeclaredField(primaryKey);
- declaredField.setAccessible(true);
- Object primaryKeyValue = declaredField.get(arg);
- //if primary key is not null that means edit, otherwise is add
- if (null != primaryKeyValue) {
- //query data by primary key
- Object o = baseService.queryId(primaryKeyValue);
- opLogEntity.setBeforeJson(JsonUtils.obj2Json(o));
- }
- }
- opLogEntity.setAfterJson(JsonUtils.obj2Json(arg));
- }
- }
- }
- /**
- * set parameter which edit data
- *
- * @param opLogEntity entity
- * @param targetCls class
- * @param primaryKey primaryKey
- * @param defaultServiceClass default service class
- * @param args parameter
- * @throws Exception
- */
- private void setOpLogEntity(SysMenuOpLogEntity opLogEntity, Class<?> targetCls, String primaryKey, Class<?> defaultServiceClass, Object[] args) throws Exception {
- Map<String, String> saveMap = autoQueryEditedData(targetCls, primaryKey, defaultServiceClass, args);
- if (null != saveMap) {
- if (saveMap.containsKey(ASPECT_LOG_OLD_DATA)) {
- opLogEntity.setBeforeJson(saveMap.get(ASPECT_LOG_OLD_DATA));
- }
- if (saveMap.containsKey(ASPECT_LOG_NEW_DATA)) {
- opLogEntity.setBeforeJson(saveMap.get(ASPECT_LOG_NEW_DATA));
- }
- }
- }
- /**
- * query data for edit and after edit operate
- *
- * @param targetCls class
- * @param primaryKey primaryKey
- * @param defaultServiceClass default service class
- * @param args parameter
- * @return map which data
- * @throws Exception
- */
- private Map<String, String> autoQueryEditedData(Class<?> targetCls, String primaryKey, Class<?> defaultServiceClass, Object[] args) throws Exception {
- if (StrUtil.isBlank(primaryKey)) {
- throw new Exception();
- }
- Map<String, String> map = new HashMap<>(16);
- Class<?> entityClz = getEntityClz(targetCls, defaultServiceClass);
- BaseService baseService = getBaseService(targetCls, defaultServiceClass);
- for (Object arg : args) {
- if (arg.getClass().equals(entityClz)) {
- Field declaredField = entityClz.getDeclaredField(primaryKey);
- declaredField.setAccessible(true);
- Object primaryKeyValue = declaredField.get(arg);
- //query the data before edit
- if (null != primaryKeyValue) {
- //query data by primary key
- Object o = baseService.queryId(primaryKeyValue);
- map.put(ASPECT_LOG_OLD_DATA, JsonUtils.obj2Json(o));
- map.put(ASPECT_LOG_NEW_DATA, JsonUtils.obj2Json(arg));
- return map;
- }
- }
- }
- return null;
- }
- /**
- * return JSON data which add operate
- *
- * @param targetCls class
- * @param args parameter
- * @param defaultServiceClass default service class
- * @return add data which will be added
- * @throws Exception
- */
- private String genAddData(Class<?> targetCls, Class<?> defaultServiceClass, Object[] args) throws Exception {
- List<Object> parameter = new ArrayList<>();
- for (Object arg : args) {
- if (arg instanceof HttpServletRequest) {
- } else {
- parameter.add(arg);
- }
- }
- return JsonUtils.obj2Json(parameter);
- }
- /**
- * query delete data before delete operate
- *
- * @param targetCls class
- * @param primaryKey primaryKey
- * @param defaultServiceClass default service class
- * @param ids ids
- * @return delete data which will be deleted
- * @throws Throwable
- */
- private String autoQueryDeletedData(Class<?> targetCls, String primaryKey, Class<?> defaultServiceClass, Object[] ids) throws Throwable {
- if (StrUtil.isBlank(primaryKey)) {
- throw new OriginException(TipEnum.LOG_ASPECT_PRIMARY_KEY_NOT_EXIST);
- }
- //get service
- BaseService baseService = getBaseService(targetCls, defaultServiceClass);
- //get entity
- Class<?> entityClz = getEntityClz(targetCls, defaultServiceClass);
- //query deleted data by primary key
- Query query = new Query();
- WhereOperator whereOperator = new WhereOperator(entityClz);
- Set<Object> set = new HashSet<>(Arrays.asList((Object[]) ids[0]));
- whereOperator.and(primaryKey).in(set.toArray());
- query.addWhereOperator(whereOperator);
- List list = baseService.queryList(query);
- return JsonUtils.obj2Json(list);
- }
- /**
- * return service by targetCls
- *
- * @param targetCls current controller class
- * @param defaultServiceClass default service class
- * @return service instance
- * @throws Exception
- */
- private BaseService getBaseService(Class<?> targetCls, Class<?> defaultServiceClass) throws Exception {
- //根據類名拿到對應的 service 名稱
- String serviceName = getServiceName(targetCls, defaultServiceClass);
- BaseService baseService;
- if (null != defaultServiceClass) {
- baseService = (BaseService) ApplicationContextProvider.getBean(serviceName, defaultServiceClass);
- } else {
- Class<?> type = targetCls.getDeclaredField(serviceName).getType();
- baseService = (BaseService) ApplicationContextProvider.getBean(serviceName, type);
- }
- return baseService;
- }
- /**
- * return service name
- *
- * @param targetCls current controller class
- * @param defaultServiceClass default service class
- * @return service name
- */
- private String getServiceName(Class<?> targetCls, Class<?> defaultServiceClass) {
- if (null != defaultServiceClass && Object.class != defaultServiceClass) {
- return StrUtil.left(defaultServiceClass.getSimpleName(), 1).toLowerCase() + defaultServiceClass.getSimpleName().substring(1);
- }
- return StrUtil.left(targetCls.getSimpleName(), 1).toLowerCase() + targetCls.getSimpleName().substring(1).replace("Controller", "Service");
- }
- /**
- * return entity class
- *
- * @param targetCls current controller class
- * @param defaultServiceClass default service class
- * @return entity class
- * @throws Exception
- */
- private Class<?> getEntityClz(Class<?> targetCls, Class<?> defaultServiceClass) {
- try {
- Class<?> type;
- if (null != defaultServiceClass && Object.class != defaultServiceClass) {
- type = defaultServiceClass;
- } else {
- type = targetCls.getDeclaredField(getServiceName(targetCls, null)).getType();
- }
- String entityName = type.getName().replace("service", "entity").replace("Service", "Entity");
- Class<?> entityClz = Class.forName(entityName);
- return entityClz;
- } catch (Exception e) {
- log.error("獲取 class 失敗");
- }
- return null;
- }
- /**
- * require path
- *
- * @param targetMethod target method
- * @param defaultPath default require path
- * @return require path
- */
- private String getPath(Method targetMethod, String defaultPath) {
- String path = defaultPath;
- PostMapping postMapping = targetMethod.getAnnotation(PostMapping.class);
- GetMapping getMapping = targetMethod.getAnnotation(GetMapping.class);
- RequestMapping requestMapping = targetMethod.getAnnotation(RequestMapping.class);
- if (null != postMapping) {
- path = postMapping.value()[0];
- } else if (null != getMapping) {
- path = getMapping.value()[0];
- } else if (null != requestMapping) {
- path = requestMapping.value()[0];
- }
- return path;
- }
- }
上面的代碼中我們定義了一個切面指定需要攔截的包名和注解,因為涉及到很多業務相關的代碼,所以不能完整的提供出來,但是整個思路就是這樣的,在每種操作類型前后將需要記錄的數據查詢出來進行記錄。代碼很長主要是用來獲取相應的參數值的,大家使用的時候可以根據自己的需要進行取舍。比如在新增操作的時候,我們將新增的數據進行記錄下來;編輯的時候將編輯前的數據查詢出來和編輯后的數據一起保存起來,刪除也是一樣的,在刪除前將數據查詢出來保存到日志表中。
同樣導出和下載都會記錄相應信息,整個操作類型的代碼如下:
- package com.api.annotation;
- /**
- * <br>
- * <b>Function:</b><br>
- * <b>Author:</b>@author 子悠<br>
- * <b>Date:</b>2020-11-17 22:11<br>
- * <b>Desc:</b>無<br>
- */
- public interface OperationType {
- /**
- * 新增
- **/
- String ADD = "add";
- /**
- * 刪除
- **/
- String DELETE = "delete";
- /**
- * 使用實體參數修改
- **/
- String EDIT = "edit";
- /**
- * 查詢
- **/
- String SELECT = "select";
- /**
- * 新增和修改的保存方法,使用此類型時必須配置主鍵字段名稱
- **/
- String SAVE = "save";
- /**
- * 導出
- **/
- String EXPORT = "export";
- /**
- * 下載
- **/
- String DOWNLOAD = "download";
- }
后續在使用的時候只需要在需要的方法上加上注解,填上相應的參數即可@OperationLog(desc = "查詢單條記錄", path = "/data")
總結
注解一個我們天天再用的東西,雖然不難,但是我們卻很少自己去寫注解的代碼,通過這篇文章能給大家展示一下注解的使用邏輯,希望對大家有幫助。Spring 中的各種注解本質上也是這種邏輯都需要定義使用和解析。很多時候我們可以通過自定義注解去解決很多場景,比如日志,緩存等。