本文出處:http://blog.csdn.net/chaijunkun/article/details/44854071,轉載請註明。由於本人不定期會整理相關博文,會對相應內容作出完善。因此強烈建議在原始出處查看此文。
前言
如今互聯網項目都採用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