詳解Java中自定義注解的使用
什么是注解
在早期的工作的時(shí)候 ,自定義注解寫的比較多,可大多都只是因?yàn)?這樣看起來 不會(huì)存在一堆代碼耦合在一起的情況,所以使用了自定義注解,這樣看起來清晰些,
Annontation是Java5開始引入的新特征,中文名稱叫注解。
它提供了一種安全的類似注釋的機(jī)制,用來將任何的信息或元數(shù)據(jù)(metadata)與程序元素(類、方法、成員變量等)進(jìn)行關(guān)聯(lián)。為程序的元素(類、方法、成員變量)加上更直觀、更明了的說明,這些說明信息是與程序的業(yè)務(wù)邏輯無關(guān),并且供指定的工具或框架使用。Annontation像一種修飾符一樣,應(yīng)用于包、類型、構(gòu)造方法、方法、成員變量、參數(shù)及本地變量的聲明語句中。
Java注解是附加在代碼中的一些元信息,用于一些工具在編譯、運(yùn)行時(shí)進(jìn)行解析和使用,起到說明、配置的功能。注解不會(huì)也不能影響代碼的實(shí)際邏輯,僅僅起到輔助性的作用。
一般我們自定義一個(gè)注解的操作是這樣的:
public @interface MyAnnotation { }
如果說我們需要給他加上參數(shù),那么大概是這樣的
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @Documented public @interface MyAnnotation { public int age() default 18; String name() ; String [] books(); }
我們可以關(guān)注到上面有些我們不曾見過的注解,而這類注解,統(tǒng)稱為元注解 ,我們可以大概來看一下
@Document
是被用來指定自定義注解是否能隨著被定義的java文件生成到JavaDoc文檔當(dāng)中。
@Target
是專門用來限定某個(gè)自定義注解能夠被應(yīng)用在哪些Java元素上面的,不定義說明可以放在任何元素上。
上面這個(gè) Target這玩意有個(gè)枚舉,可以清晰的看出來,他的 屬性
使用枚舉類ElementType來定義
public enum ElementType { /** 類,接口(包括注解類型)或枚舉的聲明 */ TYPE, /** 屬性的聲明 */ FIELD, /** 方法的聲明 */ METHOD, /** 方法形式參數(shù)聲明 */ PARAMETER, /** 構(gòu)造方法的聲明 */ CONSTRUCTOR, /** 局部變量聲明 */ LOCAL_VARIABLE, /** 注解類型聲明 */ ANNOTATION_TYPE, /** 包的聲明 */ PACKAGE }
@Retention
即用來修飾自定義注解的生命周期。
使用了RetentionPolicy枚舉類型定義了三個(gè)階段
public enum RetentionPolicy { /** * Annotations are to be discarded by the compiler. * (注解將被編譯器丟棄) */ SOURCE, /** * Annotations are to be recorded in the class file by the compiler * but need not be retained by the VM at run time. This is the default * behavior. * (注解將被編譯器記錄在class文件中,但在運(yùn)行時(shí)不會(huì)被虛擬機(jī)保留,這是一個(gè)默認(rèn)的行為) */ CLASS, /** * Annotations are to be recorded in the class file by the compiler and * retained by the VM at run time, so they may be read reflectively. * (注解將被編譯器記錄在class文件中,而且在運(yùn)行時(shí)會(huì)被虛擬機(jī)保留,因此它們能通過反射被讀取到) * @see java.lang.reflect.AnnotatedElement */ RUNTIME }
@Inherited
允許子類繼承父類中的注解
注解的注意事項(xiàng)
1.訪問修飾符必須為public,不寫默認(rèn)為public;
2.該元素的類型只能是基本數(shù)據(jù)類型、String、Class、枚舉類型、注解類型(體現(xiàn)了注解的嵌套效果)以及上述類型的一位數(shù)組;
3.該元素的名稱一般定義為名詞,如果注解中只有一個(gè)元素,請把名字起為value(后面使用會(huì)帶來便利操作);
4.()不是定義方法參數(shù)的地方,也不能在括號(hào)中定義任何參數(shù),僅僅只是一個(gè)特殊的語法;
5.default代表默認(rèn)值,值必須和第2點(diǎn)定義的類型一致;
6.如果沒有默認(rèn)值,代表后續(xù)使用注解時(shí)必須給該類型元素賦值。
注解的本質(zhì)
所有的Java注解都基于Annotation接口。但是,手動(dòng)定義一個(gè)繼承自Annotation接口的接口無效。要定義一個(gè)有效的Java注解,需要使用@interface關(guān)鍵字來聲明注解。Annotation接口本身只是一個(gè)普通的接口,并不定義任何注解類型。
public interface Annotation { boolean equals(Object obj); /** * 獲取hashCode */ int hashCode(); String toString(); /** *獲取注解類型 */ Class<? extends Annotation> annotationType(); }
在Java中,所有的注解都是基于Annotation接口的,但是手動(dòng)定義一個(gè)繼承自Annotation接口的接口并不會(huì)創(chuàng)建一個(gè)有效的注解。要定義有效的注解,需要使用特殊的關(guān)鍵字@interface來聲明注解類型。Annotation接口本身只是一個(gè)普通的接口,而不是一個(gè)定義注解的接口。因此,使用@interface聲明注解是定義Java注解的標(biāo)準(zhǔn)方法。
public @interface MyAnnotation1 { } public interface MyAnnotation2 extends Annotation { }
// javap -c TestAnnotation1.class Compiled from "MyAnnotation1.java" public interface com.spirimark.corejava.annotation.MyAnnotation1 extends java.lang.annotation.Annotation {} ? // javap -c TestAnnotation2.class Compiled from "MyAnnotation2.java" public interface com.spirimark.corejava.annotation.MyAnnotation2 extends java.lang.annotation.Annotation {}
雖然Java中的所有注解都是基于Annotation接口,但即使接口本身支持多繼承,注解的定義仍無法使用繼承關(guān)鍵字來實(shí)現(xiàn)。定義注解的正確方式是使用特殊的關(guān)鍵字@interface聲明注解類型。
同時(shí)需要注意的是,通過@interface聲明的注解類型不支持繼承其他注解或接口。任何嘗試?yán)^承注解類型的操作都會(huì)導(dǎo)致編譯錯(cuò)誤。
public @interface MyAnnotation1 { } /** 錯(cuò)誤的定義,注解不能繼承注解 */ @interface MyAnnotation2 extends MyAnnotation1 { } /** 錯(cuò)誤的定義,注解不能繼承接口 */ @interface MyAnnotation3 extends Annotation { }
自定義注解使用
使用方式 1
自定義注解的玩法有很多,最常見的莫過于
- 聲明注解
- 通過反射讀取
但是上面這種一般現(xiàn)在在開發(fā)中不怎么常用,最常用的就是,我們通過 切面去在注解的前后進(jìn)行加載
創(chuàng)建注解
@Target({ElementType.PARAMETER, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface BussinessLog { /** * 功能 */ BusinessTypeEnum value(); /** * 是否保存請求的參數(shù) */ boolean isSaveRequestData() default true; /** * 是否保存響應(yīng)的參數(shù) */ boolean isSaveResponseData() default true; }
設(shè)置枚舉
public enum BusinessTypeEnum { /** * 其它 */ OTHER, /** * 新增 */ INSERT, /** * 修改 */ UPDATE, /** * 刪除 */ DELETE, /** * 授權(quán) */ GRANT, /** * 導(dǎo)出 */ EXPORT, /** * 導(dǎo)入 */ IMPORT, }
創(chuàng)建切面操作
@Slf4j @Aspect @Component public class LogConfig { @Autowired private IUxmLogService uxmLogService; /** * 后置通過,?標(biāo)?法正常執(zhí)?完畢時(shí)執(zhí)? * */ @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult") public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) { handleLog(joinPoint, controllerLog, null, jsonResult); } /** * 異常通知,?標(biāo)?法發(fā)?異常的時(shí)候執(zhí)? * */ @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e") public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) { handleLog(joinPoint, controllerLog, e, null); } protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) { try { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); String title = methodSignature.getMethod().getAnnotation(ApiOperation.class).value(); // 獲取當(dāng)前的用戶 String userName = CurrentUser.getCurrentUserName(); // *========數(shù)據(jù)庫日志=========*// UxmLog uxmLog = new UxmLog(); uxmLog.setStatus(BaseConstant.YES); // 請求的地址 ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); assert requestAttributes != null; HttpServletRequest request = requestAttributes.getRequest(); String ip = getIpAddr(request); // 設(shè)置標(biāo)題 uxmLog.setTitle(title); uxmLog.setOperIp(ip); uxmLog.setOperUrl(request.getRequestURI()); uxmLog.setOperName(userName); if (e != null) { uxmLog.setStatus(BaseConstant.NO); uxmLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000)); } // 設(shè)置方法名稱 String className = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); uxmLog.setMethod(className + "." + methodName + "()"); // 設(shè)置請求方式 uxmLog.setRequestMethod(request.getMethod()); // 處理設(shè)置注解上的參數(shù) getControllerMethodDescription(joinPoint, controllerLog, uxmLog, jsonResult, request); // 保存數(shù)據(jù)庫 uxmLog.setOperTime(new Date()); uxmLogService.save(uxmLog); } catch (Exception exp) { // 記錄本地異常日志 log.error("==前置通知異常=="); log.error("異常信息:{}", exp.getMessage()); exp.printStackTrace(); } } public static String getIpAddr(HttpServletRequest request) { if (request == null) { return "unknown"; } String ip = request.getHeader("x-forwarded-for"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("X-Forwarded-For"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("X-Real-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; } public void getControllerMethodDescription(JoinPoint joinPoint, Log log, UxmLog uxmLog, Object jsonResult, HttpServletRequest request) throws Exception { // 設(shè)置action動(dòng)作 uxmLog.setBusinessType(log.value().ordinal()); // 是否需要保存request,參數(shù)和值 if (log.isSaveRequestData()) { // 獲取參數(shù)的信息,傳入到數(shù)據(jù)庫中。 setRequestValue(joinPoint, uxmLog, request); } // 是否需要保存response,參數(shù)和值 if (log.isSaveResponseData()) { uxmLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000)); } } private void setRequestValue(JoinPoint joinPoint, UxmLog uxmLog, HttpServletRequest request) throws Exception { String requestMethod = uxmLog.getRequestMethod(); if (RequestMethod.PUT.name().equals(requestMethod) || RequestMethod.POST.name().equals(requestMethod)) { String params = argsArrayToString(joinPoint.getArgs()); uxmLog.setOperParam(StringUtils.substring(params, 0, 2000)); } else { Map<?, ?> paramsMap = (Map<?, ?>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); uxmLog.setOperParam(StringUtils.substring(paramsMap.toString(), 0, 2000)); } } private String argsArrayToString(Object[] paramsArray) { StringBuilder params = new StringBuilder(); if (paramsArray != null && paramsArray.length > 0) { for (Object o : paramsArray) { if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) { try { Object jsonObj = JSON.toJSON(o); params.append(jsonObj.toString()).append(" "); } catch (Exception e) { log.error(e.getMessage()); } } } } return params.toString().trim(); } @SuppressWarnings("rawtypes") public boolean isFilterObject(final Object o) { Class<?> clazz = o.getClass(); if (clazz.isArray()) { return clazz.getComponentType().isAssignableFrom(MultipartFile.class); } else if (Collection.class.isAssignableFrom(clazz)) { Collection collection = (Collection) o; for (Object value : collection) { return value instanceof MultipartFile; } } else if (Map.class.isAssignableFrom(clazz)) { Map map = (Map) o; for (Object value : map.entrySet()) { Map.Entry entry = (Map.Entry) value; return entry.getValue() instanceof MultipartFile; } } return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse || o instanceof BindingResult; } }
這樣的話,我們就可以 在 項(xiàng)目當(dāng)中 去在標(biāo)注注解的前后去進(jìn)行輸出 日志
使用方式 2
我們可能還會(huì)在每次請求的時(shí)候去輸出日志,所以 我們也可以去定義一個(gè) 請求的 注解
@HttpLog 自動(dòng)記錄Http日志
在很多時(shí)候我們要把一些接口的Http請求信息記錄到日志里面。通常原始的做法是利用日志框架如log4j,slf4j等,在方法里面打日志log.info(“xxxx”)。但是這樣的工作無疑是單調(diào)而又重復(fù)的,我們可以采用自定義注解+切面的來簡化這一工作。通常的日志記錄都在Controller里面進(jìn)行的比較多,我們可以實(shí)現(xiàn)這樣的效果:
我們自定義@HttpLog注解,作用域在類上,凡是打上了這個(gè)注解的Controller類里面的所有方法都會(huì)自動(dòng)記錄Http日志。實(shí)現(xiàn)方式也很簡單,主要寫好切面表達(dá)式:
日志切面
下面代碼的意思,就是當(dāng)標(biāo)注了注解,我們通過 @Pointcut 定義了切入點(diǎn), 當(dāng)標(biāo)注了注解,我們會(huì)在標(biāo)注注解的 前后進(jìn)行輸出 ,當(dāng)然也包含了 Spring 官方 自帶的注解 例如 RestController
// 切面表達(dá)式,描述所有所有需要記錄log的類,所有有@HttpLog 并且有 @Controller 或 @RestController 類都會(huì)被代理 @Pointcut("@within(com.example.spiritmark.annotation.HttpLog) && (@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller))") public void httpLog() { } @Before("httpLog()") public void preHandler(JoinPoint joinPoint) { startTime.set(System.currentTimeMillis()); ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest(); log.info("Current Url: {}", httpServletRequest.getRequestURI()); log.info("Current Http Method: {}", httpServletRequest.getMethod()); log.info("Current IP: {}", httpServletRequest.getRemoteAddr()); Enumeration<String> headerNames = httpServletRequest.getHeaderNames(); log.info("=======http headers======="); while (headerNames.hasMoreElements()) { String nextName = headerNames.nextElement(); log.info(nextName.toUpperCase() + ": {}", httpServletRequest.getHeader(nextName)); } log.info("======= header end ======="); log.info("Current Class Method: {}", joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName()); log.info("Parms: {}", null != httpServletRequest.getQueryString() ? JSON.toJSONString(httpServletRequest.getQueryString().split("&")) : "EMPTY"); } @AfterReturning(returning = "response", pointcut = "httpLog()") public void afterReturn(Object response) { log.info("Response: {}", JSON.toJSONString(response)); log.info("Spend Time: [ {}", System.currentTimeMillis() - startTime.get() + " ms ]"); }
@TimeStamp 自動(dòng)注入時(shí)間戳
如果我們想通過自定義注解,在我們每次保存數(shù)據(jù)的時(shí)候,自動(dòng)的幫我們將標(biāo)注注解的方法內(nèi)的時(shí)間戳字段轉(zhuǎn)換成 正常日期,我們就需要
我們的很多數(shù)據(jù)需要記錄時(shí)間戳,最常見的就是記錄created_at和updated_at,通常我們可以通常實(shí)體類中的setCreatedAt()方法來寫入當(dāng)前時(shí)間,然后通過ORM來插入到數(shù)據(jù)庫里,但是這樣的方法比較重復(fù)枯燥,給每個(gè)需要加上時(shí)間戳的類都要寫入時(shí)間戳很麻煩而且不小心會(huì)漏掉。
另一個(gè)思路是在數(shù)據(jù)庫里面設(shè)置默認(rèn)值,插入的時(shí)候由數(shù)據(jù)庫自動(dòng)生成當(dāng)前時(shí)間戳,但是理想很豐滿,現(xiàn)實(shí)很骨感,在MySQL如果時(shí)間戳類型是datetime里即使你設(shè)置了默認(rèn)值為當(dāng)前時(shí)間也不會(huì)在時(shí)間戳為空時(shí)插入數(shù)據(jù)時(shí)自動(dòng)生成,而是會(huì)在已有時(shí)間戳記錄的情況下更新時(shí)間戳為當(dāng)前時(shí)間,這并不是我們所需要的,比如我們不希望created_at每次更改記錄時(shí)都被刷新,另外的方法是將時(shí)間戳類型改為timestamp,這樣第一個(gè)類型為timestamp的字段會(huì)在值為空時(shí)自動(dòng)生成,但是多個(gè)的話,后面的均不會(huì)自動(dòng)生成。再有一種思路是,直接在sql里面用now()函數(shù)生成,比如created_at = now()。
但是這樣必須要寫sql,如果使用的不是主打sql流的orm不會(huì)太方便,比如hibernate之類的,并且也會(huì)加大sql語句的復(fù)雜度,同時(shí)sql的可移植性也會(huì)降低,比如sqlServer中就不支持now()函數(shù)。為了簡化這個(gè)問題,我們可以自定義@TimeStamp注解,打上該注解的方法的入?yún)⒗锩娴乃袑ο蠡蛘咧付▽ο罄锩嬉怯衧etCreatedAt、setUpdatedAt這樣的方法,便會(huì)自動(dòng)注入時(shí)間戳,而無需手動(dòng)注入,同時(shí)還可以指定只注入created_at或updated_at。實(shí)現(xiàn)主要代碼如下:
@Aspect @Component public class TimeStampAspect { @Pointcut("@annotation(com.example.spiritmark.annotation.TimeStamp)") public void timeStampPointcut() {} @Before("timeStampPointcut() && @annotation(timeStamp)") public void setTimestamp(JoinPoint joinPoint, TimeStamp timeStamp) { Long currentTime = System.currentTimeMillis(); Class<?> type = timeStamp.type(); Object[] args = joinPoint.getArgs(); for (Object arg : args) { if (type.isInstance(arg)) { setTimestampForArg(arg, timeStamp); } } } private void setTimestampForArg(Object arg, TimeStamp timeStamp) { Date currentDate = new Date(System.currentTimeMillis()); TimeStampRank rank = timeStamp.rank(); Method[] methods = arg.getClass().getMethods(); for (Method method : methods) { String methodName = method.getName(); if (isSetter(methodName) && isRelevantSetter(methodName, rank)) { try { method.invoke(arg, currentDate); } catch (IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); } } } } private boolean isSetter(String methodName) { return methodName.startsWith("set") && methodName.length() > 3; } private boolean isRelevantSetter(String methodName, TimeStampRank rank) { if (rank.equals(TimeStampRank.FULL)) { return methodName.endsWith("At"); } if (rank.equals(TimeStampRank.UPDATE)) { return methodName.startsWith("setUpdated"); } if (rank.equals(TimeStampRank.CREATE)) { return methodName.startsWith("setCreated"); } return false; } }
1.使用@Aspect和@Component注解分別標(biāo)注切面和切面類,更符合AOP的實(shí)現(xiàn)方式。
2.將pointCut()和before()方法分別改名為timeStampPointcut()和setTimestamp(),更能表達(dá)它們的作用。
3.通過Class.isInstance(Object obj)方法,將原先的流操作改為了一個(gè)簡單的for循環(huán),使代碼更加簡潔。
4.將原先的setCurrentTime()方法改名為setTimestampForArg(),更能表達(dá)它的作用。
5.新增了兩個(gè)私有方法isSetter()和isRelevantSetter(),將原先在setTimestampForArg()中的邏輯分離出來,提高了代碼的可讀性和可維護(hù)性
到此這篇關(guān)于詳解Java中自定義注解的使用的文章就介紹到這了,更多相關(guān)Java自定義注解內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何根據(jù)帶賬號(hào)密碼的WSDL地址生成JAVA代碼
這篇文章主要介紹了如何根據(jù)帶賬號(hào)密碼的WSDL地址生成JAVA代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10Jmeter參數(shù)化實(shí)現(xiàn)方法及應(yīng)用實(shí)例
這篇文章主要介紹了Jmeter參數(shù)化實(shí)現(xiàn)方法及應(yīng)用實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08spring源碼學(xué)習(xí)之bean的初始化以及循環(huán)引用
這篇文章主要給大家介紹了關(guān)于spring源碼學(xué)習(xí)之bean的初始化以及循環(huán)引用的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10java與JSON數(shù)據(jù)的轉(zhuǎn)換實(shí)例詳解
這篇文章主要介紹了java與JSON數(shù)據(jù)的轉(zhuǎn)換實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2017-03-03Spring?Bean注冊與注入實(shí)現(xiàn)方法詳解
首先,要學(xué)習(xí)Spring中的Bean的注入方式,就要先了解什么是依賴注入。依賴注入是指:讓調(diào)用類對某一接口的實(shí)現(xiàn)類的實(shí)現(xiàn)類的依賴關(guān)系由第三方注入,以此來消除調(diào)用類對某一接口實(shí)現(xiàn)類的依賴。Spring容器中支持的依賴注入方式主要有屬性注入、構(gòu)造函數(shù)注入、工廠方法注入2022-10-10Spring Boot 2.0.0 終于正式發(fā)布-重大修訂版本
北京時(shí)間 2018 年 3 月 1 日早上,如約發(fā)布的 Spring Boot 2.0 在同步至 Maven 倉庫時(shí)出現(xiàn)問題,導(dǎo)致在 GitHub 上發(fā)布的 v2.0.0.RELEASE 被撤回2018-03-03Java8中LocalDateTime與時(shí)間戳timestamp的互相轉(zhuǎn)換
這篇文章主要給大家介紹了關(guān)于Java8中LocalDateTime與時(shí)間戳timestamp的互相轉(zhuǎn)換,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03