Spring Boot 2.0 读书笔记_04:MVC 下

版权声明:未经博主本人同意,请勿私自转发分享。 https://blog.csdn.net/Nerver_77/article/details/84587299

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 将抛出异常。
    • 自定义校验
      • 关于自定义校验,说白了就是自定义注解去构建一套验证逻辑
      • 关于自定义注解,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;
              	}
              }
            
  • WebMvcConfigurer:实现 WebMvcConfigurer 接口,即可配置应用的MVC全局特性

    • 实现 WebMvcConfigurer 接口,会看到有以下方法可以实现
      在这里插入图片描述

    • 拦截器:通过 addInterceptors 方法可以设置多个拦截器,实现对URL拦截检查用户登录状态等操作

        public void addInterceptors(InterceptorRegistry registry) {
        	// 添加一个拦截器,检查会话,URL以user开头的都是用此拦截器
          registry.addInterceptor(new SessionHandlerInterceptor()).addPathPatterns("/user/**");
        }
      

      SessionHandlerInterceptor 会话处理拦截器,实现了 HandlerInterceptor 接口
      需要注意的是:拦截器有以下三个方法需要覆盖实现

      1. preHandle,在调用 Controller 方法前会调用此方法

      2. postHandle,在调用 Controller 方法结束后、页面渲染之前调用此方法

      3. 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"
  • 通用错误处理

    • 在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 {
        	...
        }
      

猜你喜欢

转载自blog.csdn.net/Nerver_77/article/details/84587299
今日推荐