当前位置: 首页 > news >正文

一天急速通关SpringMVC

一天急速通关SpringMVC

  • 0 文章介绍
    • 1 介绍
    • 1.1 MVC架构与三层架构
    • 1.2 Spring MVC介绍
    • 1.3 入门程序
  • 2 请求的映射
  • 3 请求数据的接收
    • 3.1 @RequestParam接收
    • 3.2 POJO/JavaBean接收
    • 3.3 RequestHeader和CookieValue接收
  • 4 请求数据的传递
  • 5 视图
    • 5.1 视图的理解
    • 5.2 请求转发和响应重定向的使用
    • 5.3 特定资源映射和静态资源映射
  • 6 技巧
    • 6.1 RESTful编程风格
    • 6.2 HTTP消息转换器
    • 6.3 文件上传与下载
    • 6.4 异常处理器
    • 6.5 拦截器
  • 7 全注解开发

0 文章介绍

在倍速观看动力节点杜老师的Spring6MVC教程之后,根据视频内容以及课程笔记进行实践,经过自己的理解并总结后形成这篇学习笔记。文章总共分为六个章节,包括了原教程的十四个章节的大部分知识,学习本文的前置知识需要:JavaSEJDBCMySQLXMLMyBatisSpring6AjaxThymeleaf。本文所提供的信息和内容仅供参考,作者和发布者不保证其准确性和完整性。

1 介绍

1.1 MVC架构与三层架构

MVC架构本质是一种用户界面设计模式,主要用于分离用户界面的展示、数据管理和用户交互逻辑,将应用分为三个核心组件:

  • 模型(Model):应用程序的数据结构和业务逻辑,负责与数据库、文件系统和其他数据源进行交互,获取和处理数据。
  • 视图(View):用户界面的展示部分,负责向用户呈现数据并接收用户的输入数据,不涉及业务逻辑。
  • 控制器(Controller):充当模型和视图之间的中介,处理用户输入并调用模型和视图去完成用户的需求。

理解: 前端浏览器发送请求给Web服务器,Web服务器中的Controller接收到用户的请求,Controller负责将前端提交的数据进行封装,然后Controller调用Model来处理业务,当Model处理完业务后会返回处理之后的数据给ControllerController再调用View来完成数据的展示,最终将结果响应给浏览器,浏览器进行渲染展示页面。

"Uses"
"Updates"
"Used by"
"Used by"
Model
+ data: any
+getData()
+updateData(data: any)
View
+display(data: any)
Controller
+ model: Model
+ view: View
+request(request: any)

