自定義註解!絕對是程式設計師裝逼的利器!!
相信很多人對Java中的註解都很熟悉,比如我們經常會用到的一些如@Override、@Autowired、@Service等,這些都是JDK或者諸如Spring這類框架給我們提供的。
在以往的面試過程中,我發現,關於註解的知識很多程式設計師都僅僅停留在使用的層面上,很少有人知道註解是如何實現的,更別提使用自定義註解來解決實際問題了。
但是其實,我覺得一個好的程式設計師的標準就是懂得如何優化自己的程式碼,那在程式碼優化上面,如何精簡程式碼,去掉重複程式碼就是一個至關重要的話題,在這個話題領域,自定義註解絕對可以算得上是一個大大的功臣。
所以, 在我看來,會使用自定義註解 ≈ 好的程式設計師。
那麼,本文,就來介紹幾個,作者在開發中實際用到的幾個例子,向你介紹下如何使用註解來提升你程式碼的逼格。
基本知識
在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
)
;
}
//java學習交流:737251827 進入可領取學習資源及對十年開發經驗大佬提問,免費解答!
// 解析表示式,獲取結果
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
)
;
}
}
//java學習交流:737251827 進入可領取學習資源及對十年開發經驗大佬提問,免費解答!
/**
* 定義並返回一個通用的失敗響應
*/
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我省略了很多東西,我們真正使用的那個切面,不僅僅做了引數檢查,還可以做很多其他事情。比如異常的統一處理、錯誤碼的統一轉換、記錄方法執行時長、記錄方法的入參出參等等。
總之,使用切面+自定義註解,我們可以統一做很多事情。除了以上的這幾個場景,我們還有很多相似的用法,比如:
統一的快取處理。如某些操作需要在操作前查快取、操作後更新快取。這種就可以通過自定義註解+切面的方式統一處理。
程式碼其實都差不多,思路也比較簡單,就是通過自定義註解來標註需要被切面處理的累或者方法,然後在切面中對方法的執行過程進行干預,比如在執行前或者執行後做一些特殊的操作。
使用這種方式可以大大減少重複程式碼,大大提升程式碼的優雅性,方便我們使用。
但是同時也不能過度使用,因為註解看似簡單,但是其實內部有很多邏輯是容易被忽略的。就像我之前寫過一篇《Spring官方都推薦使用的@Transactional事務,為啥我不建議使用!》中提到的觀點一樣,無腦的使用切面和註解,可能會引入一些不必要的問題。
不管怎麼說,自定義註解卻是是一個很好的發明,可以減少很多重複程式碼。快快在你的專案中用起來吧。
(全文完)
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70010294/viewspace-2847912/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 你的開發利器Spring自定義註解Spring
- 程式設計師程式設計必備名言佳句,提升裝逼指數~程式設計師
- 自定義註解
- Java中的註解-自定義註解Java
- 好程式設計師教程分享Java註解和運用註解程式設計程式設計師Java
- 自定義JAVA註解Java
- 自定義ConditionalOnXX註解
- 讓程式設計師不再苦逼的神器(上)程式設計師
- 讓程式設計師不再苦逼的神器(下)程式設計師
- 裝逼技巧:程式設計師如何用程式碼罵別人sb,以及證明自己牛逼!程式設計師
- springBoot自定義註解的使用Spring Boot
- 提高程式設計師的幾大利器程式設計師
- 好程式設計師分享SpringBoot須掌握的註解程式設計師Spring Boot
- [趣圖]程式設計社群調查顯示,Java程式設計師最苦逼,C++程式設計師最年老,是這樣的麼?Java程式設計師C++
- Java註解-後設資料、註解分類、內建註解和自定義註解Java
- JSR303自定義校驗註解,自定義註解校驗字串是否是JSON字串,可擴充套件字串JSON套件
- 程式設計師是否有義務做好程式碼的註釋?你做好程式碼註釋了嗎?程式設計師
- 程式設計師筆記——springboot 之常用註解程式設計師筆記Spring Boot
- JAVA-註解(2)-自定義註解及反射註解Java反射
- Java程式設計師必須掌握的5個註解!Java程式設計師
- Spring註解淺入淺出——不吹牛逼不裝逼Spring
- 好程式設計師web前端分享絕對路徑與相對路徑的引用程式設計師Web前端
- 自定義註解以及註解在反射中的應用反射
- 好程式設計師web前端教程分享web中CSS絕對定位程式設計師Web前端CSS
- java中如何自定義註解Java
- Spring Boot 自定義註解失效Spring Boot
- SpringBoot自定義校驗註解Spring Boot
- 自定義校驗註解ConstraintValidatorAI
- 好程式設計師Java分享SpringMVC之@ResponseBody註解程式設計師JavaSpringMVC
- 巧用自定義註解,一行程式碼搞定審計日誌行程
- 好程式設計師分享html圖片絕對路徑改相對路徑程式設計師HTML
- 為什麼說程式碼註釋是程式設計師必備的技能?程式設計師
- 自定義註解+反射 實現給註解新增功能的效果反射
- Spring 註解程式設計之模式註解Spring程式設計模式
- Spring的資料庫程式設計淺入淺出——不吹牛逼不裝逼Spring資料庫程式設計
- 做個清醒的程式設計師之拒絕工作程式設計師
- 【1024程式設計師節】程式設計師,你學程式設計的初衷是什麼?程式設計師
- 程式設計師接私活的7大平臺利器程式設計師