本文轉(zhuǎn)載自微信公眾號「Java極客技術(shù)」,作者鴨血粉絲 。轉(zhuǎn)載本文請聯(lián)系Java極客技術(shù)公眾號。
Hello,大家好,我是阿粉,Java 的注解相信大家天天都在用,但是關(guān)于注解的原理,大家都了解嗎?這篇文章通過意見簡單的示例給大家演示一下注解的使用和原理。
Java 元注解
注解(Annotation)是一種可以放在 Java 類上,方法上,屬性上,參數(shù)前面的一種特殊的注釋,用來注釋注解的注解叫做元注解。元注解我們平常不會(huì)編寫,只需要添加到我們自己編寫的注解上即可,。
Java 自帶的常用的元注解有@Target,@Retention,@Documented,@Inherited 分別有如下含義
@Target:標(biāo)記這個(gè)注解使用的地方,取值范圍在枚舉 java.lang.annotation.ElementType:TYPE,FIELD,METHOD,PARAMETER,CONSTRUCTOR,LOCAL_VARIABLE,ANNOTATION_TYPE,PACKAGE,TYPE_PARAMETER,TYPE_USE。
@Retention :標(biāo)識這個(gè)注解的生命周期,取值范圍在枚舉 java.lang.annotation.RetentionPolicy,SOURCE,CLASS,RUNTIME,一般定義的注解都是在運(yùn)行時(shí)使用,所有要用 @Retention(RetentionPolicy.RUNTIME);
@Documented:表示注解是否包含到文檔中。
@Inherited :使用@Inherited定義子類是否可繼承父類定義的Annotation。@Inherited僅針對@Target(ElementType.TYPE)類型的annotation有效,并且僅針對class的繼承,對interface的繼承無效。
定義注解
上面介紹了幾個(gè)元注解,下面我們定義一個(gè)日志注解來演示一下,我們通過定義一個(gè)名為OperationLog 的注解來記錄一些通用的操作日志,比如記錄什么時(shí)候什么人查詢的哪個(gè)表的數(shù)據(jù)或者新增了什么數(shù)據(jù)。編寫注解我們用的是 @interface 關(guān)鍵字,相關(guān)代碼如下:
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 "";
/**
* 是否記錄日志,默認(rèn)是
*
* @return
*/
boolean write() default true;
/**
* 是否需要登錄信息
*
* @return
*/
boolean auth() default true;
/**
* 當(dāng) type 為 save 時(shí)必須
*
* @return
*/
String primaryKey() default "";
/**
* 對應(yīng) service 的 Class
*
* @return
*/
Class<?> defaultServiceClass() default Object.class;
}
說明
上面的注解,我們增加了@Target({ElementType.METHOD}) , @Retention(RetentionPolicy.RUNTIME), @Documented 三個(gè)元注解,表示我們這個(gè)注解是使用在方法上的,并且生命周期是運(yùn)行時(shí),而且可以記錄到文檔中。然后我們可以看到定義注解采用的u是@interface 關(guān)鍵字,并且我們給這個(gè)注解定義了幾個(gè)屬性,同時(shí)設(shè)置了默認(rèn)值。主要注意的是平時(shí)我們編寫的注解一般必須設(shè)置@Target和@Retention,而且 @Retention一般設(shè)置為RUNTIME,這是因?yàn)槲覀冏远x的注解通常要求在運(yùn)行期讀取,另外一般情況下,不必寫@Inherited。
使用
上面的動(dòng)作只是把注解定義出來了,但是光光定義出來是沒有用的,必須有一個(gè)地方讀取解析,才能提現(xiàn)出注解的價(jià)值,我們就采用 Spring 的 AOP 攔截這個(gè)注解,將所有攜帶這個(gè)注解的方法所進(jìn)行的操作都記錄下來。
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 {
//根據(jù)類名拿到對應(yīng)的 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;
}
}
上面的代碼中我們定義了一個(gè)切面指定需要攔截的包名和注解,因?yàn)樯婕暗胶芏鄻I(yè)務(wù)相關(guān)的代碼,所以不能完整的提供出來,但是整個(gè)思路就是這樣的,在每種操作類型前后將需要記錄的數(shù)據(jù)查詢出來進(jìn)行記錄。代碼很長主要是用來獲取相應(yīng)的參數(shù)值的,大家使用的時(shí)候可以根據(jù)自己的需要進(jìn)行取舍。比如在新增操作的時(shí)候,我們將新增的數(shù)據(jù)進(jìn)行記錄下來;編輯的時(shí)候?qū)⒕庉嬊暗臄?shù)據(jù)查詢出來和編輯后的數(shù)據(jù)一起保存起來,刪除也是一樣的,在刪除前將數(shù)據(jù)查詢出來保存到日志表中。
同樣導(dǎo)出和下載都會(huì)記錄相應(yīng)信息,整個(gè)操作類型的代碼如下:
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";
/**
* 使用實(shí)體參數(shù)修改
**/
String EDIT = "edit";
/**
* 查詢
**/
String SELECT = "select";
/**
* 新增和修改的保存方法,使用此類型時(shí)必須配置主鍵字段名稱
**/
String SAVE = "save";
/**
* 導(dǎo)出
**/
String EXPORT = "export";
/**
* 下載
**/
String DOWNLOAD = "download";
}
后續(xù)在使用的時(shí)候只需要在需要的方法上加上注解,填上相應(yīng)的參數(shù)即可@OperationLog(desc = "查詢單條記錄", path = "/data")
總結(jié)
注解一個(gè)我們天天再用的東西,雖然不難,但是我們卻很少自己去寫注解的代碼,通過這篇文章能給大家展示一下注解的使用邏輯,希望對大家有幫助。Spring 中的各種注解本質(zhì)上也是這種邏輯都需要定義使用和解析。很多時(shí)候我們可以通過自定義注解去解決很多場景,比如日志,緩存等。