天天看點

使用JSR 303和AOP簡化你的接口開發前言什麼是JSR 303什麼是AOP執行個體

本文出處: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

繼續閱讀