Spring MVC 与 web开发
项目组用了 Spring MVC 进行开发,觉得对里面的使用方式不是很满意,就想,如果是我来搭建开发环境,我会怎么做?下面就是我的想法,只关注于 MVC 的 View 层。
一、统一的响应格式
现在基本上都是用 ajax 来调用后台接口,拿到 json格式的数据再展示,有的人直接返回数据,却没有考虑异常的情况,我觉得返回的报文里必须包含表示可能的异常信息的数据和业务响应数据。我定义了下面这个类来表示报文格式:
/**
* 统一的 HTTP 响应格式。<br/>
* code 为 "ok" 表示业务调用成功,否则是失败的错误码,如果有多个则以逗号分隔。<br/>
* data 是业务数据,如果失败了则是 null。
*
* @author http://coderbee.net
*
*/
public class RespBody {
public static final String OK_CODE = "ok";
private final String code;
private final Object data;
private static final RespBody OK = new RespBody(OK_CODE, null);
private RespBody(String code, Object data) {
this.code = code;
this.data = data;
}
public static RespBody ok() {
return OK;
}
public static RespBody ok(Object data) {
return new RespBody("ok", data);
}
public static RespBody error(String code) {
return new RespBody(code, null);
}
public static RespBody error(String code, Object msg) {
return new RespBody(code, msg);
}
public String getCode() {
return code;
}
public Object getData() {
return data;
}
}
这个类提供了一些静态方法来快速构建响应报文,这也是很重要的一个设计:用静态工厂方法而不是构造函数。
这里的 code
不应该是直接的错误提示信息,应该只是简单的错误编码,这样不同的客户端都可以调用这个 API,然后再根据错误编码、客户端语言和自己的客户端特性选择合适的错误提示信息和提示方式。
二、统一的异常处理
很多人都不考虑异常的情况,导致异常栈直接抛到响应里,这是不友好也不安全的。统一的异常处理是必须的。
我定义了一个 BaseController
:
@Controller
public class BaseController {
protected final static Logger logger = LoggerFactory
.getLogger("controller");
@ResponseBody
@ExceptionHandler(Exception.class)
public RespBody exceptionHandler(Exception ex) {
return RespBody.error("exception", ex.getMessage());
}
}
它的作用很简单,就是定义了一个统一的异常处理逻辑。Spring MVC对异常处理的逻辑很好,如果某个 Controller
类没有提供带 @ExceptionHandler
注解的方法,则会查找父类是否有这种方法,所以继承自这个 BaseController
的 Controller
都自动获得异常处理能力。
三、统一的参数校验
参数校验是必须的,而且必须放在服务器端来做,客户端的校验都是可以绕过的。
Spring MVC 当然也支持参数校验,在 Spring MVC 的配置文件里加入 <mvc:annotation-driven />
即可以开启注解校验。
但 Spring MVC 的参数校验有些局限:
- 不支持对基本类型和
String
类型的参数进行校验,也就是只支持对 POJO 校验,这个非常不友好,如果一个接口只有很少的几个参数都必须定义一个 POJO 是很恼人的,要么就得手动校验,写一些 if 分支; - 每个POJO后面都得有一个
BindingResult
的参数,作为对 POJO 的校验结果。
而且在每个方法里都必须对 BindingResult
进行检测,来判断参数是否合法。
在 AOP 里进行参数校验
借助 Spring 对 AOP 的支持,我们可以在 AOP 里对请求的 Controller
的方法进行拦截,做参数校验,如果校验不合格,则直接返回(因为我们已经有了统一的响应格式)。
在 AOP 里,我们可以用 Hibernate-Validator 进行手动校验,而不是通过 Spring-MVC 进行校验,这样我们就不需要在每个 POJO 后面放一个 BindingResult
参数,且 Hibernate-Validator 支持对基本类型和 String
类型的参数进行校验。
下面的代码是在 Hibernate-Validator-4.2.0-Final、validation-api-1.0.0-GA 下测试的:
首先定义一个 BindingResultHandler
类,它的方法 validate
校验请求的 Controller
的方法的参数是否合格,如果合格则继续调用业务逻辑,否则返回错误提示。
public class BindingResultHandler {
public Object validate(ProceedingJoinPoint pjp) throws Throwable {
Object target = pjp.getTarget();
MethodSignature joinPointObject = (MethodSignature) pjp.getSignature();
Method method = joinPointObject.getMethod();
MethodValidator validator = Validation
.byProvider(HibernateValidator.class).configure()
.buildValidatorFactory().getValidator()
.unwrap(MethodValidator.class);
Set<MethodConstraintViolation<Object>> violations = validator
.validateAllParameters(target, method, pjp.getArgs(),
new Class[] {});
if (!violations.isEmpty()) {
StringBuilder sb = new StringBuilder(128);
for (ConstraintViolation<Object> violation : violations) {
sb.append(',').append(violation.getMessage());
}
return RespBody.error(sb.substring(1), "param validation failed .");
}
return pjp.proceed();
}
}
AOP 配置:
<bean id="bindingResultHandler" class="net.coderbee.demo.controller.validation.BindingResultHandler" />
<aop:config>
<aop:aspect id="aspectBindingResult" ref="bindingResultHandler">
<aop:pointcut id="bindingResultHandlerPointcut"
expression="execution(public * net.coderbee.demo.controller..*Controller.*(..))" />
<aop:around method="validate" pointcut-ref="bindingResultHandlerPointcut" />
</aop:aspect>
</aop:config>
这样在 Controller 里就只需做注解不能完成的校验了。
@Controller
public class UserController extends BaseController {
@ResponseBody
@RequestMapping(value = "/test/valids")
public RespBody valids(@Valid User user, @Valid Address address) {
return RespBody.ok(user);
}
}
这样的代码会简洁很多。