三层架构(Three-Tier Architecture)是一种分层架构,为了实现高内聚低耦合,提高系统的可维护性和可扩展性,将应用分为三个逻辑层:

  • 表示层(Presentation Layer:负责用户界面的展示,接收用户输入并展示处理结果。
  • 业务逻辑层(Business Logic Layer:处理业务规则,封装业务逻辑,组合数据访问层的基本功能,形成复杂的业务逻辑功能。
  • 数据访问层(Data Access Layer:负责与数据库进行交互,执行数据的增删改查操作。

理解: 关注点和MVC不一样,关注点在于系统逻辑分离。

"Sends request to"
"Sends request to"
"Returns data to"
"Returns data to"
PresentationLayer
+handleRequest(request: any)
BusinessLogicLayer
+processRequest(request: any)
DataAccessLayer
+saveData(data: any)
+getData()

MVC 架构在实际应用中,模型部分可能会包含复杂的业务逻辑和数据访问操作,为了更好地组织和解耦代码,将 MVC 中的模型层进一步细分为三层架构中的业务逻辑层(Service)和数据访问层(DAO),三层架构中的表示层负责接收用户请求并展示处理结果,其实就对应MVC架构的视图层(View)和控制层(Controller)。

"Updates"
"Uses"
"Includes"
"Uses"
"Returns data to"
"Returns data to"
Model
+ data: any
+getData()
+updateData(data: any)
View
+display(data: any)
Controller
+ model: Model
+ view: View
+request(request: any)
Service
+processRequest(request: any)
DAO
+saveData(data: any)
+getData()

理解:MVC 架构本质上是一种用户界面设计模式,核心目标是分离用户界面的展示、数据和交互逻辑,本身并不涉及应用程序的底层架构设计,如数据存储、业务逻辑处理等,而这些通常是通过其他设计模式或架构(如三层架构、微服务架构等)来实现。通过结合两者,MVC 架构专注于用户界面的设计和交互,而三层架构则提供了更清晰的系统逻辑分离和模块化设计,使得应用程序更加易于维护和扩展。

1.2 Spring MVC介绍

SpringMVC是一个实现了MVC架构模式的Web框架,底层基于Servlet实现,之前学习JavaWeb的时候学了Servlet的一些操作,现在这个框架能够更方便的实现这些操作,同时还支持Spring6IoCAOP,具体的点有:

  • 通过DispatcherServlet作为入口控制器,负责接收请求和分发请求。Servlet需要手动编写Servlet程序。
  • 表单提交时可以通过简单的操作就能自动将表单数据绑定到相应的JavaBean对象中。Servlet需要手动获取。
  • 通过IoC容器管理对象,只需要在配置文件中进行相应的配置即可获取实例对象。Servlet需要手动创建。
  • 提供了拦截器、异常处理器等统一处理请求的机制,并且可以灵活地配置这些处理器。Servlet需要手动过滤器、异常处理器等。
  • 提供了多种视图模板,如JSPFreemarkerVelocity等,并且支持国际化、主题等特性。Servlet需要手动处理视图层。

总结:简化了很多操作的同时提供了更多的功能和扩展性。

1.3 入门程序

0 创建Maven Java Web项目 -> 1 配置pom.xml依赖 -> 2 配置web.xml -> 3 配置springmvc-servlet.xml -> 4 编写Java代码与视图。

  • 环境准备

    • IntelliJ IDEA:2024.1.7

    • Navicat for MySQL:17.1.2

    • MySQL:8.0.26

    • JDK:17.0.2

    • Maven:3.9.1

  • 设置pom.xml的打包方式以及添加依赖,打包方式设置为war

    <groupId>com.cut</groupId>
    <artifactId>spring-mvc-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <!-- 打包方式设置为war方式 -->
    <packaging>war</packaging>
    <dependencies>
        <!-- Spring MVC依赖 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>6.1.14</version>
        </dependency>
        <!--日志框架Logback依赖-->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.5.3</version>
        </dependency>
        <!--Servlet依赖-->
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>6.0.0</version>
            <scope>provided</scope>
        </dependency>
        <!--Spring6和Thymeleaf整合依赖-->
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring6</artifactId>
            <version>3.1.2.RELEASE</version>
        </dependency>
    </dependencies>
    
  • 配置web.xml文件,主要配置SpringMVC的前端控制器DispatcherServlet,让除了JSP的所有请求都走这个控制器。

    <?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_6_0.xsd"
             version="6.0">
        <!--SpringMVC提供的前端控制器-->
        <servlet>
            <servlet-name>springmvc</servlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    
            <!--手动设置springmvc配置文件的路径及名字-->
            <init-param>
                <param-name>contextConfigLocation</param-name>
                <param-value>classpath:springmvc.xml</param-value>
            </init-param>
    
            <!--为了提高用户的第一次访问效率,建议在web服务器启动时初始化前端控制器-->
            <load-on-startup>6</load-on-startup>
        </servlet>
        <servlet-mapping>
            <servlet-name>springmvc</servlet-name>
            <!-- /* 表示任何一个请求都交给DispatcherServlet来处理 -->
            <!-- / 表示当请求不是xx.jsp的时候,DispatcherServlet来负责处理本次请求-->
            <url-pattern>/</url-pattern>
        </servlet-mapping>
    </web-app>
    
  • 配置springmvc-servlet.xml,SpringMVC框架的配置文件,名字默认为:<servlet-name>-servlet.xml,默认存放的位置是WEB-INF根目录。

    如果web.xmlDispatcherServletservlet-namespringmvc,并且WEB-INF根目录下有springmvc-servlet.xml(短横线前面为servlet-name)的配置文件,就不用设置contextConfigLocation,自动加载,如果没有,则需要手动加载,这边在resources目录下也创建了springmvc.xml,内容一样,手动加载这个文件,没有使用默认路径下默认名称的springmvc-servlet.xml,作为演示。

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
                               http://www.springframework.org/schema/beans/spring-beans.xsd
                               http://www.springframework.org/schema/context
                               https://www.springframework.org/schema/context/spring-context.xsd">
        <!--组件扫描, 扫描注解-->
        <context:component-scan base-package="com.cut.springmvc"/>
    
        <!--视图解析器-->
        <bean id="thymeleafViewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
            <property name="characterEncoding" value="UTF-8"/>
            <!--如果配置多个视图解析器,它来决定优先使用哪个视图解析器,它的值越小优先级越高-->
            <property name="order" value="1"/>
            <property name="templateEngine">
                <bean class="org.thymeleaf.spring6.SpringTemplateEngine">
                    <property name="templateResolver">
                        <bean class="org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver">
                            <property name="prefix" value="/WEB-INF/templates/"/>
                            <property name="suffix" value=".html"/>
                            <!--设置模板类型,例如:HTML,TEXT,JAVASCRIPT,CSS等-->
                            <property name="templateMode" value="HTML"/>
                            <property name="characterEncoding" value="UTF-8"/>
                        </bean>
                    </property>
                </bean>
            </property>
        </bean>
    </beans>
    
  • 编写控制器com.cut.springmvc.controller.IndexController.java

    package com.cut.springmvc.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    /**
     * @author tang
     * @version 1.0.0
     * @since 1.0.0
     */
    @Controller
    public class IndexController {
        @RequestMapping("/")
        public String toIndex() {
            return "index";
        }
    
        @RequestMapping("/hello")
        public String toHello() {
            // 返回逻辑视图名称(决定跳转到哪个页面)
            return "hello";
        }
    }
    
  • 编写视图WEB-INF/templates/index.html,WEB-INF/templates/hello.html

    <!DOCTYPE html>
    <!--指定 th 命名空间,让 Thymeleaf 标准表达式可以被解析和执行-->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Index</title>
    </head>
    <body>
    <!-- Thymeleaf检测到以 / 开始,表示绝对路径,自动会将webapp的上下文路径加上去 -->
    <!-- 最终的效果是:href="/springmvc/hello" -->
    <a th:href="@{/hello}">点击进入hello页面</a>
    </body>
    </html>
    
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Hello</title>
    </head>
    <body>
    <h1>Hello Spring MVC</h1>
    </body>
    </html>
    
  • 配置Tomcat并访问即可。

2 请求的映射

SpringMVC中,@RequestMapping 注解是控制器映射注解,用于将请求映射到相应的处理方法上,即将指定URL的请求绑定到一个特定的方法或类上,从而实现对请求的处理和响应,可以用在类或者方法上。

  • 类属性和方法属性结合使用

    同一个webapp中,RequestMapping必须具有唯一性,即value不能重复,同一个Controller或者不同Controller中不能出现相同的映射。

    @Controller
    public class UserController {
        @RequestMapping("/detail")
        public String toDetail(){
            return "detail";
        }
    }
    
    @Controller
    public class ProductController {
        @RequestMapping("/detail")
        public String toDetail(){
            return "detail";
        }
    }
    

    这种情况应该要加上前缀,区分两个detail映射,如何加,两种方式,一是方法上直接加前缀。

    @RequestMapping("/user/detail")
    public String toDetail(){
        return "/user/detail";
    }
    
    @RequestMapping("/product/detail")
    public String toDetail(){
        return "/product/detail";
    }
    

    二是在类上面加。

    @Controller
    @RequestMapping("/user")
    public class UserController {
        @RequestMapping("/detail")
        public String toDetail(){
            return "/user/detail";
        }
    }
    
    @Controller
    @RequestMapping("/product")
    public class ProductController {
        @RequestMapping("/detail")
        public String toDetail(){
            return "/product/detail";
        }
    }
    
    
  • RequestMappingvalue属性

    value属性是请求路径,通过该请求路径与对应的控制器的方法绑定,是一个字符串数组,可以映射多个路径。还支持模糊匹配路径,称之为Ant风格

    • ?,代表任意一个字符
    • *,代表0N个任意字符
    • **,代表0N个任意字符,路径中可以有分隔符 /,只能出现在路径的末尾。

    请求路径中还可以使用占位符,配合@PathVariable注解接收请求传递的参数

    @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 "testRESTful";
    }
    
    <!--测试RequestMapping注解的value属性支持占位符-->
    <a th:href="@{/testRESTful/1/zhangsan/20}">测试value属性使用占位符</a>
    

    之前学习的请求路径应该是下面这样的

    http://localhost:8080/springmvc/login?username=admin&password=123&age=20
    

    上面代码的请求路径是RESTful风格的,在现代的开发中使用较多,因此请求路径变为

    http://localhost:8080/springmvc/login/admin/123/20
    
  • RequestMappingmethod属性

    这个属性用于限定请求方式,如果请求方式和method值不一致会出现405错误,下面演示了要求前端必须得是POST请求访问/login路径。两种方式,一种是RequestMapping,另一种直接使用PostMapping,其他请求方式用法一致。

    //@RequestMapping(value="/login", method = RequestMethod.POST)
    @PostMapping("/login")
    public String testMethod(){
        return "testMethod";
    }
    

    请求方式主要有下面的九种:

    • GETURL 中或请求头中传递参数,获取资源,只允许读取数据,不影响数据的状态和功能。
    • POST:通过表单等方式提交请求体提交资源。
    • PUT:通过请求体发送,更新指定的资源上所有可编辑内容。
    • DELETE:将要被删除的资源标识符放在 URL 中或请求体中用于删除指定的资源。
    • HEAD:类似 GET 命令,但是所有返回的信息都是头部信息,不能包含数据体。
    • PATCH:当被请求的资源是可被更改的资源时,每次更新一部分。
    • OPTIONS:请求获得服务器支持的请求方法类型,以及支持的请求头标志。
    • TRACE:服务器响应输出客户端的 HTTP 请求,主要用于调试和测试。
    • CONNECT:建立网络连接,通常用于加密 SSL/TLS 连接。

    注意:超链接发送的请求是get请求,form表单只能提交getpost请求,putdeletehead请求可以使用发送ajax请求的方式来实现,form表单的method如果不是get或者post,按照get请求处理。

    比较常用的getpost请求的区别:

    • get请求适合从服务器端获取数据,只能发送普通字符串,且长度有限制,请求的数据会回显到地址栏,支持缓存,如果存在缓存,就不会再发送到服务器,如果要避免,建议请求路径后加时间戳。

    • post请求适合向服务器端传递数据,可以发送任何数据,数据在请求体中,不会回显,不支持缓存,每次请求都走服务器。

  • RequestMappingparams属性

    用于限定请求传递的方式必须和设置的一致,否则发生400错误,对于@RequestMapping(value="/login", params={}) ,匹配规则如下:

    • params={**"username"**, "password"},必须包含 usernamepassword
    • params={**"!username"**, "password"},不能包含username,但必须包含password
    • params={**"username=admin"**, "password"},必须包含值是adminusername参数,必须包含password
    • params={**"username!=admin"**, "password"},不能包含值是adminusername参数,必须包含password
  • RequestMappingheaders属性

    params原理相同,用法也相同。请求头信息后端要求的不一致,则出现404错误

  • 状态码

    400 Bad Request:请求无效,请求格式错误或缺少必要的参数。

    404 Not Found:请求的资源未找到。

    405 Method Not Allowed:请求方法不被允许。

3 请求数据的接收

请求的数据接收主要用到@RequestParamPOJO/JavaBean来进行接收,还有RequestHeader@CookieValue用于接收请求头和Cookie

3.1 @RequestParam接收

对于@RequestParam来说,主要有三个属性vauenamerequireddefaultValue,其中namevalue本质一样。

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";

    boolean required() default true;

    String defaultValue() default "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n";
}

注册页面和注册成功页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>用户注册</title>
</head>
<body>
<div class="container">
    <h3>用户注册</h3>
    <hr>
    <form th:action="@{/success}" method="post">
        <label for="username">用户名:</label>
        <input type="text" id="username" name="username" required>

        <label for="password">密码:</label>
        <input type="password" id="password" name="password" required>

        <label>性别:</label>
        <label><input type="radio" name="sex" value="1"></label>
        <label><input type="radio" name="sex" value="0"></label>

        <label>爱好:</label>
        <label><input type="checkbox" name="hobby" value="smoke"> 抽烟</label>
        <label><input type="checkbox" name="hobby" value="drink"> 喝酒</label>
        <label><input type="checkbox" name="hobby" value="perm"> 烫头</label>

        <label for="intro">简介:</label>
        <textarea id="intro" rows="10" cols="60" name="intro"></textarea>

        <input type="submit" value="注册">
    </form>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Register Success</title>
</head>
<body>
<h1>注册成功!</h1>
</body>
</html>

RegisterController

package com.cut.springmvc.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * @author tang
 * @version 1.0.0
 * @since 1.0.0
 */
@Controller
public class RegisterController {
    @RequestMapping("/success")
    public String toSuccess(
            @RequestParam(value = "username", required = true)
            String username,
            @RequestParam(value = "password", required = false)
            String password,
            @RequestParam(value = "sex")
            String sex,
            @RequestParam(value = "hobby")
            String[] hobby,
            @RequestParam(name = "intro")
            String intro,
            @RequestParam(value = "otherParam", required = false, defaultValue = "default value")
            String otherParam
    ) {
        return "success";
    }

    @RequestMapping("/register")
    public String toRegister(
    ) {
        return "register";
    }
}

关于几个属性的说明:

  • 混用namevalue或者省略,都可以成功接收参数。
  • required默认情况为 true,表示请求参数必需,如果缺少会抛出异常。设置为false如果请求中缺少则方法的参数值为null
  • defaultValue设置形参的默认值,当没有提供对应的请求参数或者请求参数的**值是空字符串""**的时候,方法的形参会采用默认值。

注意,@RequestParam 可以省略的,请求参数和方法形参的名字相同,则可以省略,如果形参有请求参数没有的,为null

3.2 POJO/JavaBean接收

当提交的数据非常多时,方法的形参个数会非常多,能直接使用POJO/JavaBean来接收吗,可以的,但是有个要求,实体类得含有对应请求参数名一样的set方法,属性名可以不一致,但是set方法得一致。

private Long ID;
public void setId(Long id) {
    this.ID = id;
}
// 请求参数中有id
// 那么实体类这样也是可以的,只要setId对应id即可,属性ID名称没关系
@PostMapping("/register")
public String register(User user){
    System.out.println(user);
    return "success";
}

3.3 RequestHeader和CookieValue接收

@GetMapping("/register")
public String register(
    User user,
    @RequestHeader(value="Referer", required = false, defaultValue = "")
    String referer,
    @CookieValue(value="id", required = false, defaultValue = "1")
    String id){
    return "success";
}

4 请求数据的传递

之前在学习Servlet的时候,通过setAttribute + getAttribute方法来完成在域中数据的传递和共享。回顾Servlet的三个域对象:

域对象作用范围生命周期使用场景
应用域ServletContext全局范围Web应用启动到停止全局变量、配置信息
会话域HttpSession会话范围用户会话开始到结束用户登录信息、购物车
请求域HttpServletRequest请求范围请求开始到结束请求参数、临时数据
  • 请求域对象中共享数据方式有四种,前三者本质都是一样的BindingAwareModelMap

    • Model接口,model.addAttribute(key,value)

      @RequestMapping("/testModel")
      public String testModel(Model model){
          model.addAttribute("testRequestScope", "在SpringMVC中使用Model接口实现request域数据共享");
          return "view";
      }
      
    • Map接口,map.put(key,value)

      @RequestMapping("/testMap")
      public String testMap(Map<String, Object> map){
          map.put("testRequestScope", "在SpringMVC中使用Map接口实现request域数据共享");
          return "view";
      }
      
    • ModelMap接口,modelMap.addAttribute(key,value),直接添加对应类型的形参使用即可。

      @RequestMapping("/testModelMap")
      public String testModelMap(ModelMap modelMap){
          modelMap.addAttribute("testRequestScope", "在SpringMVC中使用ModelMap实现request域数据共享");
          return "view";
      }
      
    • ModelAndView接口

      这个类的实例封装了Model和View也就是说这个类既封装业务处理之后的数据,也体现了跳转到哪个视图。不是添加形参,而是在方法里new

      @RequestMapping("/testModelAndView")
      public ModelAndView testModelAndView(){
          ModelAndView modelAndView = new ModelAndView();
          // 绑定数据
          modelAndView.addObject("testRequestScope", "在SpringMVC中使用ModelAndView实现request域数据共享");
          // 绑定视图
          modelAndView.setViewName("view");
          return modelAndView;
      }
      
  • 会话域对象中共享数据方式

    直接在Controller类上使用@SessionAttributes注解,标注了当keyx 或者 y 时,数据将被存储到会话session中,如果没有 SessionAttributes注解,默认存储到request域中。

    @Controller
    @SessionAttributes(value = {"x", "y"})
    public class SessionScopeTestController {
    
        @RequestMapping("/testSessionScope2")
        public String testSessionAttributes(ModelMap modelMap){
            // 向session域中存储数据
            modelMap.addAttribute("x", "我是埃克斯");
            modelMap.addAttribute("y", "我是歪");
            return "view";
        }
    }
    
  • 应用域对象中共享数据方式

    直接使用Servlet API即可

    @Controller
    public class ApplicationScopeTestController {
        @RequestMapping("/testApplicationScope")
        public String testApplicationScope(HttpServletRequest request){
            // 获取ServletContext对象
            ServletContext application = request.getServletContext();
            // 向应用域中存储数据
            application.setAttribute("applicationScope", "我是应用域当中的一条数据");
            return "view";
        }
    }
    

5 视图

5.1 视图的理解

视图(View)是MVC架构中的表示层组件,负责将模型(Model)中的数据呈现给用户,并处理用户交互,之前在 springmvc.xml配置的,就是Thymeleaf视图解析器ThymeleafViewResolver

<bean id="thymeleafViewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
...
</bean>

除了Thymeleaf以外,还有其他的视图:

  • InternalResourceView:内部资源视图(Spring MVC框架内置的,专门为JSP模板语法准备的)
  • RedirectView:重定向视图(Spring MVC框架内置的,用来完成重定向效果)
  • ThymeleafViewThymeleaf视图(第三方的,为Thymeleaf模板语法准备的)
  • FreeMarkerViewFreeMarker视图(第三方的,为FreeMarker模板语法准备的)
  • VelocityViewVelocity视图(第三方的,为Velocity模板语法准备的)
  • PDFViewPDF视图(第三方的,专门用来生成pdf文件视图)
  • ExcelViewExcel视图(第三方的,专门用来生成excel文件视图)

SpringMVC如何实现视图机制的呢,主要有下面几个核心接口

  • 负责中央调度的前端控制器DispatcherServlet,核心方法为doDispatch
  • 将逻辑视图名转换为物理视图名的视图解析器ViewResolver,核心方法为resolveViewName,返回视图实现类对象。
  • 将模板语言转换成HTML代码的视图View,核心代码为render
  • 将视图解析器注册到web容器中的视图解析器注册器ViewResolverRegistry,按order顺序放入到List集合中。

理解: 浏览器发送请求给web服务器,DispatcherServlet接收到请求并根据请求路径分发到对应的Controller并调用,Controller返回逻辑视图名给DispatcherServletDispatcherServlet再调用ThymeleafViewResolverresolveViewName方法,将逻辑视图名转换为物理视图名,并获取ThymeleafView对象,最后DispatcherServlet调用ThymeleafViewrender方法,render方法将模板语言转换为HTML代码,响应给浏览器,完成最终的渲染。

5.2 请求转发和响应重定向的使用

关于请求转发和响应重定向在SpringMVC中的使用,回顾之前Servlet中的使用

request.getRequestDispatcher("/index").forward(request, response);
response.sendRedirect("/webapproot/index");

两个的区别:

  • 转发是一次请求,服务器端内部的行为,地址栏不变化,不能实现跨域,但是可以访问WEB-INF下受保护的目录。
  • 重定向是两次请求,既可以完成服务端内部资源跳转也可以完成跨域跳转,地址栏会发生变化,无法访问WEB-INF下受保护的目录。注意第一次发送请求,服务端会将重定向的地址返回给浏览器,浏览器再对这个地址发送请求。

Spring MVC中控制器的返回值解析有下面三种情况:

  • 返回值以 "forward:" 开头,Spring MVC 会执行请求转发到指定的路径(/path)。

    @RequestMapping("/a")
    public String toA(){
        return "forward:/b";
    }
    

    一共创建了两个视图对象,转发源:InternalResourceView,转发目的地:ThymeleafView

  • 返回值以 "redirect:" 开头,Spring MVC 会执行 HTTP 重定向到指定的路径(/path)。

    @RequestMapping("/a")
    public String toA(){
        return "redirect:/b";
    }
    

    一共创建了两个视图对象,重定向源:RedirectView,重定向目的地:ThymeleafView

  • 返回值是除了前两种情况的字符串,Spring MVC 会将其视为视图名,通过视图解析器来解析对应的视图资源。

5.3 特定资源映射和静态资源映射

<mvc:view-controller> 配置用于将某个请求映射到特定的视图上,即指定某一个 URL 请求到一个视图资源的映射,使得这个视图资源可以被访问。当Controller中只是简单返回视图名的话,可以直接使用这个配置,例如首页之类的资源映射。

<mvc:view-controller path="/" view-name="index" />
<!-- 使用了上面的需要再手动开启SpringMVC的注解,不然会发生404错误 -->
<mvc:annotation-driven/>

关于静态资源的映射,比较简单的方式就是:

<!-- 开启注解驱动 -->
<mvc:annotation-driven />
<!-- 配置静态资源处理,请求路径是"/static/"开始的,都会去"/static/"目录下找该资源。 -->
<mvc:resources mapping="/static/**" location="/static/" />

也可以使用默认Servlet处理,直接开启默认Servlet处理静态资源功能,同一个请求路径,先走DispatcherServlet,如果找不到则走默认的Servlet

<!-- 开启注解驱动 -->
<mvc:annotation-driven />
<!--开启默认Servlet处理-->
<mvc:default-servlet-handler>

6 技巧

主要包括了RESTful编程风格,HTTP消息转换器,文件上传下载,异常处理器,拦截器等方面的知识。

6.1 RESTful编程风格

RESTfulRepresentational State Transfer,表述性状态转移)是WEB服务接口的一种设计风格,定义了一组约束条件和规范用于让WEB服务接口更加简洁、易于理解、易于扩展、安全可靠。其中:

  • 表述性(Representational)是:URI + 请求方式。
  • 状态(State)是:服务器端的数据。
  • 转移(Transfer)是:变化。
  • 表述性状态转移是指:通过URI + 请求方式来控制服务器端数据的变化。
