本文出處:http://blog.csdn.net/chaijunkun/article/details/44854071,轉載請注明。由于本人不定期會整理相關博文,會對相應内容作出完善。是以強烈建議在原始出處檢視此文。
注意:本文介紹的是Bean Validation 1.0(JSR 303)的相關内容,目前最新版本的驗證協定為Bean Validation 1.1(JSR 349)
前言
如今網際網路項目都采用HTTP接口形式進行開發。無論是Web調用還是智能裝置APP調用,隻要約定好參數形式和規則就能夠協同開發。傳回值用得最多的就是JSON形式。服務端除了保證正常的業務功能,還要經常對傳進來的參數進行驗證,例如某些參數不能為空,字元串必須含有可見字元,數值必須大于0等這樣的要求。那麼如何做到最佳實踐,讓接口開發的效率提升呢?今天我們就來聊一聊JSR 303和AOP的結合。
什麼是JSR 303
首先JSR 303是Java的标準規範,根據官方文檔的描述(https://jcp.org/en/jsr/proposalDetails?id=303):在一個應用的不同層面(例如呈現層到持久層),驗證資料是一個是反複共同的任務。許多時候相同的驗證要在每一個獨立的驗證架構中出現很多次。為了提升開發效率,阻止重複造輪子,于是形成了這樣一套規範。該規範定義了一個中繼資料模型,預設的中繼資料來源是注解(annotation)。針對該規範的驗證API不是為某一個程式設計模型來開發的,是以它不束縛于Web或者持久化。也就是說不僅僅是服務端應用程式設計可以用它,甚至富用戶端swing應用開發也可以用它。相關的入門參考資料可以參見我之前的博文:http://blog.csdn.net/chaijunkun/article/details/9083171,也可以參閱IBM開發者社群的一篇文章:http://www.ibm.com/developerworks/cn/java/j-lo-jsr303/。
什麼是AOP
然後再聊聊AOP。AOP就是Aspect Oriented Programming(面向切面程式設計)的縮寫。AOP 是一個概念,一個規範,本身并沒有設定具體語言的實作,這實際上提供了非常廣闊的發展的空間。AspectJ就是AOP的一個很悠久的實作,在Java語言中,他使用的範圍很廣。到底什麼是切面呢?舉個例子吧。在Spring MVC中,開發了若幹個Controller(控制器),并且這些控制器負責不同的子產品。每一個控制器中都有若幹個public的方法來對應各自的@RequestMapping。現在我想增加一個日志,記錄調用每個URL請求後端處理的時間。如果隻有一兩個public的方法還好一些,無非在方法開頭加個開始時間startTime,在末尾加個結束時間endTime。endTime - startTime=執行時間,最後輸出就好了。可是如果一個系統有幾十上百個控制器方法呢?挨個寫嗎?老闆說要改下日志格式呢?整個人會崩潰的!那我們就把這個描述抽象出來:public * net.csdn.blog.chaijunkun.controller.*.*(..),在包net.csdn.blog.chaijunkun.controller下面的所有類,所有public的,無論有沒有傳回值,也無論參數是什麼樣的方法,全部聚合在一起。就像劃定了一個滿足特定條件的“圈”,那麼這個“圈”就是切面,所有滿足這個條件的方法都是”切點“。我們的程式設計就建立在這之上。隻要在這個切面上加上開始時間,調用切點,再記錄結束時間,最後輸出就可以了。隻寫一次,改也很友善。
BTW,實作AOP有三種方式:①在編譯期修改源代碼;②在運作期位元組碼加載前修改位元組碼;③位元組碼加載後動态建立代理類的位元組碼。當采用第二種方式時如果你的項目在釋出時使用了代碼混淆,那麼有些時候面向切面的代碼将會失效,這點要特别注意。
執行個體
接下來我們就來個執行個體,說明一下Web項目中JSR 303為什麼要和AOP結合。該執行個體的場景是傳回JSON資料的接口,功能是對Student實體和Teacher實體進行CRUD操作:
建立Web項目及其依賴
為了簡化描述,使用maven來建立Web項目。使用JSR 303需要引入一個該規範實作使用的架構,這裡使用Hibernate Validator。另外針對AOP,需要引入AspectJ相關依賴。具體如下:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.1.3.Final</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.6.8</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.6.8</version>
</dependency>
另外還需要引入Spring Web和MVC相關的包,這裡就不贅述了
配置Spring Servlet
這裡我們要啟用注解,并且打開Spring對JSR 303的支援,另外掃描指定包下的Controller進行執行個體化:
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />
<mvc:annotation-driven />
<context:component-scan base-package="net.csdn.blog.chaijunkun.controller" />
編寫持久化對象
在本例中,為了簡化代碼,将傳參VO與持久化PO共用一個。
Student實體:
/**
* 學生對象
* @author chaijunkun
* @since 2015年4月3日
*/
public class Student {
@NotNull(groups = {Get.class, Del.class, Update.class})
private Integer id;
@NotBlank(groups = {Add.class, Update.class})
private String name;
@NotNull(groups = {Add.class, Update.class})
private Boolean male;
private Integer teacherId;
//getters and setters
}
在Student實體限制中引入了groups。主要是針對不同場景下驗證的字段不同。該參數必須是interface類型,不用實作,就是一個标記而已。聲明如下:
/**
* 學生驗證分組
* @author chaijunkun
* @since 2015年4月3日
*/
public interface StudentGroup {
public static interface Add{}
public static interface Del{}
public static interface Get{}
public static interface Update{}
}
Teachers實體:
/**
* 教師對象
* @author chaijunkun
* @since 2015年4月3日
*/
public class Teacher {
@NotNull(groups = {Get.class, Del.class, Update.class})
private Integer id;
@NotBlank(groups = {Add.class, Update.class})
private String name;
@NotNull(groups = {Add.class, Update.class})
private Boolean male;
//getters and setters
}
同Student實體類似,需要定義一個Teacher實體專用的驗證編組:
/**
* 教師驗證分組
* @author chaijunkun
* @since 2015年4月3日
*/
public interface TeacherGroup {
public static interface Add{}
public static interface Del{}
public static interface Get{}
public static interface Update{}
}
編寫虛拟的持久化服務
Student實體持久化服務:
/**
* 學生持久化服務
* @author chaijunkun
* @since 2015年4月3日
*/
@Service
public class StudentService {
private static Map<Integer, Student> vDB = new HashMap<Integer, Student>();
private static int counter = 1;
public Integer add(Student student){
student.setId(counter);
vDB.put(counter, student);
counter++;
return student.getId();
}
public boolean del(Integer id){
Student student = vDB.remove(id);
return student != null ? true : false;
}
public Student get(Integer id){
return vDB.get(id);
}
public boolean update(Student student){
Student dbObj = vDB.get(student.getId());
if (dbObj==null){
return false;
}else{
vDB.put(student.getId(), student);
return true;
}
}
}
Teacher實體持久化服務
/**
* 教師持久化服務
* @author chaijunkun
* @since 2015年4月3日
*/
@Service
public class TeacherService {
private static Map<Integer, Teacher> vDB = new HashMap<Integer, Teacher>();
private static int counter = 1;
public Integer add(Teacher teacher){
teacher.setId(counter);
vDB.put(counter, teacher);
counter++;
return teacher.getId();
}
public boolean del(Integer id){
Teacher teacher = vDB.remove(id);
return teacher != null ? true : false;
}
public Teacher get(Integer id){
return vDB.get(id);
}
public boolean update(Teacher teacher){
Teacher dbObj = vDB.get(teacher.getId());
if (dbObj==null){
return false;
}else{
vDB.put(teacher.getId(), teacher);
return true;
}
}
}
規定接口傳回資料結構
傳回資料結構為JSON。當出現錯誤時,格式為:{"code":-1,"msg":"必選參數丢失"},當成功時,格式為:{"code":0,"msg":{傳回資料}}
/**
* 響應對象
* @author chaijunkun
* @since 2015年4月3日
*/
@JsonPropertyOrder(alphabetic = false)
public class Resp<T> {
/**
* 生成成功傳回對象
* @param msg
* @return
*/
public static <T> Resp<T> success(T msg){
Resp<T> resp = new Resp<T>();
resp.setCode(0);
resp.setMsg(msg);
return resp;
}
/**
* 生成失敗傳回對象
* @param msg
* @return
*/
public static Resp<String> fail(String msg){
Resp<String> resp = new Resp<String>();
resp.setCode(-1);
resp.setMsg(msg);
return resp;
}
/** 響應代碼 */
private Integer code;
/** 響應消息 */
private T msg;
//getters and setters
}
編寫API接口
由于Teacher接口與Student接口類似,本文隻給出一個接口代碼
/**
* 學生控制器
* @author chaijunkun
* @since 2015年4月3日
*/
@Controller
@RequestMapping(value = "student")
public class StudentController {
@Autowired
private StudentService studentService;
@ResponseBody
@RequestMapping(value = "add", method = {RequestMethod.GET})
public Resp<?> add(@Validated(StudentGroup.Add.class) Student student, BindingResult result){
Integer id = studentService.add(student);
if (id == null){
return Resp.fail("添加學生資訊失敗");
}else{
return Resp.success(id);
}
}
@ResponseBody
@RequestMapping(value = "del", method = {RequestMethod.GET})
public Resp<?> del(@Validated(StudentGroup.Del.class) Student student, BindingResult result){
if (studentService.del(student.getId())){
return Resp.success(true);
}else{
return Resp.fail("删除學生資訊失敗");
}
}
@ResponseBody
@RequestMapping(value = "get", method = {RequestMethod.GET})
public Resp<?> get(@Validated(StudentGroup.Get.class) Student student, BindingResult result){
Student data = studentService.get(student.getId());
if (data == null){
return Resp.fail("未找到指定學生");
}else{
return Resp.success(data);
}
}
@ResponseBody
@RequestMapping(value = "update", method = {RequestMethod.POST})
public Resp<?> update(@Validated(StudentGroup.Update.class) Student student, BindingResult result){
if (studentService.update(student)){
return Resp.success(true);
}else{
return Resp.fail("更新學生資訊失敗");
}
}
}
使用JSR 303進行驗證,需要在Controller參數前加入@Validated注解。如果指定特别的編組,需要将編組class作為參數附加給該注解。最後一個參數定義為BindingResult類型。這樣,在進入該Controller方法後使用result.hassErrors()方法來判斷參數是否通過了限制驗證。若沒通過,可以通過BindingResult對象來擷取詳細的錯誤資訊。當然,這不是我們本文的用法,我們要突破這種麻煩的寫法。
針對Controller方法的切面程式設計
由于例子總的所有Controller都放在net.csdn.blog.chaijunkun.controller包下,是以切面的配置應該是這樣(在dispatcher-servlet.xml中配置):
<!-- JSR 303驗證切面 -->
<bean id="jsrValidationAdvice" class="net.csdn.blog.chaijunkun.aop.JSRValidationAdvice" />
<aop:config>
<aop:pointcut id="jsrValidationPC" expression="execution(public * net.csdn.blog.chaijunkun.controller.*.*(..))" />
<aop:aspect id="jsrValidationAspect" ref="jsrValidationAdvice">
<aop:around method="aroundMethod" pointcut-ref="jsrValidationPC" />
</aop:aspect>
</aop:config>
重點來了,我們來看看JSRValidationAdvice是如何實作的:
/**
* JSR303驗證架構統一處理
* @author chaijunkun
* @since 2015年4月1日
*/
public class JSRValidationAdvice {
Logger logger = LoggerFactory.getLogger(JSRValidationAdvice.class);
/**
* 判斷驗證錯誤代碼是否屬于字段為空的情況
* @param code 驗證錯誤代碼
*/
private boolean isMissingParamsError(String code){
if (code.equals(NotNull.class.getSimpleName()) || code.equals(NotBlank.class.getSimpleName()) || code.equals(NotEmpty.class.getSimpleName())){
return true;
}else{
return false;
}
}
/**
* 切點處理
* @param joinPoint
* @return
* @throws Throwable
*/
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
BindingResult result = null;
Object[] args = joinPoint.getArgs();
if (args != null && args.length != 0){
for (Object object : args) {
if (object instanceof BindingResult){
result = (BindingResult)object;
break;
}
}
}
if (result != null && result.hasErrors()){
FieldError fieldError = result.getFieldError();
String targetName = joinPoint.getTarget().getClass().getSimpleName();
String method = joinPoint.getSignature().getName();
logger.info("驗證失敗.控制器:{}, 方法:{}, 參數:{}, 屬性:{}, 錯誤:{}, 消息:{}", targetName, method, fieldError.getObjectName(), fieldError.getField(), fieldError.getCode(), fieldError.getDefaultMessage());
String firstCode = fieldError.getCode();
if (isMissingParamsError(firstCode)){
return Resp.fail("必選參數丢失");
}else{
return Resp.fail("其他錯誤");
}
}
return joinPoint.proceed();
}
}
該切面處理方法屬于圍繞Controller方法的形式,在進入Controller方法前會先調用該切面的aroundMethod(别問為什麼,看上文中這個配置:<aop:around method="aroundMethod" pointcut-ref="jsrValidationPC" />),切面方法要求第一個參數類型必須為org.aspectj.lang.ProceedingJoinPoint。進入切面方法後,周遊Controller的所有參數類型,看下有沒有BindingResult類型的參數。如果有,就調用它,判斷是否有錯誤。如果有錯誤,通過日志将詳細資訊輸出。并且傳回錯誤資訊。如果沒有錯誤,執行切點的proceed()方法,按預定Controller邏輯進行計算。
另外多說 一句,在非web項目中也可以使用JSR 303,當引入Hibernate Validator後我們可以使用下面語句來初始化一個Validator:
protected static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
然後用這個validator去驗證輸入的參數(驗證分組可以不填,使用預設分組;也可以指定一個或多個驗證分組。得到的集合是所有違規資料,可以通過是否為空來判斷是否存在違規,若不為空則對這個集合進行周遊進而得到違規資訊的細節):
Set<ConstraintViolation<QueryParam>> commonValidate = validator.validate(param, CommonGroup.class);
if (CollectionUtils.isNotEmpty(commonValidate)){
throw new IllegalArgumentException(commonValidate.iterator().next().getMessage());
}
執行個體總結
通過上面的例子,可以看到最終業務邏輯并沒有驗證代碼,隻需要注意參數前使用@Validated注解,在最後加入BindingResult類型參數即可。切面會自動幫你做驗證檢查。今後的接口開發隻需要關注業務即可,恭喜你,再也不用為驗證的事情煩心了。
本文代碼已上傳至資源分享。下載下傳位址:http://download.csdn.net/detail/chaijunkun/8562033