架构模式(是一种软件架构设计思想,不止Java开发中用到,其它语言也需要用到),它将应用分为三块:M:Model(模型),负责业务处理及数据的收集V:View(视图),负责数据的展示C:Controller(控制器),负责调度。它是一个调度中心,它来决定什么时候调用Model来处理业务,什么时候调用View视图来展示数据
MVC架构模式的描述:前端浏览器发送请求给web服务器,web服务器中的Controller接收到用户的请求,Controller负责将前端提交的数据进行封装,然后Controller调用Model来处理业务,当Model处理完业务后会返回处理之后的数据给Controller,Controller再调用View来完成数据的展示,最终将结果响应给浏览器,浏览器进行渲染展示页面。
什么是三层模型
表现层,将Model数据模型拆封为业务层和与数据库交互的持久层
MVC架构模式与三层模型的区别?
关注点不同业务逻辑组件的划分应用程序的层次关系和分离思想MVC架构模式的Web框架,底层基于Servlet实现Spring MVCSpring WebSocket
4.0.0 com.xc springmvc-xml 1.0-SNAPSHOT war org.springframework spring-webmvc 5.3.1 javax.servlet javax.servlet-api 3.1.0 provided org.thymeleaf thymeleaf-spring5 3.0.11.RELEASE 8 8 UTF-8 组件扫描。spring扫描这个包中的类,将这个包中的类实例化并纳入IoC容器的管理视图解析器。视图解析器(View Resolver)的作用主要是将Controller方法返回的逻辑视图名称解析成实际的视图对象。视图解析器将解析出的视图对象返回给DispatcherServlet,并最终由DispatcherServlet将该视图对象转化为响应结果,呈现给用户 ServletDispatcherServlet,我们称其为前端控制器 springmvc org.springframework.web.servlet.DispatcherServlet contextConfigLocation classpath:springmvc.xml 1 springmvc / DispatcherServlet是SpringMVC框架为我们提供的最核心的类,它是整个SpringMVC框架的前端控制器,负责接收HTTP请求、将请求路由到处理程序、处理响应信息,最终将响应返回给客户端。
控制器(Controller)来处理此请求模型对象(Model)HTML页面 Title hello world @Controller public class HelloController { @RequestMapping("/test") public String test(){ return "success"; } } 配置Tomcat

启动tomcat,调用test