传统的 URLRESTful URL
GET /getUserById?id=1GET /user/1
GET /getAllUserGET /user
POST /addUserPOST /user
POST /modifyUserPUT /user
GET /deleteUserById?id=1DELETE /user/1

如何通过表单来提交这些请求呢,之前说过表单只能提交get或者post请求,这里就得需要针对请求做一个处理,例如put请求:

<!--修改用户-->
<hr>
<form th:action="@{/api/user}" method="post">
    <!--隐藏域的方式提交 _method=put -->
    <input type="hidden" name="_method" value="put">
    用户名:<input type="text" name="username"><br>
    <input type="submit" th:value="修改">
</form>

表面上是post请求,服务端接收到后通过HiddenHttpMethodFilter判断是否为post请求,如果是post请求,调用request.getParameter(this.methodParam)this.methodParam的值是"_method",对应名称为"_method"input标签,只有请求方式是put/delete/patch的时候会创建HttpMethodRequestWrapper对象将methodpost变成put/delete/patch

<!--隐藏的HTTP请求方式过滤器-->
<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>

注意:在web.xml文件中,应该先配置CharacterEncodingFilter,然后再配置HiddenHttpMethodFilter,因为HiddenHttpMethodFilter使用了request.getParameter(),避免乱码问题。

6.2 HTTP消息转换器

