使用JSR 303和AOP簡化你的接口開發

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章