@RequestMapping 注解是 Spring MVC 框架中的一个控制器映射注解,用于将请求映射到相应的处理方法上。具体来说,它可以将指定 URL 的请求绑定到一个特定的方法或类上,从而实现对请求的处理和响应。
RequestMapping的出现位置
类上或者方法上
请求路径,也就是说通过该请求路径与对应的控制器的方法绑定在一起数组,表示可以提供多个路径,也就是说,多个不同的请求路径可以映射同一个控制器的同一个方法value属性和path属性互为别名,两个属性一样
举例
@Controller @RequestMapping("/hello") public class RequestMappingController { @RequestMapping(value = {"/test1","test2"}) public String test(){ return "success"; } } 模糊匹配,我们把这种模糊匹配称之为Ant风格?:表示任意的单个字符*:表示任意的0个或多个字符**:表示任意的一层或多层目录(只能使用xxx/**的方式)匹配?例子
@RequestMapping("/x?z/testValueAnt") public String testValueAnt(){ return "success"; } 

匹配*例子
@RequestMapping("/x*z/testValueAnt") public String testValueAnt(){ return "success"; } 

匹配**例子
**通配符只能出现在路径的末尾,否则抛错,spring5可以不用在末尾@RequestMapping("/testValueAnt/**") public String testValueAnt(){ return "success"; } 
普通的请求路径:http://localhost:8080/springmvc/login?username=admin&password=123&age=20
restful风格的请求路径:http://localhost:8080/springmvc/login/admin/123/20
如果使用restful风格的请求路径,在控制器中应该如何获取请求中的数据呢?
@PathVariable路径变量注解会抛500异常@RequestMapping(value = "/testRestful/{id}/{username}/{age}") public String testRestful( @PathVariable("id") int id, @PathVariable("username") String username, @PathVariable("age") int age) { System.out.println(id + "," + username + "," + age); return "success"; } 405错误RequestMapping注解的method属性来实现限制请求方式
数组RequestMethod,而RequestMethod是一个枚举类型的数据
举例
@RequestMapping(value="/login", method = {RequestMethod.GET,RequestMethod.POST}) public String testMethod(){ return "success"; } GetMapping:要求前端必须发送get请求PutMapping:要求前端必须发送put请求DeleteMapping:要求前端必须发送delete请求PatchMapping:要求前端必须发送patch请求举例
//@RequestMapping(value="/login", method = RequestMethod.POST) @PostMapping("/login") public String testMethod(){ return "success"; } 前端向服务器发送请求的方式包括哪些?共9种
获取资源,只允许读取数据,不影响数据的状态和功能URL中传递参数或者在HTTP请求的头部使用参数,服务器返回请求的资源提交资源,可能还会改变数据的状态和功能请求体,服务器接收请求体后,进行数据处理更新资源,用于更新指定的资源上所有可编辑内容请求体发送需要被更新的全部内容,服务器接收数据后,将被更新的资源进行替换或修改删除资源,用于删除指定的资源URL中或请求体中跨域检查⚠️注意
ajax请求的方式来实现GET和POST的区别
获取数据传送数据支持缓存。 也就是说当第二次发送get请求时,会走浏览器上次的缓存结果,不再真正的请求服务器不支持缓存。每一次发送post请求都会真正的走服务器数组,不过要求请求参数必须和params数组中要求的所有参数完全一致后,才能映射成功
params属性的4种用法
@RequestMapping(value="/login", params={"username", "password"}) @RequestMapping(value="/login", params={"!username", "password"})@RequestMapping(value="/login", params={"username=admin", "password"})@RequestMapping(value="/login", params={"username!=admin", "password"})请求头信息一致时,才能映射成功headers属性的4种用法
@RequestMapping(value="/login", headers={"Referer", "Host"}) @RequestMapping(value="/login", headers={"!Referer", "Host"})@RequestMapping(value="/login", headers={"Referer=xxx", "Host"})@RequestMapping(value="/login", headers={"Referer!=xxx", "Host"})前端表单提交数据

F12查询提交数据方式

后端控制器获取数据
@PostMapping(value="/register") public String register(HttpServletRequest request){ // 通过当前请求对象获取提交的数据 String username = request.getParameter("username"); String password = request.getParameter("password"); String sex = request.getParameter("sex"); String[] hobbies = request.getParameterValues("hobby"); String intro = request.getParameter("intro"); System.out.println(username + "," + password + "," + sex + "," + Arrays.toString(hobbies) + "," + intro); return "success"; } 这样通过Servlet原生的API获取到提交的数据。但是这种方式不建议使用,因为方法的参数HttpServletRequest依赖Servlet原生API,Controller的测试将不能单独测试,必须依赖web服务器才能测试。
请求参数与方法上的形参映射@PostMapping(value = "/register") public String register( @RequestParam(value = "username") String a, @RequestParam(value = "password") String b, @RequestParam(value = "sex") String c, @RequestParam(value = "hobby") String[] d, @RequestParam(name = "intro") String e ) { System.out.println(a); System.out.println(b); System.out.println(c); System.out.println(Arrays.toString(d)); System.out.println(e); return "success"; } value和name,互为别名,作用相同
name1=value1&name2=value2,则这个注解应该这样写:@RequestParam(value="name1")、@RequestParam(value="name2")是否为必须的true,表示方法参数是必需的。如果请求中缺少对应的参数,则会抛出异常false,false表示不是必须的,如果请求中缺少对应的参数,则方法的参数为null
举例


没有提供对应的请求参数或者请求参数的值是空字符串""的时候,方法的形参会采用默认值举例

方法形参的名字和提交数据时的name相同,则@RequestParam可以省略@PostMapping(value="/register") public String register(String username, String password, String sex, String[] hobby, String intro){ System.out.println(username + "," + password + "," + sex + "," + Arrays.toString(hobby) + "," + intro); return "success"; } 实体类来接收请求参数实体类的属性名必须和请求参数的参数名保持一致@PostMapping("/register") public String register(User user){ System.out.println(user); return "success"; } setter方法名请求头信息映射到方法的形参上@PostMapping("/register") public String register(User user, @RequestHeader(value="Referer", required = false, defaultValue = "") String referer){ System.out.println(user); System.out.println(referer); return "success"; } 请求提交的Cookie数据映射到方法的形参上@GetMapping("/register") public String register(User user, @CookieValue(value="id", required = false, defaultValue = "110") String id){ System.out.println(user); System.out.println(id); return "success"; } server.xml文件,找到其中配置端口号的标签,在该标签中添加URIEncoding="UTF-8
Tomcat10,Tomcat9,有如下的默认配置,在默认情况下URIEncoding使用的就是UTF-8的编码方式
Tomcat8,URIEncoding的默认配置是ISO-8859-1
请求体的中文乱码问题request.setCharacterEncoding("UTF-8"); Tomcat10服务器来说,针对请求体中的字符编码也是配置好的,默认也是采用了UTF-8,web.xml配置如下
乱码过滤器 encoding org.springframework.web.filter.CharacterEncodingFilter encoding utf-8 forceEncoding true encoding /* 

request、会话域:session、应用域:application// 向域中存储数据 void setAttribute(String name, Object obj); // 从域中读取数据 Object getAttribute(String name); // 删除域中的数据 void removeAttribute(String name); 数据的传递和共享一次请求,一次请求一个request使用原生Servlet API方式
@RequestMapping("/testServletAPI") public String testServletAPI(HttpServletRequest request){ // 向request域中存储数据 request.setAttribute("testRequestScope", "在SpringMVC中使用原生Servlet API实现request域数据共享"); return "view"; } 使用Model接口
@RequestMapping("/testModel") public String testModel(Model model){ // 向request域中存储数据 model.addAttribute("testRequestScope", "在SpringMVC中使用Model接口实现request域数据共享"); return "view"; } 使用Map接口
@RequestMapping("/testMap") public String testMap(Map map){ // 向request域中存储数据 map.put("testRequestScope", "在SpringMVC中使用Map接口实现request域数据共享"); return "view"; } 使用ModelMap类
@RequestMapping("/testModelMap") public String testModelMap(ModelMap modelMap){ // 向request域中存储数据 modelMap.addAttribute("testRequestScope", "在SpringMVC中使用ModelMap实现request域数据共享"); return "view"; } Model、Map、ModelMap的关系?
BindingAwareModelMap
ModelMap实现Model,而ModelMap又实现了Map接口
使用ModelAndView类
ModelAndView。这个类的实例封装了Model和View@RequestMapping("/testModelAndView") public ModelAndView testModelAndView(){ // 创建“模型与视图对象” ModelAndView modelAndView = new ModelAndView(); // 绑定数据 modelAndView.addObject("testRequestScope", "在SpringMVC中使用ModelAndView实现request域数据共享"); // 绑定视图 modelAndView.setViewName("view"); // 返回 return modelAndView; } 注意:
以上我们通过了五种方式完成了request域数据共享,这几种方式在底层DispatcherServlet调用我们的Controller之后,返回的对象都是ModelAndView。

使用原生Servlet API方式
@RequestMapping("/testSessionScope1") public String testServletAPI(HttpSession session) { // 向会话域中存储数据 session.setAttribute("testSessionScope1", "使用原生Servlet API实现session域共享数据"); return "view"; } 使用原生Servlet API方式
@RequestMapping("/testApplicationScope") public String testApplicationScope(HttpServletRequest request){ // 获取ServletContext对象 ServletContext application = request.getServletContext(); // 向应用域中存储数据 application.setAttribute("applicationScope", "我是应用域当中的一条数据"); return "view"; } HttpMessageConverter是Spring MVC中非常重要的一个接口HTTP消息转换器。该接口下提供了很多实现类,不同的实现类有不同的转换方式
HTTP协议与Java程序中的对象之间的互相转换
Form表单转换器
请求体中的数据是如何转换成user对象的,底层实际上使用了HttpMessageConverter接口的其中一个实现类FormHttpMessageConverter。

通过上图可以看出FormHttpMessageConverter是负责将请求协议转换为Java对象的。
默认转换器
Controller返回值看做逻辑视图名称,视图解析器将其转换成物理视图名称,生成视图对象,StringHttpMessageConverter负责将视图对象中的HTML字符串写入到HTTP协议的响应体中。最终完成响应。

通过上图可以看出StringHttpMessageConverter是负责将Java对象转换为响应协议的。
首页面AJAX请求获取数据,非跳转页面Controller
// 有返回值 @RequestMapping(value = "/hello1") public String hello1(HttpServletResponse response) throws IOException { response.getWriter().print("hello"); return null; } // 无返回值 @RequestMapping(value = "/hello2") public void hello2(HttpServletResponse response) throws IOException { response.getWriter().print("hello"); } 页面展示

注意:如果采用这种方式响应,则和 springmvc.xml 文件中配置的视图解析器没有关系,不走视图解析器了
StringHttpMessageConverter,为什么会启用这个消息转换器呢?@ResponseBody这个注解@Controller public class HelloController { @RequestMapping(value = "/hello") @ResponseBody public String hello(){ // 由于你使用了 @ResponseBody 注解 // 以下的return语句返回的字符串则不再是“逻辑视图名”了 // 而是作为响应协议的响应体进行响应。 return "hello"; } } JSON格式的字符串,可以返回JSON格式的字符串吗?当然可以,代码如下:@Controller public class HelloController { @RequestMapping(value = "/hello") @ResponseBody public String hello(){ return "{\"username\":\"zhangsan\",\"password\":\"1234\"}"; } } 页面展示

StringHttpMessageConverterPOJO对象,怎么将POJO对象以JSON格式的字符串响应给浏览器呢?MappingJackson2HttpMessageConverter消息转换器启动JSON消息转换器需要两个步骤
jackson依赖,可以将java对象转换为json格式字符串 com.fasterxml.jackson.core jackson-databind 2.17.0 注解驱动,会自动装配一个消息转换器:MappingJackson2HttpMessageConverter @ResponseBody最经典用法如下:
@RequestMapping(value = "/hello") @ResponseBody public User hello(){ User user = new User("zhangsan", "18"); return user; } 
@RestController。这一个注解代表了:@Controller + @ResponseBody所有的方法上都会自动标注@ResponseBody@RestController public class HelloController { @RequestMapping(value = "/hello") public User hello(){ User user = new User("zhangsan", "18"); return user; } } 在没有使用这个注解的时候:
@RequestMapping("/save") public String save(User user){ // 执行保存的业务逻辑 userDao.save(user); // 保存成功跳转到成功页面 return "success"; } 当请求体提交的数据是:
username=zhangsan&password=1234&email=zhangsan@powernode.com 那么Spring MVC会自动使用 FormHttpMessageConverter消息转换器,将请求体转换成user对象
当使用这个注解的时候:这个注解只能出现在
方法的参数上
@RequestMapping("/save") public String save(@RequestBody String requestBodyStr){ System.out.println("请求体:" + requestBodyStr); return "success"; } Spring MVC仍然会使用 FormHttpMessageConverter消息转换器,将请求体直接以字符串形式传递给requestBodyStr变量
请求体是JSON格式字符串,可以将其转化为POJO对象MappingJackson2HttpMessageConverterjackson依赖、开启注解驱动@RequestMapping("/send") @ResponseBody public String send(@RequestBody User user){ System.out.println(user); System.out.println(user.getUsername()); System.out.println(user.getPassword()); return "success"; } 请求行、请求头、请求体所有信息@RequestMapping("/send") @ResponseBody public String send(RequestEntity requestEntity){ System.out.println("请求方式:" + requestEntity.getMethod()); System.out.println("请求URL:" + requestEntity.getUrl()); HttpHeaders headers = requestEntity.getHeaders(); System.out.println("请求的内容类型:" + headers.getContentType()); System.out.println("请求头:" + headers); User user = requestEntity.getBody(); System.out.println(user); System.out.println(user.getUsername()); System.out.println(user.getPassword()); return "success"; } 执行结果:

状态行、响应头、响应体@Controller public class UserController { @GetMapping("/users/{id}") public ResponseEntity getUserById(@PathVariable Long id) { User user = userService.getUserById(id); if (user == null) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); } else { return ResponseEntity.ok(user); } } } 异常,跳转到对应的视图,在视图上展示友好信息默认处理器DefaultHandlerExceptionResolver核心方法:

当请求方式和处理方式不同时,DefaultHandlerExceptionResolver的默认处理态度是:

@ControllerAdvice public class ExceptionController { @ExceptionHandler public String exceptionHandler(Exception e, Model model){ model.addAttribute("e", e); return "error"; } } 出错了 出错了,请联系管理员!

@ControllerAdvice public class ExceptionController { @ExceptionHandler(value = {Exception.class}) @ResponseBody public ResponseEntity exceptionHandler(Exception e, Model model) { // 这里先判断拦截到的Exceptiion是不是我们自定义的异常类型 if (e instanceof MyException) { MyException myException = (MyException) e; return ResponseEntity.status(500).body(myException.getMeg()); } else { // 如果拦截的异常不是我们自定义的异常(例如:数据库主键冲突) return ResponseEntity.status(500).body("服务器端异常"); } } } 请求到达控制器之前或之后进行拦截,可以对请求和响应进行一些特定的处理登录验证:对于需要登录才能访问的网址,使用拦截器可以判断用户是否已登录,如果未登录则跳转到登录页面权限校验:根据用户权限对部分网址进行访问控制,拒绝未经授权的用户访问请求日志:记录请求信息,例如请求地址、请求参数、请求时间等,用于排查问题和性能优化更改响应:可以对响应的内容进行修改,例如添加头信息、调整响应内容格式等请求和响应的流程中进行处理,可以修改请求和响应的内容,例如设置编码和字符集、请求头、状态码等控制器进行前置或后置处理,在请求到达控制器之前或之后进行特定的操作,例如打印日志、权限验证等Filter、Servlet、Interceptor、Controller的执行顺序:

定义拦截器
org.springframework.web.servlet.HandlerInterceptor 接口,共有三个方法可以进行选择性的实现preHandle:处理器方法调用之前执行(返回true放行,false拦截)postHandle:处理器方法调用之后执行afterCompletion:渲染完成后执行(无论是否抛异常最终必会执行)@Component public class MyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("处理器方法前调用"); return true; } @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("渲染完成后调用"); } } 基本配置
springmvc.xml配置如下
添加组件扫描

注意:对于这种基本配置来说,拦截器是拦截所有请求的
如果所有拦截器preHandle都返回true
按照springmvc.xml文件中配置的顺序,自上而下调用 preHandle
执行顺序:

如果其中一个拦截器preHandle返回false
如果interceptor2的preHandle返回false,执行顺序:

规则:只要有一个拦截器preHandle返回false,所有postHandle都不执行。但返回false的拦截器的前面的拦截器按照逆序执行afterCompletion。