HTTP消息转换器HttpMessageConverter的主要作用是完成HTTP协议Java程序中的对象之间的互相转换,主要学习下面几个注解和类的使用:

  • @ResponseBody:将控制器方法的返回值直接写入 HTTP 响应体中。

    @Controller
    public class MyController {
        // StringHttpMessageConverter
        @GetMapping("/hello")
        @ResponseBody
        public String hello() {
            return "Hello, World!";
        }
    	
        //MappingJackson2HttpMessageConverter
        @PostMapping("/user")
        @ResponseBody
        public User createUser() {
            // 处理用户数据
            return new User(); // 返回 JSON 格式的用户数据
        }
    }
    

    这里只要有@ResponseBody注解,那么返回的就不是视图的逻辑名称了,而是响应体,上面演示了两个消息转换器,一个是StringHttpMessageConverter,另一个是MappingJackson2HttpMessageConverterPOJO对象转换成JSON格式的字符串,响应给前端,使用MappingJackson2HttpMessageConverter得添加依赖

    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.17.0</version>
    </dependency>
    

    并在配置文件中开启注解

    <mvc:annotation-driven/>
    

    如果这个Controller类中全是响应体,那么可以在类上使用RestController,就不用每个方法都添加@ResponseBody

    @RestController
    public class MyController {
        // StringHttpMessageConverter
        @GetMapping("/hello")
        public String hello() {
            return "Hello, World!";
        }
    	
        //MappingJackson2HttpMessageConverter
        @PostMapping("/user")
        public User createUser() {
            // 处理用户数据
            return new User(); // 返回 JSON 格式的用户数据
        }
    }
    
  • @RequestBody:将 HTTP 请求体中的数据绑定到控制器方法的参数上。

    @RestController
    public class MyController {
        // FormHttpMessageConverter将请求体转换成user对象
        @PostMapping("/user")
        public String hello(User user) {
    		System.out.println(user)
            return "success";
        }
    	
        // FormHttpMessageConverter将请求体转换成user字符串
        @PostMapping("/userStr")
        public User createUser(@RequestBody String userStr) {
            System.out.println("请求体:" + userStr);
            return "success";
        }
    }
    

    那还有一种组合呢

    // FormHttpMessageConverter将请求体的JSON字符串转换成POJO对象
    @PostMapping("/user")
    public User createUser(@RequestBody User user) {
        System.out.println("请求体:" + userStr);
        return "success";
    }
    

    总结:

    • User user:Form表单请求体用POJO接收
    • @RequestBody User user:Json请求体转换为POJO
    • @RequestBody String user:Form表单请求体转换为字符串
  • RequestEntity:用于封装 HTTP 响应的完整信息,包括状态码、响应头和响应体。直接在形参中添加即可使用。

    @RequestMapping("/send")
    @ResponseBody
    public String send(RequestEntity<User> 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";
    }
    
  • RequestEntity:用于封装 HTTP 请求的完整信息,包括请求方法、请求头和请求体,将返回值类型变为对应的ResponseEntity对象即可,有啥用呢:前端提交一个id,后端根据id进行查询,如果返回null,请在前端显示404错误。如果返回不是null,则输出返回的user

    @Controller
    public class UserController {
        @GetMapping("/users/{id}")
        public ResponseEntity<User> 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);
            }
        }
    }
    

