Spring MVC Spring MVC 流程 Spring MVC 是围绕 DispatchServlet 工作的,在它的基础上,还包含其它组件一起工作,具体流程图如下:
在 Web 服务器启动 Spring MVC 时, @RequestMapping
代表的请求路径与映射方法的映射关系就被扫描到 HandlerMapping 中存储,之后当用户发起的请i去被 DispatcherServlet 拦截之后,通过 URI 与其它条件,可以在 HandlerMapping 中找到对应的控制器或方法响应,但是 HandlerMapping 返回的是一个 HandlerExecutionChain 对象。
在 HandlerExecutionChain 对象中,包含一个处理器(handler)以及一系列拦截器。处理器其实是对控制器的包装。得到了处理器后,还需要运行它,但是因为有各种各样的请求,比如普通 HTTP 请求,WebSocket 请求等,所以需要一个适配器 HandlerAdapter 去运行处理器。
调用处理器后,会返回一个模型和视图对象 ModelAndView,然后通过视图解析器 ViewResolver 解析视图逻辑名称,当视图解析器定位到试图后,将数据模型渲染出来,返回给客户端。当然这里的步骤并不是必须的,比如在前后端分离的场景中,处理器返回的不是视图名,就不需要经过这些步骤。
静态资源配置 Spring Boot 中,默认有5个地方可以存放静态资源,按优先级排序如下:
classpath:/META-INF/resources/
classpath:/resources/
classpath:/static/
classpath:/public/
/
前面四个目录表示 resources 目录下的不同目录,第五个表示 webapp 目录,使用较少。创建 Spring Boot 项目时,系统默认为我们创建了 classpath:/static/ 目录。
如果想自定义配置,可以在配置文件中修改如下配置:
1 2 3 4 spring.resources.static-locations =classpath:/mystatic/ spring.mvc.static-path-pattern =/**
@ControllerAdvice @ControllerAdvice 是一个增强的 Controler, 使用增强 Controller 可以实现三个方面的功能:
全局异常处理
全局数据绑定
全局数据预处理
全局异常处理 全局异常处理与 @ExceptionHandler 注解搭配使用,例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler (SQLException.class ) public RespBean SQLException (SQLException e ) { if (e instanceof SQLIntegrityConstraintViolationException) return RespBean.error("该数据有外键关联,操作失败!" ); e.printStackTrace(); return RespBean.error("数据库操作失败!" ); } }
在该类中,也可以定义多个不同的方法处理不同的异常。
全局数据绑定 全局数据绑定可以做一些数据初始化操作,将公共数据定义下来:
1 2 3 4 5 6 7 @ModelAttribute (name="md" )public Map<String, Object> myData () { Map<String, Object> map = new HashMap<>(); map.put("name" ,"adam" ); map.put("age" , 66 ); return map; }
将定义添加在 @ControllerAdvice 注解的类中,这样每个 Controller 接口中都能访问这些数据。
全局数据预处理 通过 @ControllerAdvice 中的 @InitBinder 注解可以绑定一个前缀,在前端传参的时候可以通过前缀获取对应参数。@InitBinder 可以在进入控制器方法前先运行它标注的方法,可以修改 WebDataBinder机制 。
获取参数 无注解下获取参数 在无注解情况下,Spring MVC 也允许获取参数,且允许参数为空,要求参数名称和 HTTP 请求的参数名称一致 。测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 @RestController public class TestController { @GetMapping ("/no/annotation" ) public Map<String, Object> noAnnotation (Integer intVal, Long longVal, String stringVal) { Map<String, Object> map = new HashMap<>(); map.put("intVal" , intVal); map.put("longVal" , longVal); map.put("stringVal" , stringVal); return map; } }
在浏览器输入 http://localhost:8080/no/annotation?intVal=1&longVal=1&stringVal=String
查看结果
@RequestParam 由于前后端命名规则可能不同,为了将参数对应起来,可以使用 @RequestParam 来确定前后端关系。测试代码如下:
1 2 3 4 5 6 7 8 9 10 @GetMapping ("/annotation" )public Map<String, Object> requestParam (@RequestParam("int_val" ) Integer intVal, @RequestParam ("long_val" ) Long longVal, @RequestParam (value = "string_val" , required = false ) String stringVal) { Map<String, Object> map = new HashMap<>(); map.put("intVal" , intVal); map.put("longVal" , longVal); map.put("stringVal" , stringVal); return map; }
在浏览器输入 http://localhost:8080/annotation?int_val=1&long_val=1&string_val=String
查看结果,默认情况下 @RequestParam
注解标注的参数不能为空,如果需要配置为空,可配置其属性 required 为 false 。
传递数组 Spring MVC 内部支持由逗号分隔的数组,测试代码如下:
1 2 3 4 5 6 7 8 9 10 @GetMapping ("/array" ) public Map<String, Object> requestArray (@RequestParam("int_val" ) Integer[] intVals, @RequestParam ("long_val" ) Long[] longVals, @RequestParam (value = "string_val" , required = false ) String []stringVals) { Map<String, Object> map = new HashMap<>(); map.put("intVals" , Arrays.toString(intVals)); map.put("longVals" , Arrays.toString(longVals)); map.put("stringVals" , Arrays.toString(stringVals)); return map; }
在浏览器输入 http://localhost:8080/array?intVals=1,2,3&longVals=4,5,6&stringVals=adam,tom,%E5%AD%97%E7%AC%A6%E4%B8%B2
查看结果即可。
获得 URL 参数 在 REST 风格的网站中,参数有时候会通过 URL 进行传递,此时可以通过 @PathVariable 获取 URL 参数。测试代码如下:
1 2 3 4 5 6 7 8 @GetMapping ("/{id}/{name}" )public Map<String, Object> get (@PathVariable("id" ) int id, @PathVariable ("name" ) String name) { Map<String, Object> map = new HashMap<>(); map.put("id" , id); map.put("name" , name); return map; }
在浏览器输入 http://localhost:8080/99/adam
查看结果
传递 JSON 在很多情况下,需要通过请求体传递 JSON 到服务端,服务端可以使用 @RequestBody 来接收请求体的 JSON 参数。
获取格式化参数 在某些情况,需要格式化数据, Spring MVC 对日期类型和数字类型分别提供了 @DateTimeFormat 和 @NumberFormat 来进行处理。对于日期类的数据参数,还可以在配置文件中添加配置项 spring.mvc.date-format=yyyy-MM-dd
类进行配置。
1 2 3 4 5 6 7 8 @GetMapping ("/format" ) public Map<String, Object> format (@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date date, @NumberFormat (pattern = "#,###.##" ) Double number) { Map<String, Object> map = new HashMap<>(); map.put("date" , date); map.put("number" , number); return map; }
在浏览器输入 http://localhost:8080/format?date=1998-01-03&number=1,32.2
验证结果。
自定义参数转换规则 处理器获取参数逻辑 当一个请求到来,在处理器执行过程中,首先从 HTTP 请求和上下文环境来获取参数,如果是简单的参数会用简单的转换器进行转换,如果是 HTTP 请求体,会调用 HttpMessageConverter 接口的方法对请求体信息进行转换 。
在 HttpMessageConverter 中,会调用 WebDataBinder 机制获取参数,它主要通过三种接口进行参数类型转换与验证:Converter, Formatter 与 GenericConverter 。其中 Converter 是一个普通的转换器,Formatter 是格式化转化器, GenericConverter 是数组转换器。Spring MVC 会自动遍历 IoC 容器注册以上三种类的 Bean。
自定义一对一转换器 首先顶一个 User 类如下:
1 2 3 4 5 public class User { String name; int id; }
然后定义一个转换器如下,传入第一个泛型参数是原始类型,第二个参数是转换后的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 @Component public class String2UserConverter implements Converter <String , User > { @Override public User convert (String s) { User user = new User(); String[] attrs = s.split("-" ); String name = attrs[0 ]; int id = Integer.parseInt(attrs[1 ]); user.setId(id); user.setName(name); return user; } }
编写控制器方法如下:
1 2 3 4 @GetMapping ("/converter" ) public User getUserByConverter (User user) { return user; }
在浏览器输入 http://localhost:8080/converter?user=adam-22
查看结果即可。
数组转换 数组转换器首先会把字符串用逗号分割为一个个子字符串,然后根据原始泛型,目标泛型找到对应的 Converter 进行转换。
编写控制器方法如下:
1 2 3 4 @GetMapping ("/list" )public List<User> getUserListByConverter (List<User> users) { return users; }
在浏览器输入 http://localhost:8080/list?users=adam-22,tom-13,marry-99
查看结果即可。
自定义验证器 在 WebDataBinder 中除了可以注册转换器之外,还可以注册验证器,验证接口定义如下:
1 2 3 4 5 6 7 public interface Validator { boolean supports (Class<?> var1) ; void validate (Object var1, Errors var2) ; }
绑定验证器需要获得 WebDataBinder 对象,调用它的 setValidator 对象来绑定验证器。
自定义拦截器 自定义拦截器需要实现 HandlerInterceptor 接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class MyInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("处理器前方法" ); return false ; } @Override public void postHandle (HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("处理器后方法" ); } @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("处理器完成后方法" ); } }
之后注册这个拦截器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @SpringBootApplication public class MvcdemoApplication implements WebMvcConfigurer { public static void main (String[] args) { SpringApplication.run(MvcdemoApplication.class , args ) ; } @Override public void addInterceptors (InterceptorRegistry registry) { InterceptorRegistration ir = registry.addInterceptor(new MyInterceptor()); ir.addPathPatterns("/*" ); } }
然后添加控制器方法测试:
1 2 3 4 @GetMapping ("/interceptor/" )public void interceptorTest () { System.out.println("执行处理器逻辑" ); }
多个拦截器之间也是按责任链模式的规则调用,执行顺序便是注册顺序 。
文件上传 Spring MVC 对文件上传提供了良好的支持,首先 DispatherServlet 通过适配器模式将 HttpServletRequest 对象转换为 MultipartHttpServletReques 对象 ,这个对象定义了许多文件相关方法。
在使用 Spring MVC 上传文件时,还需要配置 MultipartHttpServletRequest,这个是通过 MultipartResolver 接口实现,它有两个实现类:
StandardServletMultipartResolver 推荐使用,不需要依赖第三方包,并且也是默认使用。
CommonsMultipartResolver 不推荐使用,依赖第三方包。
在 Controller 中,可以使用 Part 接口(Servlet API 提供)或者 MultipartFile 接口(Spring MVC 提供)作为参数 。
首先在配置文件对上传文件做配置:
1 2 3 4 5 6 7 8 9 10 11 12 spring.servlet.multipart.enabled =true spring.servlet.multipart.file-size-threshold =10MB spring.servlet.multipart.max-file-size =2MB spring.servlet.multipart.max-request-size =10MB spring.servlet.multipart.location =G:/upload spring.servlet.multipart.resolve-lazily =false
添加控制器方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @PostMapping ("/upload/part" )public String upload (Part file) { String fileName = file.getSubmittedFileName(); try { file.write(fileName); }catch (Exception e){ e.printStackTrace(); return "上传失败" ; } return "上传成功!!!" ; } @PostMapping ("/upload/multipart" )public String uploadMultipart (MultipartFile file) { String filename = file.getOriginalFilename(); try { file.transferTo(new File(filename)); }catch (Exception e){ e.printStackTrace(); return "上传失败" ; } return "上传成功!!!" ; }
重定向 重定向就是将网络请求重新定个方向或者转到其它位置,在 Spring MVC 中,可以使用字符串指定跳转或者使用模型和视图指定跳转,只需要在需要重定向的 url 前加上 redirect: 即可。
1 2 3 4 5 6 return "redirect:/home" 使用模型和视图指定跳转 mv.setViewName("redirect:/home" ) return mv
转发与重定向的区别
转发的地址栏还是原来的地址,重定向地址栏显示的是新的URL。
转发页面和转发到的页面可以共享 request 里面的数据,重定向不能。
转发是服务器行为,重定向是客户端行为(两次 Request)。
转发效率较高。
RedirectAttributes 为了解决转发的数据共享问题,Spring MVC 提供了 RedirectAttributes,它扩展了 ModelMap 接口,有一个 addFlashAttribute方法,这个方法可以保存需要传递给重定向的数据。
操作会话对象 Spring MVC 提供了两个注解操作会话对象:
@SessionAttribute 应用于参数,将 HttpSession 中的属性读出,赋予控制器参数。
@SessionAttributes 应用于类,将相关数据模型的属性自动保存到 Session 中,可以指定数据模型名称或者数据模型的类型来保存。例:@SessionAttributes(names = {"user"}, types = Long.class)
跨域问题 跨域问题可以通过 @CrossOrigin 注解解决,例如:@CrossOrigin(value="http://localhost:8081")
,该注解可以放在类上或者方法上。
也可以进行全局配置:
1 2 3 4 5 6 7 8 9 10 11 @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings (CorsRegistry registry) { registry.addMapping("/**" ) .allowedOrigins("http://localhost:8081" ) .allowedMethods("*" ) .allowedHeaders("*" ); } }