跳到主要内容

SpringMVC

学习 SpringMVC 时的 GitHub 地址:https://github.com/2022zhang125/SpringMVC.git

主要分享一些关于 SpringMVC 自己的见解,如若有错,欢迎指正。

在学习 SpringMVC 时,我采用了 Thymeleaf 作为模版语言的 All In One 形式学习的。


前端分发器

  • 通过 DispatcherServlet 前端分发器将请求进行分发操作
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0">
<!--前端控制器-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--配置初始化参数,让SpringMVC在classpath中-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</init-param>
<!--让DispatchServlet在服务器启动时自动创建-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

请求方式(@RequestMapping)

后期可以直接使用 PostMapping 这些代替掉。

  • RESTFul 风格的请求格式

    • 所谓 RESTFull 风格,就是将传统的请求/springmvc/login?username=zhangsan&password=123,通过对请求方式来判断执行的操

      也就可以写成/springmvc/login/zhangsan/123就请求的值作为 URL 进行发送。

  • 举例,路径中通过{变量名}然后通过@PathVariable 来进行接收操作

    • @RequestMapping("/login/{username}/{b}")
      public String testRESTFul(@PathVariable("username") String username, @PathVariable("b") String password){
      System.out.println("账号:" + username + ",密码:" + password);
      return "ok";
      }
    提示
    1.占位符使用的是 `{}`

    2.接收参数使用 `@PathVariable("username")......`

    3.其中`@PathVariable` 后要与占位符一致!!!
  • RESTFul 格式的请求准则

    请求方式对应操作
    POST增加
    PUT修改
    GET获取
    DELETE删除
    HEAD获取响应头

后端获取前端数据流程和方式

后端流程

  • 用户直接访问

    • 开启服务,DispatcherServlet持续监听 / (也就是除了 JSP 之外的所有请求)

    • 分发请求到指定的方法上,比如是:/ 请求

    • 通过拼接前后缀使逻辑视图变为物理视图

    • 通过 Thymeleaf 的视图解析器将页面展示

  • 用户点击超链接

    • 用户点击超链接后进行跳转
    • DispatcherServlet检测 /user/register
    • 执行register方法 ----> 拼接 ---->展示。
  • 用户表单提交

    • 用户填写完表单后,点击提交
    • DispatcherServlet检测/user/register /user/register01
    • 执行toRegister方法,通过HttpServletRequest获取表单数据。

后端获取数据方式

  • 使用@RequestParam注解,将请求映射到我们的变量上

    • @RequestMapping(value = "/user/register02",method = RequestMethod.POST)
      public String toRegister02(@RequestParam("username") String username, @RequestParam("password") String password){
      // 使用@RequestParam将这个的value值映射上我们的变量username。
      System.out.println("用户名:" + username + ",密码:" + password);
      return "ok";
      }
  • 使用POJO类作为接受参数

用一个 POJO 对象,该对象存储着此次请求的所有变量,然后让请求映射到我们的 POJO 上即可。

@PostMapping("/user/register02")
public String toRegister02(User user){
System.out.println(user);
return "ok";
}

通过设置字符编码过滤器解决 POST 请求乱码问题