6.3 文件上传与下载

  • 文件的上传

    html文件上传表单,enctype="multipart/form-data"

    <!--文件上传表单-->
    <form th:action="@{/file/up}" method="post" enctype="multipart/form-data">
        文件:<input type="file" name="fileName"/><br>
        <input type="submit" value="上传">
    </form>
    

    前端控制器配置multipart-config

    <!--前端控制器-->
    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
        <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>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    

    Controller,主要就是文件的写入操作,注意@RequestParam("fileName") MultipartFile multipartFile的理解

    @RequestMapping(value = "/file/up", method = RequestMethod.POST)
    public String fileUp(@RequestParam("fileName") MultipartFile multipartFile, HttpServletRequest request) throws IOException {
        String name = multipartFile.getName();
        System.out.println(name);
        // 获取文件名
        String originalFilename = multipartFile.getOriginalFilename();
        System.out.println(originalFilename);
        // 将文件存储到服务器中
        // 获取输入流
        InputStream in = multipartFile.getInputStream();
        // 获取上传之后的存放目录
        File file = new File(request.getServletContext().getRealPath("/upload"));
        // 如果服务器目录不存在则新建
        if(!file.exists()){
            file.mkdirs();
        }
        // 开始写
        //BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file.getAbsolutePath() + "/" + originalFilename));
        // 可以采用UUID来生成文件名,防止服务器上传文件时产生覆盖
        BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file.getAbsolutePath() + "/" + UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."))));
        byte[] bytes = new byte[1024 * 100];
        int readCount = 0;
        while((readCount = in.read(bytes)) != -1){
            out.write(bytes,0,readCount);
        }
        // 刷新缓冲流
        out.flush();
        // 关闭流
        in.close();
        out.close();
    
        return "ok";
    }
    
  • 文件的下载即可

    <!--文件下载-->
    <a th:href="@{/download}">文件下载</a>
    

    注意返回类型

    @GetMapping("/download")
    public ResponseEntity<byte[]> downloadFile(HttpServletResponse response, HttpServletRequest request) throws IOException {
        File file = new File(request.getServletContext().getRealPath("/upload") + "/1.jpeg");
        // 创建响应头对象
        HttpHeaders headers = new HttpHeaders();
        // 设置响应内容类型
        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        // 设置下载文件的名称
        headers.setContentDispositionFormData("attachment", file.getName());
    
        // 下载文件
        ResponseEntity<byte[]> entity = new ResponseEntity<byte[]>(Files.readAllBytes(file.toPath()), headers, HttpStatus.OK);
        return entity;
    }
    

