2. MVC 下
-
验证框架
关于验证框架,之前很少用到, 在前端传递的参数中,前端框架已经存在一些验证策略。比如:类型监测、长度监测、日期正则判断等。因此在后端Controller层中的校验就很少用到。但实际情况也可能存在有些恶意代码绕过前端验证,直接向后端发送请求这样的事情发生,因此后端的验证框架的存在也是做了二次验证,防止恶意的请求产生。- JSR-303
- JSR-303是Java标准的验证框架,已有的实现有 Hibernate Validator
- JSR-303定义一系列的注解来验证Bean的属性,如:
-
空检查
- @Null,验证对象是否为空
- @NotNull,验证对象不为空
- @NotBlank,验证字符串不为空或不是空字符串,即:"" 和 " " 都会验证失败
- @NotEmpty,验证对象不为null,或者集合不为空
-
长度检查
- @Size(min= , max= ),验证对象长度,可支持字符串、集合
- @Length,字符串长度
-
数值监测
- @Min,验证数字是否大于等于指定的值
- @Max,验证数字是否小于等于指定的值
- @Digits,验证数字是否符合指定格式,如:@Digits(integer=9, fraction=2)
- @Range,验证数字是否在指定的范围内,如:@Range(min=1, max=1000)
-
其他
- @Email,验证是否为邮件格式,为null则不做校验,已过期
- @Pattern,验证String对象是否符合正则表达式规则
-
举个栗子
public class UserInfo { @NotNull Long id; @Size(min=3, max=20) String name; }
-
- Group
-
通常,不同的业务逻辑会有不同的验证逻辑。比如上述例子,当UserInfo更新的时候,id字段不能为null;当UserInfo添加的时候,id字段必须为null;
-
JSR-303中定义了group概念,每个校验注解都必须支持。校验注解作用在字段上的时候,可以指定一个或多个group,当 Spring Boot 校验对象的时候,也可以指定校验的上下文属于某一个group。这样,只有group匹配的时候,校验注解的作用才能生效。 改写上述例子:
public class UserInfo { // 更新校验组 public interface Update{} // 添加校验组 public interface Add{} @NotNull(groups={Update.class}) @Null(groups={Add.class}) Long id; }
上述代码表示:
当校验上下文为 Add.class 的时候,@Null 生效,id需为空才能校验通过;
当校验上下文为 Update.class 的时候,@NotNull 生效,id不能为空;
-
- @Validated
-
在Controller中,只需要给方法参数添加 @Validated 即可触发参数校验,比如:
@PostMapping("/addUserInfo") @ResponseBody public void addUserInfo(@Validated({UserInfo.Add.class}) UserInfo userInfo, BindingResult result) { ... }
此方法可以接受HTTP参数并映射到UserInfo对象,该参数使用了 @Validated 注解,将触发 Spring 的校验,并将验证结果存放到 BindingResult 对象中。
@Validated 注解使用了分组后的添加校验组 UserInfo.Add.class ,因此,整个校验按照 Add.class 来校验。 -
BindingResult
- BindingResult 包含验证结果,并提供以下方法:
- hasErrors,判断验证是否通过;
- getAllErrors,获取所有的错误信息,通常返回的是 FieldError 列表
- 如果Controller参数未提供BindingResult对象,则Spring MVC 将抛出异常。
- BindingResult 包含验证结果,并提供以下方法:
-
- 自定义校验
- 关于自定义校验,说白了就是自定义注解去构建一套验证逻辑。
- 关于自定义注解,AOP这篇文章中介绍到了。详情请移步:AOP
- 这里简单分析下案例中的自定义注解:@WorkOverTime
-
注解接口:关键是@Constraint注解,声明注解实现类
@Constraint(validatedBy = { WorkOverTimeValidator.class }) @Documented @Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface WorkOverTime { String message() default "加班时间过长,不能超过{max}"; int max() default 4; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
@Documented,声明需要加入JavaDoc
@Target,描述注解使用范围,这里是分别描述了:类、接口或枚举;方法;域
@Retention,描述注解声明周期,这里是描述了:运行时注解有效
参数方面:- 常规参数:message(String)、max(int)
- 其他参数:
- groups:验证规则分组
- payload:验证有效负荷
-
注解实现类
-
注解实现类必须实现 ConstraintValidator 接口 initialize 方法及验证方法 isValid
public class WorkOverTimeValidator implements ConstraintValidator<WorkOverTime, Integer> { WorkOverTime work; int max; public void initialize(WorkOverTime work) { // 获取注解定义 this.work = work; max = work.max(); } public boolean isValid(Integer value, ConstraintValidatorContext context) { // 校验逻辑 if (value == null) { return true; } return value < max; } }
-
-
- JSR-303
-
WebMvcConfigurer:实现 WebMvcConfigurer 接口,即可配置应用的MVC全局特性
-
实现 WebMvcConfigurer 接口,会看到有以下方法可以实现
-
拦截器:通过 addInterceptors 方法可以设置多个拦截器,实现对URL拦截检查用户登录状态等操作
public void addInterceptors(InterceptorRegistry registry) { // 添加一个拦截器,检查会话,URL以user开头的都是用此拦截器 registry.addInterceptor(new SessionHandlerInterceptor()).addPathPatterns("/user/**"); }
SessionHandlerInterceptor 会话处理拦截器,实现了 HandlerInterceptor 接口
需要注意的是:拦截器有以下三个方法需要覆盖实现-
preHandle,在调用 Controller 方法前会调用此方法
-
postHandle,在调用 Controller 方法结束后、页面渲染之前调用此方法
-
afterCompletion,在页面渲染完毕后调用此方法
具体代码实现/** * 检查用户是否已经登录,如果未登录,重定向到登录页面 */ class SessionHandlerInterceptor implements HandlerInterceptor { // 调用Controller方法前会进行调用 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { User user = (User) request.getSession().getAttribute("user"); if (user == null) { response.sendRedirect("/login.html"); return false; } return true; } // 调用Controller方法结束后,页面渲染之前调用此方法 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } // 页面渲染完毕后调用此方法 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
-
-
跨域访问
-
Spring Boot 提供对 CORS 的支持,可以通过实现 addCorsMappings 接口来添加特定配置
// 配置跨域访问 public void addCorsMappings(CorsRegistry registry) { // 仅允许来自domain2.com的跨域访问,路径限定为/api,方法限定为:POST、GET registry.addMapping("/api/**") .allowedOrigins("http://domain2.com") .allowedMethods("POST", "GET"); }
allowedOrigins的作用:跨域请求发起的时候,浏览器会对请求与返回的响应信息检查 HTTP 头,如果 Access-Control-Allow-Origin 包含了自身域,则表示允许访问。反之报错。
-
-
格式化
-
当HTTP请求映射到Controller方法上的参数后,Spring会自动的进行类型转换。针对日期类型的参数,Spring默认并没有配置如何将字符串转换为日期类型,为了支持可按照指定格式转换为日期类型,需要添加一个DateFormatter类。
public void addFormatters(FormatterRegistry registry) { registry.addFormatter(new DateFormatter("yyyy-MM-dd HH:mm:ss")); }
-
-
视图映射
-
有些时候没有必要为一个URL指定一个Controller方法,可以直接将URL请求转到对应的模板渲染上。
public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/index.html").setViewName("/index.btl"); registry.addRedirectViewController("/**/*.do", "/index.html"); }
对于 index.html 的请求,设置返回的视图为 index.btl
所有以 .do 结尾的请求重定向到 /index.html 请求
-
-
-
内置视图技术
- FreeMarker
- Groovy
- Thymeleaf
- Mustache
-
Redirect 和 Forward
- Controller 中重定向可以返回以 “redirect:” 为前缀的URL
- return "redirect:/other/page"
- ModelAndView view = new ModelAndView(“redirect:/other/page”)
- RedirectView view = new RedirectView("/other/page")
- Controller 中转发可以返回以 “forward:” 为前缀的URL
- return "forward:/next/page"
- Controller 中重定向可以返回以 “redirect:” 为前缀的URL
-
通用错误处理
-
在Spring Boot 中,Controller 中抛出的异常默认交给了 “/error” 处理,应用程序可以将 /error 映射到一个特定的 Controller 中处理来替代的 Spring Boot 的默认实现,应用可以继承 AbstractErrorController 来统一处理各种系统异常。
@Controller public class ErrorController extends AbstractErrorController { private static final String ERROR_PATH = "/error"; public ErrorController() { super(new DefaultErrorAttributes()); } @RequestMapping(ERROR_PATH) public ModelAndView getErrorPath(HttpServletRequest request, HttpServletResponse response) { ... }
-
AbstractErrorController 提供多个方法可以从 request 中获取错误相关信息。
错误信息 说明 timestamp 错误发生时间 status HTTP Status error 错误信息,如Bad Request、Not Found message 详细错误信息 exception 抛出异常类名 path 请求的URI errors @Validated 参数校验错误的结果信息 - 错误处理优化:
- 异常信息直接显示给用户并不合适,尤其是 RuntimeException。
- 页面渲染、JSON请求的错误处理应分类处理。前者返回错误页面,后者返回JSON结果。
getErrorPath 方法完善:-
获取错误信息
// getErrorAttributes 提供用于获取错误信息的方法,返回Map Map<String, Object> model = Collections.unmodifiableMap( getErrorAttributes(request, false));
-
获取异常
// 获取异常(存在空情况) Throwable cause = getCause(request);
getCause() 方法
protected Throwable getCause(HttpServletRequest request) { Throwable error = (Throwable) request.getAttribute("javax.servlet.error.exception"); if (error != null) { // MVC有可能会封装异常成ServletException,需要调用getCause获取真正的异常 while (error instanceof ServletException && error.getCause() != null) { error = ((ServletException) error).getCause(); } } return error; }
-
信息获取
int status = (Integer) model.get("status"); //错误信息 String message = (String) model.get("message"); String requestPath = (String) model.get("path"); //友好提示 String errorMessage = getErrorMessage(cause);
-
日志信息打印
//后台打印日志信息方方便查错 log.info(message, cause);
-
区分客户端发起的是页面渲染请求还是JSON请求
protected boolean isJsonRequest(HttpServletRequest request) { String requestUri = request.getRequestURI(); if (requestUri.endsWith(".json")) { return true; } else { return (request.getHeader("accept").contains("application/json") || (request.getHeader("X-Requested-With") != null && request.getHeader("X-Requested-With").contains("XMLHttpRequest"))); } }
-
针对请求类型判断,进行相应的错误处理
if (!isJsonRequest(request)) { ModelAndView view = new ModelAndView("/error.btl"); view.addAllObjects(model); view.addObject("status", status); view.addObject("errorMessage", errorMessage); view.addObject("cause", cause); return view; } else { Map error = new HashMap(); error.put("success", false); error.put("errorMessage", getErrorMessage(cause)); error.put("message", message); // Json 数据写入 writeJson(response, error); return null; }
writeJson() 方法
protected void writeJson(HttpServletResponse response, Map error) { response.setContentType("application/json;charset=utf-8"); try { response.getWriter().write(objectMapper.writeValueAsString(error)); } catch (IOException e) { // ignore } }
-
友好提示:getErrorMessage() 方法
protected String getErrorMessage(Throwable ex) { /*不给前端显示详细错误*/ if (ex instanceof YourApplicationException) { // 如果YourApplicationException的信息可以显示给用户 return ((YourApplicationException)ex).getMessage(); } return "服务器错误,请联系管理员"; }
-
-
-
Transitional
-
在业务逻辑层Service中,常规采用 “接口 + 实现类” 的方式进行项目构建。
-
通常情况下,业务接口实现类中要添加 @Service 注解,同时搭配上 @Transactional 进行事务增强。
@Service @Transactional public class UserServiceImpl implements UserService { ... }
-