Java注解的原理,来了解一下

2020/11/26

1、Java元注解

随意点开一个spring的注解源码,都会看到几个常用的元注解

之前老是看不懂,今天就来弄懂它。

  • 注解(Annotation)是一种可以放在 Java 类上,方法上,属性上,参数前面的一种特殊的注释。

  • 元注解是用来注释注解的注解,元注解我们平常不会编写,只需要添加到我们自己编写的注解上即可。

Java 自带的常用的元注解有@Target@Retention@Documented@Inherited 分别有如下含义

  1. @Target:标记这个注解使用的地方,取值范围是一个枚举 java.lang.annotation.ElementType

  2. @Retention :标识这个注解的生命周期,取值范围在枚举 java.lang.annotation.RetentionPolicy

    一般定义的注解都是在运行时使用,所有要用

    @Retention(RetentionPolicy.RUNTIME)
    
  3. @Documented:表示注解是否包含到文档中。

  4. @Inherited :使用@Inherited 定义子类是否可继承父类定义的注解Annotation@Inherited仅针对@Target(ElementType.TYPE)类型的annotation有效,并且仅针对class的继承,对interface的继承无效

2、自定义注解

上面介绍了几个元注解,下面我们定义一个日志注解来演示一下,我们通过定义一个名为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;
}

说明

  1. 上面的注解,我们增加了@Target({ElementType.METHOD}) , @Retention(RetentionPolicy.RUNTIME), @Documented 三个元注解,表示我们这个注解是使用在方法上的,并且生命周期是运行时,而且可以记录到文档中。
  2. 然后我们可以看到定义注解采用的是@interface 关键字,并且我们给这个注解定义了几个属性,同时设置了默认值。
  3. 平时我们编写的注解一般必须设置@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.*;

@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")

Post Directory