6.4 异常处理器

Spring MVC 中,异常处理是一个非常重要的功能,用于捕获和处理控制器方法中抛出的异常,从而向客户端返回友好的错误信息。实际就是当异常发生了,决定返回什么的操作。

主要有两个异常处理的方式,一个是方法级别的@ExceptionHandler,一个是类级别的@ControllerAdvice ,这里结合之前学习的ResponseEntity使用。

  • @ExceptionHandler,方法上捕获特定异常或者所有异常。
@RestController
public class MyController {

    @GetMapping("/test")
    public String test() {
        throw new IllegalArgumentException("测试异常");
    }

    // 捕获特定异常
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException ex) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("发生非法参数异常:" + ex.getMessage());
    }

    // 捕获所有异常
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleException(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("发生未知错误:" + ex.getMessage());
    }
}
  • @ControllerAdvice,统一管理异常,Advice通知,之前AOP的术语之一应该就明白什么意思了,面向切面编程,业务Controller就只关注核心业务。

    @ControllerAdvice
    public class GlobalExceptionHandler {
        // 捕获特定异常
        @ExceptionHandler(IllegalArgumentException.class)
        public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException ex) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("发生非法参数异常:" + ex.getMessage());
        }
    
        // 捕获所有异常
        @ExceptionHandler(Exception.class)
        public ResponseEntity<String> handleException(Exception ex) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("发生未知错误:" + ex.getMessage());
        }
    }
    