<!--使用SpringMVC自带的编码过滤器来定义请求和响应的字符编码方式-->
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<!--设置编码方式-->
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<!--设置请求必须要使用我们的UTF-8-->
<param-name>forceRequestEncoding</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<!--设置响应必须要使用我们的UTF-8-->
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<!--对所有的请求都过滤-->
<url-pattern>/*</url-pattern>
</filter-mapping>

其实就是配置一个特殊的类,然后设置其属性值。


SpringMVC 对三个域的操作

域操作,一般是能选择小的就选小的,这样会优化空间。

Request 请求域

生命周期:一次请求

  • 获取请求域的几种方式

    • 原生ServletAPI

      • @RequestMapping("/requestServletAPI")
        public String requestServletApi(HttpServletRequest request){
        // 存放数据
        request.setAttribute("requestScope","使用SpringMVC中的原生Servlet API实现一次请求的请求域数据共享");
        return "ok"; // 这里默认是使用转发机制(forward)
        }
    • Map接口

      • @RequestMapping("/requestScopeMap")
        public String requestScopeMap(Map<String, Object> map){
        map.put("requestScope","使用SpringMVC中的Map接口实现一次请求的请求域数据共享");
        return "ok";
        }
    • Model接口

      • @RequestMapping("/requestScopeModel")
        public String requestScopeModel(Model model){
        model.addAttribute("requestScope","使用SpringMVC中的Model接口实现一次请求的请求域数据共享");
        return "ok";
        }
    • ModelMap

      • @RequestMapping("/requestScopeModelMap")
        public String requestModelMap(ModelMap modelMap){
        modelMap.put("requestScope","使用SpringMVC中的ModelMap接口实现一次请求的请求域数据共享");
        return "ok";
        }w
    • ModelAndView

      • @RequestMapping("/requestModelAndView")
        public ModelAndView requestModelAndView(){
        ModelAndView mav = new ModelAndView();
        mav.addObject("requestScope","使用SpringMVC中的ModelAndView类实现一次请求的请求域数据共享");
        // 跳转的视图
        mav.setViewName("ok");
        return mav;
        }
提示
只要是请求,在经过Dispatcher分发器时,都会被封装成一个ModelAndView对象。(适配器模型)

Session 会话域

生命周期:一次浏览器的开启与页面的跳转,只要浏览器不关 Session 不清除,我就在

  • 通过原生 HttpSession 来获取

    • @RequestMapping("/sessionServletAPI")
      public String sessionServletApi(HttpSession session){
      session.setAttribute("sessionScope","在SpringMVC中使用原生Servlet API获取Session域对象并统一会话域数据");
      return "ok";
      }
  • 使用ModelMap类 + @SessionAttributes({"sessionScope"}) 来指定某个 Key 储存到 Session 内

    • @SessionAttributes({"sessionScope"})
      @Controller
      public class SessionScopeTestController {
      @RequestMapping("/sessionModelMap")
      public String sessionModelMap(ModelMap modelMap){
      modelMap.put("sessionScope","在SpringMVC中使用ModelMap接口获取Session域对象并统一会话域数据");
      return "ok";
      }
      }

Application 应用域

生命周期:整个应用各个地方都可以调用,一般是公共元素

提示
获取ServletContext对象的方式(用得少),所以直接用ServletAPI原生的即可
 @RequestMapping("/applicationServletAPI")
public String applicationServletAPI(HttpServletRequest request){
ServletContext context = request.getServletContext();// 这里不能直接将 ServletContext作为参数进行获取,需要通过request域、session域来间接获取。
context.setAttribute("applicationScope","使用原生Servlet API实现应用域数据共享");
return "ok";
}

request ---> ServletContext ---> Application 对象


转发与重定向方式

改变 return 的格式,将其设定为转发(默认)/重定向

转发:return "forward:/b"
重定向:return "redirect:/b" (使用的较多)
提示
如果对于一个只是跳转,而没有任何业务的Controller时,我们可以采用 mvc:view-controller去指定 路径与资源的映射关系,从而简化代码
但是,在springmvc.xml文件中配置时,会导致所有的注解失效,于是,我们需要绑定上 mvc-annotation-driven/> 一起使用。
<mvc:view-controller path="/" view-name="index" />
<mvc:annotation-driven/>

访问服务器静态资源

  • 打开 web 容器自带的 defaultServlet 这个 Servlet 是当我们的 DispatcherServlet 为 404 时,会使用这个 Servlet 去静态资源里面找。

  • 配置静态资源处理

    <mvc:resources mapping="/static/**" location="/static/" />
  • 注意:静态资源一定要放在 webapp 下,不能放在 WEB-INF 下


RESTful 风格的编程

这里着重写一下对于前端,非 GET,POST 请求的其他请求,如何发送?

这里以PUT请求为例

  • 第一步:前提是在 POST 请求之下,就是必须得是 POST 请求

  • 第二步:添加隐藏域

    • <input type="hidden" name="_method" value="put" />
  • 第三步:在 Web.xml 文件中开启过滤器,用于将 POST 请求转为对应的隐藏域请求

    • <!--添加HiddenHttpMethodFilter用于将POST请求转化为PUT、delete请求-->
      <filter>
      <filter-name>hiddenHttpMethodFilter</filter-name>
      <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
      </filter>
      <filter-mapping>
      <filter-name>hiddenHttpMethodFilter</filter-name>
      <url-pattern>/*</url-pattern>
      </filter-mapping>
提示
如果,我们将HiddenHttpMethodFilter 配置在 CharacterEncodingFilter 之上,则会率先调用前者
这样就会导致 后者无法执行原有操作
因为,request.setCharacterEncoding("UTF-8") 的前面不能有 request.getParamter("")操作,这样会导致编码设置出错。
因此,我们需要交换位置。
所以,隐藏过滤器必须要在字符编码过滤器之后配置!!!.

@ResponseBody 与@RequestBody 的使用方法

  • 对于@RequestBody,这是请求体注解,自然而然应该位于方法的形参内,用于将前端的请求体内的内容原封不动的变为 String 传递给后端,用的还是 FormHttpMessageConverter 转换器。

    • public void save(@RequestBody String requestStr){
      System.out.println(requestStr); // key=value&key=value&key=value......
      }
  • 对于@ResponseBody,属于响应体注解,让 return 的结果不在作为逻辑视图名称进行拼接操作,而是直接作为 HTML 正文返回给前端页面进行展示操作。使用的 HttpMessageConverter 是 StringHttpMessageConverter


JSON 与 POJO 对象互相转换

  • 首先,引入 JackSon 的依赖

    • <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.17.0</version>
      </dependency>
  • POJO 转 JSON,直接转换即可

    • @GetMapping("/ajax")
      @ResponseBody
      public User ajax(){
      User user = new User(123,"张三","321");
      return user;
      }
      提示
      注意!这里使用的消息转换器为:**MappingJackson2HttpMessageConverter**
  • JOSN 转 POJO 对象,需要使用@RequestBody 注解

    • 前端

      • let jsonObj = {"name" : "lisi", "password" : '321'}
        Vue.createApp({
        data(){
        return{
        message : ''
        }
        },
        methods : {
        async getMessage(){
        let response = await axios.post([[@{/}]] + 'save',JSON.stringify(jsonObj),{
        headers:{
        "Content-Type" : "application/json"
        }
        })
        this.message = response.data
        }
        }
        }).mount("#app")
    • 后端

      • @Controller
        public class RequestBodyController {
        @ResponseBody
        @PostMapping("save")
        public String save(@RequestBody User user){
        return user;
        }
        }
        // 输出是:User{id=null, name='lisi', password='321'},说明获取到了前端的JSONObj对象并且将其转为了User对象,前提是引入jackson-databind依赖,这里使用的Http消息转换器为:MappingJackson2HttpMessageConverter

上传与下载

代码都是死的,这些基本就写成工具类就行,要的时候调用即可。

  • 前端

    • <form th:action="@{/upload}" enctype="multipart/form-data" method="post">
      文件上传:<input type="file" name="fileName" /> <br />
      <input type="submit" value="上传" />
      </form>
      <br />
提示
编写前端页面,注意表单必须为POST请求,而且enctype必须为:multipart/form-data 而不是:application/x-www-form-urlencoded
  • 后端 XML 文件配置

    • <multipart-config>
      <!--单个文件最大大小-->
      <max-file-size>102400</max-file-size>
      <!--表单上所有文件大小-->
      <max-request-size>102400</max-request-size>
      <!--设置最小上传文件大小-->
      <file-size-threshold>0</file-size-threshold>
      </multipart-config>
  • 后端完整的上传代码

    • @Controller
      public class FileController {
      @ResponseBody
      @PostMapping("/upload")
      public String fileup(@RequestParam("fileName") MultipartFile multipartFile, HttpServletRequest request) throws IOException {
      String originalFilename = multipartFile.getOriginalFilename();
      BufferedInputStream bis = new BufferedInputStream(multipartFile.getInputStream());
      ServletContext application = request.getServletContext();
      String realPath = application.getRealPath("/upload");
      File file = new File(realPath);
      if (!file.exists()) {
      file.mkdirs();
      }
      File destFile = new File(file.getAbsolutePath() + "/" + UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf('.')));
      BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile));
      byte[] bytes = new byte[1024 * 100];
      int readCount = 0;
      if ((readCount = bis.read(bytes)) != -1) {
      bos.write(bytes, 0, readCount);
      }
      bos.flush();
      bos.close();
      bis.close();
      return "ok";
      }
      }
  • 后端完整的下载代码

    • public class FileController{
      @GetMapping("/download")
      public ResponseEntity<byte[]> fileDownload(HttpServletRequest request) throws IOException {
      File file = new File(request.getServletContext().getRealPath("/upload") + "/83b976d1-b07a-4ba5-8dac-02ddbf9d620f.jpg");
      // 响应头的设置
      HttpHeaders headers = new HttpHeaders();
      // 设置响应类型
      headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
      // 设置下载文件名称(固定写法)
      headers.setContentDispositionFormData("attachment",file.getName());

      // 下载文件
      return new ResponseEntity<byte[]>(Files.readAllBytes(file.toPath()),headers, HttpStatus.OK);
      }
      }

拦截器

  • 拦截器与过滤器的区别在于域不同,也就是范围不同,过滤器的范围要大的多,将拦截器涵盖在内

  • 拦截器的具体实现

    • 类去实现 HandlerInterceptor 接口即可,重写方法 preHandlepostHandleafterCompletion

      • @Component
        public class InterceptorTest03 implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("InterceptorTest03's preHandle execute!");
        return true;
        }

        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("InterceptorTest03's postHandle execute!");
        }

        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("InterceptorTest03's afterCompletion execute!");
        }
        }
提示
preHandle:在Controller之前进行拦截,返回True放行,false拦截。
postHandle:在Controller之后执行,不进行拦截操作了。
afterCompletion:在渲染视图结束后执行。
  • 将拦截器挂载到 IoC 即可

    • <mvc:interceptors>
      <mvc:interceptor>
      <!--对多层路径进行拦截-->
      <mvc:mapping path="/**"/>
      <!--排除 /ok 路径,对 /ok 请求不进行拦截-->
      <mvc:exclude-mapping path="/ok"/>
      <!--配置拦截器-->
      <ref bean="interceptorTest01"/>
      </mvc:interceptor>
      </mvc:interceptors>
  • 拦截器的运行流程

    • 如果,其中一个拦截器返回 false,则整个的流程如下
    • 拦截器集合:[I1,I2,I3.....Ii....In]
      • 先说结论:类似于栈的方式,先进后出。
      • 如果有一个拦截器返回 false,则后续拦截器的 postHandler 方法不执行,然后开始倒叙执行afterCompletion方法。、
      • 流程:[preI1,preI2,preI3,preI4......preIi] ---> [aC(i-1),aC(i-2),aC(i-3).....aC3,aC2,aC1]