6.5 拦截器

Spring MVC的拦截器作用是在请求到达控制器之前或之后进行拦截,可以对请求和响应进行一些特定的处理。下面是拦截器在整个请求到返回过程中的运行位置。

Filter 链完成
返回阶段
Spring 框架
Servlet 容器
客户端
FilterN doFilter完成
返回响应到客户端
Filter2 doFilter完成
Filter1 doFilter完成
请求完成
渲染页面
Interceptor2 afterCompletion
Interceptor1 afterCompletion
Servlet分发到Spring
请求到达Servlet
Interceptor1 preHandle
Interceptor2 preHandle
Controller处理请求
返回ModelAndView
Interceptor2 postHandle
Interceptor1 postHandle
执行Filter链
Servlet容器接收请求
Filter1 doFilter
Filter2 doFilter
FilterN doFilter
客户端请求
视图解析器解析视图
客户端接收响应

可以看到有三个核心方法,preHandle,postHandle,afterCompletion,Interceptor是后于Filer的执行的。

客户端发请求,Servlet容器接收,然后执行FiltersdoFilter方法,之后进入Spring,执行InterceptorspreHandle后进入Controller并返回ModelAndView,之后可以执行InterceptorspostHandle,再进行视图解析,渲染,完成后执行InterceptorsafterCompletion方法,最后返回响应到客户端并完成Filters链。

SpringMVC中配置拦截器的方式

<mvc:interceptors>
    <bean class="com.powernode.springmvc.interceptors.MyInterceptor"/>
</mvc:interceptors>
<!-- 下面这种需要配置包扫描并使用 @Component 注解进行标注 -->
<mvc:interceptors>
    <ref bean="myInterceptor"/>
</mvc:interceptors>

这种简单的配置会拦截所有请求,如何自定义呢,下面表示除 /test 请求路径之外,剩下的路径全部拦截。

<mvc:interceptors>
    <mvc:interceptor>
        <!--拦截所有路径-->
        <mvc:mapping path="/**"/>
        <!--除 /test 路径之外-->
        <mvc:exclude-mapping path="/test"/>
        <!--拦截器-->
        <ref bean="myInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

如果有多个拦截器,他们的三个方法的执行顺序呢

<mvc:interceptors>
    <ref bean="interceptor1"/>
    <ref bean="interceptor2"/>
</mvc:interceptors>

preHandles顺序执行,postHandle逆序执行,afterCompletion逆序执行

Interceptor1.preHandle()  // 请求阶段,按顺序执行
Interceptor2.preHandle()  // 请求阶段,按顺序执行
Controller处理请求       // 请求到达Controller
Interceptor2.postHandle() // 返回阶段,逆序执行
Interceptor1.postHandle() // 返回阶段,逆序执行
视图解析与渲染           // 视图解析器渲染页面
Interceptor2.afterCompletion() // 完成阶段,逆序执行
Interceptor1.afterCompletion() // 完成阶段,逆序执行

注意: 只要有一个拦截器preHandle返回false,任何postHandle都不执行。但返回false的拦截器的前面(不包括当前)的拦截器按照逆序执行afterCompletion

Interceptor1.preHandle()  // 请求阶段,按顺序执行
Interceptor2.preHandle()  // 请求阶段,按顺序执行,返回false,阻止后续处理
Interceptor1.afterCompletion() // 完成阶段,逆序执行

7 全注解开发

其实就web.xmlspringmvc.xml怎么通过类配置

Spring3.1后提供的API
Servlet3.0新特性
Tomcat服务器
SpringServletContainerInitializer 类
WebApplicationInitializer 接口
AbstractAnnotationConfigDispatcherServletInitializer 类
ServletContainerInitializer 接口
Tomcat服务器
自定义web.xml配置类
  • web.xml的配置文件需要继承AbstractAnnotationConfigDispatcherServletInitializer
import jakarta.servlet.Filter;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[0];
    }
	
    // 指定Spring MVC配置类
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{SpringmvcConfig.class}; 
    }
	
    // 指定DispatcherServlet的映射路径
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"}; 
    }
    
	// 配置过滤器,characterEncodingFilter先于hiddenHttpMethodFilter
    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter("UTF-8");
        characterEncodingFilter.setForceRequestEncoding(true);
        characterEncodingFilter.setForceResponseEncoding(true);
        HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
        return new Filter[]{characterEncodingFilter, hiddenHttpMethodFilter};
    }
}
  • springmvc.xml配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

// 指定该类是一个配置类,可以当配置文件使用
@Configuration
// 指定扫描的包路径
@ComponentScan("com.example.controller") 
// 开启Spring MVC注解支持
@EnableWebMvc 
public class SpringmvcConfig implements WebMvcConfigurer {
    // 配置视图解析器
    @Bean
    public ThymeleafViewResolver getViewResolver(SpringTemplateEngine springTemplateEngine) {
        ThymeleafViewResolver resolver = new ThymeleafViewResolver();
        resolver.setTemplateEngine(springTemplateEngine);
        resolver.setCharacterEncoding("UTF-8");
        resolver.setOrder(1);
        return resolver;
    }

    @Bean
    public SpringTemplateEngine templateEngine(ITemplateResolver iTemplateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(iTemplateResolver);
        return templateEngine;
    }

    @Bean
    public ITemplateResolver templateResolver(ApplicationContext applicationContext) {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setApplicationContext(applicationContext);
        resolver.setPrefix("/WEB-INF/thymeleaf/");
        resolver.setSuffix(".html");
        resolver.setTemplateMode(TemplateMode.HTML);
        resolver.setCharacterEncoding("UTF-8");
        resolver.setCacheable(false);//开发时关闭缓存,改动即可生效
        return resolver;
    }

    // 配置静态资源处理
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
	
    // view-controller特殊映射
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/test").setViewName("test");
    }
    
    // 配置拦截器,这里的MyInterceptor需要自己写
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**").excludePathPatterns("/test");
    }

    // 配置异常处理器
    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();
        Properties properties = new Properties();
        properties.setProperty("java.lang.Exception", "error");
        exceptionResolver.setExceptionMappings(properties);
        exceptionResolver.setExceptionAttribute("ex");
        resolvers.add(exceptionResolver);
    }
}

相关文章:

  • 算法08-递归调用转为循环的通用方法
  • Lua闭包的使用以及需要注意的问题
  • RadASM环境,win32汇编入门教程之二
  • 技术评测:MaxCompute MaxFrame——阿里云自研分布式计算框架的Python编程接口
  • 第四十四篇--Tesla P40+Janus-Pro-7B部署与测试
  • CI/CD部署打包方法
  • 2.11寒假
  • SiliconCloud 支持deepseek,送2000w token
  • 使用 Nginx 搭建代理服务器(正向代理 HTTPS 网站)指南
  • 剑指offer第2版:搜索算法(二分/DFS/BFS)
  • 算法练习——哈希表
  • Python实现从SMS-Activate平台,自动获取手机号和验证码(进阶版2.0)
  • 前端包管理器的发展以及Npm、Yarn和Pnpm对比
  • AWTK fscript 中的 TCP/UDP 客户端扩展函数
  • C++课程设计 运动会分数统计(含源码)
  • 打开游戏缺少C++组件怎么修复?缺少C++组件问题的解决方法
  • FastAPI 高并发与性能优化
  • XXL-Job源码分析
  • 2024春秋杯网络安全联赛冬季赛wp
  • Jenkins+gitee 搭建自动化部署
  • “多规合一”改革7年成效如何?自然资源部总规划师亮成绩单
  • 国家统计局向多省份反馈统计督察意见
  • 李成钢出席中国与《数字经济伙伴关系协定》成员部级会议
  • 中拉互联网发展与合作论坛在西安开幕
  • 上海高院与上海妇联签协议,建立反家暴常态化联动协作机制
  • 苏轼“胡为适南海”?