SpringMVC初体验

  • 新建Maven项目
  • pom.xml文件导包
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.powernode</groupId>
<artifactId>SpringMVC</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>springMVC-001</module>
<module>springmvc-hellomvc</module>
<module>springmvc-002</module>
<module>springMVC-003</module>
</modules>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.1.7</version>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring6</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.1.0</version>
<!--指定servlet-api有tomcat容器提供,打包时将不会将servlet-api包加入打包中-->
<scope>provided</scope>
</dependency>
<!--过时了 <dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.3</version>
</dependency>
</dependencies>
</project>
  • 新建maven模块springmvc-002
    在springmvc-002模块中的pom.xml中新增打包方式为war
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SpringMVC</artifactId>
<groupId>com.powernode</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>springmvc-002</artifactId>
<packaging>war</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
  • 在springmvc-002模块上右击,新增框架支持,如图所示:
  • 补充:从Java EE 8开始,Servlet API的维护权转交给了Eclipse Foundation的Jakarta EE社区。因此,在最新的Jakarta EE版本中,Servlet API的包名已经从javax.servlet更改为了jakarta.servlet。这意味着jakarta.servlet-api是Servlet API在Jakarta EE 9(以及后续版本)中的新命名形式。
  • 添加完web框架后会自动生成web目录,如图所示
  • 编辑web.xml文件
<?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>
<!--Servlet初始化参数,指定springmvc配置文件和位置-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<!--在web服务器启动时,就启动DispatcherServlet,优化-->
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
  • 新增并编辑spring配置文件
<?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.powernode.springmvc.controller"/>
<!--配置视图解析器-->
<bean id="viewResolver" 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"/>
<!--设置模板类型-->
<property name="templateMode" value="HTML"/>
<!--设置模板文件在读取和解析过程中采用的编码字符集-->
<property name="characterEncoding" value="utf-8"/>
</bean>
</property>
</bean>
</property>
</bean>
</beans>
  • 在com.powernode.springmvc.controller新增IndexController类
package com.powernode.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class IndexController {
@RequestMapping("/")
public String index(){
return "index";
}
@RequestMapping("/first")
public String first(){
System.out.println("业务逻辑");
return "first";
}
}
  • 在WEB-INF目录下新增templates目录,并在此目录下新增firstm.html文件和index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>First HTML PAGE</h1>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h1>首页</h1>
<a th:href="@{/first}">First Spring MVC Code!</a>
</body>
</html>
  • 启动tomcat10,访问http://localhost:8080/ 效果图

    单击First Spring MVC Code!后打开下图

获取请求数据的几种方法

  1. 使用原生的Servlet API进行获取
  • 补充: Servlet接口是Java Servlet API中的一个核心接口,它定义了Servlet的生命周期方法以及用于处理HTTP请求和响应的方法。一个Servlet是运行在Web服务器或Servlet容器(如Apache Tomcat、Jetty等)中的Java程序,用于扩展服务器的功能。
    Servlet接口中定义的主要方法有:
    init(ServletConfig config): 当Servlet被加载到容器中时调用,用于初始化Servlet。
    service(ServletRequest req, ServletResponse res): 这是Servlet处理请求的主要方法,但通常我们不直接实现它,而是重写doGet()、doPost()等特定HTTP方法的方法。
    destroy(): 当Servlet不再需要时(例如,服务器关闭或重新加载Servlet时)调用,用于释放资源。
    ServletRequest接口代表客户端发送的HTTP请求。它定义了一系列方法来获取请求信息,如请求头、请求参数、请求方法、请求路径等。
  • 在SpringMVC当中,一个Controller类中的方法参数上如果有HttpServletRequest,SpringMVC会自动将当前请求对象传递给这个参数,因此我们可以通过这个参数来获取请求提交的数据。测试一下。
  • 前端准备注册页面register.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户注册</title>
</head>
<body>
<!--注册页面-->
<form th:action="@{/user/reg}" method="post">
用户名: <input type="text" name="username"><br>
密码: <input type="password" name="password"><br>
性别:
<input type="radio" name="sex" value="1">
<input type="radio" name="sex" value="0">
<br>
兴趣:
抽烟<input type="checkbox" name="interest" value="smoke">
喝酒<input type="checkbox" name="interest" value="drink">
烫头:<input type="checkbox" name="interest" value="perm">
<br>
简介:
<textarea cols="60" rows="10" name="intro"></textarea>
<br>
<input type="submit" value="注册"/>
</form>
</body>
</html>
  • 准备测试成功页面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ok</title>
</head>
<body>
<h1>Test OK!</h1>
</body>
</html>
  • 准备Controller类,UserController
package com.powernode.springmvc.controller;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Arrays;
@Controller
public class UserController {
@RequestMapping("/")
public String toRegister(){
return "register";
}
/*第一种方法原生的Servlet API*/
/*@RequestMapping(value = "/user/reg",method = {RequestMethod.POST})
public String register(HttpServletRequest request, HttpServletResponse response, HttpSession session){
//获取请求提交的数据
String username = request.getParameter("username");
String password = request.getParameter("password");
String sex = request.getParameter("sex");
String[] interest = request.getParameterValues("interest");
String intro = request.getParameter("intro");
System.out.println(username);
System.out.println(password);
System.out.println(sex);
System.out.println(Arrays.toString(interest));
System.out.println(intro);
return "ok";
}*/
/*第二种方法:通过注解@RequestParam*/
/*@RequestMapping(value = "/user/reg",method = {RequestMethod.POST})
public String register(
@RequestParam("username")
String username, //变量名可以自定义
@RequestParam("password")
String password,
@RequestParam(value = "sex",defaultValue = "1")
Integer sex,
@RequestParam("interest")
String[] interest,
@RequestParam("intro")
String intro){
System.out.println(username);
System.out.println(password);
System.out.println(sex);
System.out.println(Arrays.toString(interest));
System.out.println(intro);
return "ok";
}*/
/*第三种方法(不常用):如果方法形参的名字和提交数据时的name相同,则 @RequestParam 可以省略。*/
/*@RequestMapping(value = "/user/reg",method = {RequestMethod.POST})
public String register(
String username, //此处变量名如果写错,那么前端提交后值为null
String password,
Integer sex,
String[] interest,
String intro){
System.out.println(username);
System.out.println(password);
System.out.println(sex);
System.out.println(Arrays.toString(interest));
System.out.println(intro);
return "ok";
}*/
/*第四种方法:POJO类/JavaBean接收请求参数,当提交的数据非常多时,方法的形参个数会非常多,这不是很好的设计。在SpringMVC中也可以使用POJO类来接收请求参数。要求POJO类的属性名必须和请求参数的参数名保持一致*/
@RequestMapping(value = "/user/reg",method = {RequestMethod.POST})
public String register(User user,
@RequestHeader(value = "Referer", required = false,defaultValue ="" )
String referer){
System.out.println(user);
System.out.println(referer);
return "ok";
}
}

其中@RequestHeader是将请求头信息中的Referer映射到形参referer上,前端提交后后台能看到Referer对应的值.
POJO类如下:

package com.powernode.springmvc.pojo;
import java.util.Arrays;
public class User {
private Long id;
private String username;
private String password;
private Integer sex;
private String[] interest;
private String intro;
private String age;
/* public User() {
}*/
public User(Long id, String username, String password, Integer sex, String[] interest, String intro, String age) {
this.id = id;
this.username = username;
this.password = password;
this.sex = sex;
this.interest = interest;
this.intro = intro;
this.age = age;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getSex() {
return sex;
}
public void setSex(Integer sex) {
this.sex = sex;
}
public String[] getInterest() {
return interest;
}
public void setInterest(String[] interest) {
this.interest = interest;
}
public String getIntro() {
return intro;
}
public void setIntro(String intro) {
this.intro = intro;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", sex=" + sex +
", interest=" + Arrays.toString(interest) +
", intro='" + intro + '\'' +
", age='" + age + '\'' +
'}';
}
}

第三种方法有一个前提:如果你采用的是Spring6+版本,需要在该模块的pom.xml文件中指定编译参数'-parameter',配置如下:

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>17</source>
<target>17</target>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>

分别测试以上方法,启动tomcat,在浏览器打开效果图:

输入选项后能够跳转到ok页面,可以通过浏览器F12查看提交的表单数据,也可以在idea控制台看到所填的值

如果前端有cookie,后端可以通过@CookieValue获取前端提交的cookie,比如id(此处的id是测试在前端提前生成好的),通过Get方式获得.

@GetMapping(value = {"/user/reg"})
public String register(User user,
@RequestHeader(value = "Referer", required = false,defaultValue ="" )
String referer,
@CookieValue(value = "id",required = false,defaultValue = "")
String id){
System.out.println(user);
System.out.println(referer);
System.out.println("客户端提交过来的CookieID值"+id);
return "ok";
}

---

2024年7月5日

request域对象

接口名:HttpServletRequest
简称:request
request对象代表了一次请求。一次请求一个request。
使用请求域的业务场景:在A资源中通过转发的方式跳转到B资源,因为是转发,因此从A到B是一次请求,如果想让A资源和B资源共享同一个数据,可以将数据存储到request域中。

  1. 在上面的父项目中创建模块springmvc-005,刷新新模块中的pom.xml,引用父项目中依赖.
  2. 添加web支持.spring6中添加web依赖

  3. 编写web.xml
<?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">
<!--字符编码过滤器-->
<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>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--配置前端控制器-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc-servlet.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
  1. 创建IndexController
package com.powernode.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class IndexController {
@RequestMapping("/")
public String index(){
return "index";
}
}
  1. 创建spring配置文件并编辑
<?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.powernode.springmvc.controller"/>
<!--视图解析器-->
<bean id="viewResolver" 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"/>
<!--设置模板类型-->
<property name="templateMode" value="HTML"/>
<!--设置模板文件在读取和解析过程中采用的编码字符集-->
<property name="characterEncoding" value="utf-8"/>
</bean>
</property>
</bean>
</property>
</bean>
</beans>
  1. 编写index.html,都测试完成后写的文档,所以index.html页面后后面测试的内容.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>测试三个域对象</h1>
<h2>测试request域对象</h2>
<a th:href="@{/testServletAPI}">测试在SpringMVC当中使用原生Servlet API,完成request域共享</a><br>
<h2>测试Model接口</h2>
<a th:href="@{/testModel}">测试在SpringMVC当中使用Model接口,完成request域共享</a><br>
<h2>测试Map接口</h2>
<a th:href="@{/testMap}">测试在SpringMVC当中使用Map接口,完成request域共享</a><br>
<h2>测试ModelMap接口</h2>
<a th:href="@{/testModelMap}">测试在SpringMVC当中使用ModelMap接口,完成request域共享</a><br>
<h2>测试ModelAndView</h2>
<a th:href="@{/testModelAndView}">测试在SpringMVC当中使用ModelAndView类,完成request域共享</a><br>
</body>
</html>
  1. 创建转发成功访问的ok.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ok</title>
</head>
<body>
<div th:text="${testRequestScope}"></div>
</body>
</html>

  • 在SpringMVC中,在request域中共享数据有以下几种方式:
  1. 使用原生HttpServletRequestAPI方式。
  2. 使用Model接口。
  3. 使用Map接口。
  4. 使用ModelMap类。
  5. 使用ModelAndView类。
package com.powernode.springmvc.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import java.util.Map;
@Controller
public class RequestScopeTestController {
@RequestMapping("/testServletAPI")
public String testServletAPI(HttpServletRequest request){
//将共享的数据存储到request域当中
request.setAttribute("testRequestScope","在SpringMVC中使用原生ServletAPI完成request域数据共享");
return "ok";
}
@RequestMapping("/testModel")
public String testModel(Model model){
//向request域当中绑定数据
model.addAttribute("testRequestScope","在SpringMVC中使用Model接口完成request域数据共享");
System.out.println(model);
System.out.println(model.getClass().getName());
return "ok";
}
@RequestMapping("/testMap")
public String testMap(Map<String,Object> map){
//向request域当中存数据
map.put("testRequestScope","在SpringMVC中使用Map接口完成request域数据共享");
System.out.println(map);
System.out.println(map.getClass().getName());
//转发
return "ok";
}
@GetMapping("/testModelMap")
public String testMap(ModelMap modelMap){
//向request域当中存数据
modelMap.addAttribute("testRequestScope","在SpringMVC中使用ModelMap接口完成request域数据共享");
System.out.println(modelMap);
System.out.println(modelMap.getClass().getName());
//转发
return "ok";
}
@RequestMapping("/testModelAndView")
public ModelAndView testModelAndView(){
//创建模型视图
ModelAndView modelAndView = new ModelAndView();
//给模型视图绑定数据
modelAndView.addObject("testRequestScope","在SpringMVC中使用ModelAndView接口完成request域数据共享");
//给模型视图绑定视图
modelAndView.setViewName("ok");
System.out.println("获取ModelAndView类名称"+modelAndView.getClass().getName());
return modelAndView;
}
}
  • 通过测试无论是Model、Map还是ModelMap,底层实例化的对象都是:BindingAwareModelMap


  • BindingAwareModelMap 补充:
  1. 内部实现类:BindingAwareModelMap 是 Spring MVC 内部用于实现 Model、ModelMap 接口和 Map<String, Object> 的具体类。

  2. 数据传递:在 Spring MVC 中,控制器方法可以通过参数接收 Model、ModelMap 或 Map 类型的对象,并向其中添加数据。这些数据最终会通过 BindingAwareModelMap 实例进行管理和传递,最终到达视图层。

  3. 请求域绑定:BindingAwareModelMap 中的数据默认会绑定到 HTTP 请求的作用域(request scope)中,使得视图层可以方便地访问这些数据。

  4. 不要直接使用:尽管 BindingAwareModelMap 提供了许多功能,但开发者通常不需要(也不应该)直接在代码中创建或操作 BindingAwareModelMap 的实例。相反,应该通过 Model、ModelMap 或 Map 接口来与模型数据进行交互。

  5. 作用域:默认情况下,BindingAwareModelMap 中的数据会绑定到 HTTP 请求的作用域中。这意味着这些数据只在当前请求的生命周期内有效。如果需要跨多个请求共享数据,可以考虑使用其他作用域(如 session scope)或数据存储机制(如数据库)。


  • 但是在SpringMVC框架中为了更好的体现MVC架构模式,提供了一个类:ModelAndView。这个类的实例封装了Model和View。也就是说这个类既封装业务处理之后的数据,也体现了跳转到哪个视图。使用它也可以完成request域数据共享。需要注意:
  1. 方法的返回值类型不是String,而是ModelAndView对象。
  2. ModelAndView不是出现在方法的参数位置,而是在方法体中new的。
  3. 需要调用addObject向域中存储数据。
  4. 需要调用setViewName设置视图的名字。
  • 在Spring MVC中,ModelAndView 是用于将模型数据和视图名称封装在一起的一个类。ModelAndView 本身并不直接使用 BindingAwareModelMap,但确实使用了一个模型映射(Model Map)来存储模型数据。
    BindingAwareModelMap 是Spring MVC内部使用的一个类,它扩展了 ModelMap(后者又扩展了 LinkedHashMap),并添加了一些额外的功能,主要是与数据绑定相关的
    在 ModelAndView 的上下文中,你通常不会直接看到 BindingAwareModelMap 的使用,因为 ModelAndView 提供了 addObject、addAllObjects 等方法来向模型中添加数据,这些方法会委托给其内部的模型映射(通常是 ModelMap 的一个实例,但不一定是 BindingAwareModelMap)。
    但是,在某些情况下,Spring MVC内部可能会使用 BindingAwareModelMap 来处理与数据绑定相关的场景。例如,在处理带有表单提交的请求时,如果使用了 @ModelAttribute 注解,并且表单验证失败,Spring MVC可能会使用 BindingAwareModelMap 来存储表单数据以及验证错误。
    总的来说,虽然 ModelAndView 不直接使用 BindingAwareModelMap,但Spring MVC在处理与数据绑定相关的场景时可能会使用它。在大多数情况下,你不需要直接关注这一点,只需要使用 ModelAndView 来封装你的模型数据和视图名称即可。

session域对象

在SpringMVC中使用session域共享数据,实现方式有多种,其中比较常见的两种方式:

  1. 使用原生HttpSession API
  2. 使用SessionAttributes注解
  • 在上面基础上创建SessionScopeController类,分别包括原生的和SessionAttributes注解
package com.powernode.springmvc.controller;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
@Controller
@SessionAttributes(value = {"X","Y"})
public class SessionScopeController {
@RequestMapping("/testSessionServletAPI")
public String testServletAPI(HttpServletRequest request, HttpServletResponse response, HttpSession session){
//处理核心业务...
//将数据存储到session中
session.setAttribute("testSessionScope","测试在SpringMVC当中使用原生HttpSessionAPI,完成request域共享");
//返回逻辑视图名称
return "ok";
}
@RequestMapping("/testSessionAttribute")
public String testSessionAttribute(ModelMap modelMap){
//处理业务
//将数据存储到session中
modelMap.addAttribute("X","我是X");
modelMap.addAttribute("Y","我是Y");
return "ok";
}
}
注意:@SessionAttributes注解使用在Controller类上。标注了当key是 x 或者 y 时,数据将被存储到会话session中。如果没有 SessionAttributes注解,默认存储到request域中。
  • 在index.html添加如下
<h2>测试session域对象</h2>
<a th:href="@{/testSessionServletAPI}">测试在SpringMVC当中使用HttpSession接口,完成session域共享</a><br>
<a th:href="@{/testSessionAttribute}">测试在SpringMVC当中使用@SessionAttributes注解,完成session域共享</a><br>
  • 在ok页面添加如下:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ok</title>
</head>
<body>
<div th:text="${testRequestScope}"></div>
<div th:text="${session.testSessionScope}">
</div>
<p th:text="${session.X}"></p>
<p th:text="${session.Y}"></p>
</body>
</html>

RESTFul编程风格

RESTFul是WEB服务接口的一种设计风格。
RESTFul定义了一组约束条件和规范,可以让WEB服务接口更加简洁、易于理解、易于扩展、安全可靠。
REST对请求方式的约束是这样的:
● 查询必须发送GET请求
● 新增必须发送POST请求
● 修改必须发送PUT请求
● 删除必须发送DELETE请求

REST对URL的约束是这样的:
● 传统的URL:get请求,/springmvc/getUserById?id=1
● REST风格的URL:get请求,/springmvc/user/1

● 传统的URL:get请求,/springmvc/deleteUserById?id=1
● REST风格的URL:delete请求, /springmvc/user/1
传统的 URL 与 RESTful URL 的区别是传统的 URL 是基于方法名进行资源访问和操作,而 RESTful URL 是基于资源的结构和状态进行操作的。下面是一张表格,展示两者之间的具体区别:

  1. 模拟get请求
  • 在父模块下创建springmvc-006模块,新增web框架支持
  • web.xml
<?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">
<!--字符编码过滤器在最上面,优先执行-->
<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>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--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>
<!--前端控制器-->
<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:dispatcherServlet-servlet.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
  • resources目录下dispatcher-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"
xmlns:mvc="http://www.springframework.org/schema/mvc"
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
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--组件扫描-->
<context:component-scan base-package="com.powernode.springmvc.controller"/>
<!--视图解析器-->
<bean id="viewResolver" 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"/>
<!--设置模板类型-->
<property name="templateMode" value="HTML"/>
<!--设置模板文件在读取和解析过程中采用的编码字符集-->
<property name="characterEncoding" value="utf-8"/>
</bean>
</property>
</bean>
</property>
</bean>
<!--开启注解驱动-->
<mvc:annotation-driven/>
<!--视图控制器-->
<mvc:view-controller path="/" view-name="index"/>
</beans>
  • UserController类
@Controller
public class UserController {
@RequestMapping(value = "/user",method = RequestMethod.GET)
public ModelAndView modelAndView(){
ModelAndView modelAndView = new ModelAndView();
System.out.println("正在查询用户所有信息");
modelAndView.setViewName("ok");
return modelAndView;
}
@RequestMapping(value = "/user/{id}",method = RequestMethod.GET)
public String getById(@PathVariable(value = "id")String id){
System.out.println("正在根据用户id查询用户信息...用户id是"+id);
return "ok";
}
  • index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h1>测试RESTFul编程风格</h1>
<hr>
<!--RESTFul风格的,查看用户列表-->
<a th:href="@{/user}">查看用户列表</a><br>
<!--RestFul风格,根据用户id查询用户信息-->
<a th:href="@{/user/110}">查询id是1的用户信息</a><br>
<!--RESTFul风格的,新增用户信息,用post请求-->
<form th:action="@{/user}" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
年龄:<input type="text" name="age"><br>
<input type="submit" name="保存">
</form>
<!--RESTFul风格的,修改用户信息,用put请求,发送put请求先用post请求-->
<form th:action="@{/user}" method="post">
<!--隐藏域-->
<input type="hidden" name="_method" value="put">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
年龄:<input type="text" name="age"><br>
<input type="submit" name="修改">
</form>
<!--RESTFul风格删除用户信息
发送delete请求的前提是发送post请求,通过隐藏域提交_method=delete-->
<a th:href="@{/user/120}" onclick="dele(event)">删除用户id为120的用户信息</a>
<form id="deleForm" method="post">
<input type="hidden" name="_method" value="delete">
</form>
<script>
function dele(event){
//获取表单
let deleForm = document.getElementById("deleForm");
//给form的action赋值
deleForm.action = event.target.href;
//发送POST请求提交表单
deleForm.onsubmit();
//阻止超链接的默认行为
event.preventDefault();
}
</script>
</body>
</html>
  • ok.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ok</title>
</head>
<body>
<h1>OK!</h1>
</body>
</html>
  • 启动tomcat10测试


    测试:查询id是1的用户信息
  1. 模拟post请求
  • 在UserController类中新增
@RequestMapping(value = "/user",method = RequestMethod.POST)
public String save(User user){
System.out.println("正在保存用户信息");
System.out.println(user);
return "ok";
}
  • 新增User实体类
package com.powernode.springmvc.bean;
public class User {
private String username;
private String password;
private Integer age;
public User(String username, String password, Integer age) {
this.username = username;
this.password = password;
this.age = age;
}
public User() {
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", age=" + age +
'}';
}
}
  • 启动tomcat测试


    后台控制台输出信息:
    正在保存用户信息
    User
  1. 模拟PUT请求
    对于PUT请求,前提是一个POST请求。然后在发送POST请求的时候,提交这样的数据:_method=PUT,最后在web.xml文件配置SpringMVC提供的过滤器:HiddenHttpMethodFilter
  • 在UserController类中新增
@RequestMapping(value = "/user",method = RequestMethod.PUT)
public String modify(User user){
System.out.println("正在修改用户信息: "+ user);
return "ok";
}
  • 配置过滤器,前面已经在dispatcherServlet-servlet.xml中写了
  • index.html页面的配置
  • 启动tomat测试

  • idea控制台输出:
    正在修改用户信息: User
  1. 模拟delete请求
  • 在UserController类中新增
@RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE)
public String del(@PathVariable("id") String id){
System.out.println("正在删除用户");
return "ok";
}
  • index.html页面
  • 启动tomcat测试

  • @ResponseBody注解
    @ResponseBody 是 Spring MVC 中的一个注解,可以直接将控制器(Controller)的方法返回的对象自动转换为字符串,并写入到 HTTP 响应(HttpServletResponse)的 body 中。这样,前端就可以直接接收到这些数据
@Controller
public class AjaxController {
/* @GetMapping("/ajax")
public String ajax(HttpServletResponse response) throws IOException {
PrintWriter writer = response.getWriter();
writer.print("hello ajx,My name is SpringMVC");
return null;
}*/
@GetMapping("/ajax")
@ResponseBody
public String ajax(){
return "hello ajx,My name is SpringMVC";
}
}

@ResponseBody注解等同于原生的

  • 实体类转换为json
package com.powernode.springmvc.bean;
public class User {
private Long id;
private String name;
private String password;
public User(Long id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
public User() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", password='" + password + '\'' +
'}';
}
}

修改AjaxController类

@RequestMapping(value = "/ajax", method = RequestMethod.GET)
@ResponseBody
public User ajax(){
User user = new User(1122L, "张三", "123");
return user;
}

前端Vue3+axios+Thymeleaf代码:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
<!--引入vue-->
<!-- <script th:src="@{/static/js/main.js}"></script>-->
<script src="https://cdn.jsdelivr.net/npm/vue@3.x/dist/vue.global.js"></script>
<!--引入axios-->
<!-- <script th:src="@{/static/js/axios.js}"></script>-->
<script th:src="@{https://cdn.staticfile.org/axios/0.27.2/axios.min.js}"></script>
</head>
<body>
<h1>发送Vue3+axios+Thymeleaf+springmvc发送Ajax请求</h1>
<hr>
<div id="app">
<button @click="getMessage">获取消息</button>
<h1>{{message}}</h1>
</div>
<script th:inline="javascript">
Vue.createApp({
data(){
return{
message: ''
}
},
methods: {
//异步方法
async getMessage(){
//发送ajax请求
let response = await axios.get('/ajax')
//将返回的数据交给message
this.message = response.data
}
}
}).mount("#app")
</script>
</body>
</html>

启动tomcat,前端通过访问/ajax,产生的效果:

补充:
MappingJackson2HttpMessageConverter是Spring框架中的一个HTTP消息转换器,主要用于在Java对象和JSON之间进行转换。
MappingJackson2HttpMessageConverter依赖于Jackson库来实现Java对象与JSON之间的序列化和反序列化。
jackson-databind是Jackson库的核心包之一,它提供了数据绑定的功能,即在Java对象和JSON之间进行转换。
所以pom.xml需要导入jackson-databind包

<!--专门负责将java对象转换成json字符串的包,也可以互转-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
@RestController

在类上添加@RestController 等于 类上添加@Controller + 在每个返回数据的方法上添加@ResponseBody,因为 在类上添加@RestController 默认所有方法都带有 @ResponseBody

@RequestBody

这个注解的作用是直接将请求体传递给Java程序,在Java程序中可以直接使用一个String类型的变量接收这个请求体的内容。

  1. 将前端页面中的form表单提交后后台转成java对象测试:
    编写User类
package com.powernode.springmvc.bean;
public class User {
private Long id;
private String name;
private String password;
public User(Long id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
public User() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", password='" + password + '\'' +
'}';
}
}

编写RequestBodyController类

package com.powernode.springmvc.controller;
import com.powernode.springmvc.bean.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
//@Controller
@RestController
public class RequestBodyController {
@PostMapping("/save")
public String save(@RequestBody String requestBodyStr){
System.out.println(requestBodyStr);
return "ok";
}
/* @PostMapping("/save")
public String save(User user){
System.out.println(user);
return "ok";
}*/

上面的两个方法都可以获取index.html页面中提交的表单信息

<form th:action="@{/save}" method="post">
用户名:<input type="text" name="name"/>
密码:<input type="password" name="password">
<input type="submit" value="保存"/>
</form>

通过@RequestBody注解获取的格式是name=alice&password=222222222,Springmvc使用FormHttpMessageConverter消息转换器,将请求体转换成user对象。
通过pojo类的方法获取的格式时User{id=null, name='江疏影', password='222222222'}

  1. 在请求体中提交的是一个JSON格式的字符串,这个JSON字符串传递给Spring MVC之后,将JSON字符串转换成POJO对象。
    在index.html页面新增测试代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
<!--引入vue-->
<!-- <script th:src="@{/static/js/main.js}"></script>-->
<script src="https://cdn.jsdelivr.net/npm/vue@3.x/dist/vue.global.js"></script>
<!--引入axios-->
<!-- <script th:src="@{/static/js/axios.js}"></script>-->
<script th:src="@{https://cdn.staticfile.org/axios/0.27.2/axios.min.js}"></script>
</head>
<body>
<h1>发送Vue3+axios+Thymeleaf+springmvc发送Ajax请求</h1>
<hr>
<form th:action="@{/save}" method="post">
用户名:<input type="text" name="name"/>
密码:<input type="password" name="password">
<input type="submit" value="保存"/>
</form>
<div id="app">
<button @click="getMessage">获取消息</button>
<h1>{{message}}</h1>
</div>
<script th:inline="javascript">
Vue.createApp({
data(){
return{
message: ''
}
},
methods: {
//异步方法
async getMessage(){
//发送ajax请求
let response = await axios.get('/ajax')
//将返回的数据交给message
this.message = response.data
}
}
}).mount("#app")
</script>
<div id="app1">
<button @click="getMessage">提交post</button>
<h1>{{message1}}</h1>
</div>
<!--发送ajax post请求,并提交json-->
<script th:inline="javascript">
let jsonObj = {"name" : "zhangsan","password":"123456"}
Vue.createApp({
data1(){
return{
message: ''
}
},
methods: {
//异步方法
async getMessage(){
//发送ajax请求
let response = await axios.post('/save2',JSON.stringify(jsonObj),{
headers : {
"Content-Type": "application/json"
}
})
//将返回的数据交给message
this.message1 = response.data1
}
}
}).mount("#app1")
</script>
</body>
</html>
ON.stringify(jsonObj),{
headers : {
"Content-Type": "application/json"
}
})
//将返回的数据交给message
this.message1 = response.data1
}
}
}).mount("#app1")
</script>

在RequestBodyController中新增

// @RequestMapping(value = {"/save2"}, method= RequestMethod.POST)
@PostMapping(value = "/save2")
public String saveUser(@RequestBody User user){
System.out.println(user);
return "ok";
}

重启tomcat,打开浏览器测试,点击"提交post"

idea后台控制台显示User{id=null, name='zhangsan', password='123456'},该用户信息是在index.html提交定义好的,如图所示:

RequestEntity类

RequestEntity 是 Spring Framework 的 org.springframework.http.RequestEntity 类,它用于表示一个带有请求头、请求体和 URL 的 HTTP 请求。RequestEntity 通常在发送 HTTP 请求时使用
获取index.html中的post请求信息

在RequestBodyController类中新增

@PostMapping(value = "/save2")
public String saveUser(RequestEntity<User> requestEntity) {
//请求方法
HttpMethod method = requestEntity.getMethod();
System.out.println(method);
//请求URL
URI url = requestEntity.getUrl();
System.out.println(url);
//请求头
HttpHeaders headers = requestEntity.getHeaders();
System.out.println("请求头"+headers);
//获取请求头中的内容信息
MediaType contentType = headers.getContentType();
System.out.println("请求头中的内容信息"+contentType);
//请求体
User body = requestEntity.getBody();
System.out.println(body);
return "ok";
}

重启tomcat,启动浏览器,点击"提交post",在ideal后台控制中心获取请求的信息,如图所示:

ResponseEntity类

ResponseEntity不是注解,是一个类。用该类的实例可以封装响应协议,包括:状态行、响应头、响应体。也就是说:如果你想定制属于自己的响应协议,可以使用该类。
假如我要完成这样一个需求:前端提交一个id,后端根据id进行查询,如果返回null,请在前端显示404错误。如果返回不是null,则输出返回的user。

  • 编辑index.html
<a th:href="@{/user/2}">查找id=1的用户信息</a>
  • 编写UerController类
package com.powernode.springmvc.controller;
import com.powernode.springmvc.bean.User;
import com.powernode.springmvc.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class UserController {
@Autowired
private UserService userService;
@RequestMapping(value = "/user/{id}",method = RequestMethod.GET)
public ResponseEntity<User> getById(@PathVariable("id") Long id){
User user = userService.getById(id);
if (user == null){
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}else{
return ResponseEntity.ok(user);
}
}
}
  • 编写UserServie类
package com.powernode.springmvc.service;
import com.powernode.springmvc.bean.User;
import org.springframework.stereotype.Service;
@Service
public class UserService {
public User getById(Long id){
if (id == 1){
return new User(111L,"zhangsan","123");
}
return null;
}
}

重启tomcat,浏览器点击"查找id=1的用户信息"

跳转结果

如果浏览器地址手动更改访问为http://localhost:8080/user/2

拦截器
  • 拦截器基本配置
    第一种方式:在spring配置文件中配置
    配置测试拦截器Interceptor1
package com.powernode.springmvc.interceptors;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Component
public class Interceptor1 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("Interceptor1's preHandlehaha");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
System.out.println("Interceptor1's postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
System.out.println("Interceptor1's afterCompletion");
}
}
  • 在dispatcherServlet-servlet.xml中添加
<mvc:interceptors>
<bean class="com.powernode.springmvc.interceptors.Interceptor1"/>
</mvc:interceptors>

第二种方法,使用注解配置

<mvc:interceptors>
<!--通过注解方式,引用类名,首字母小写-->
<ref bean="interceptor1"/>
</mvc:interceptors>

第二种方式的前提:
● 前提1:包扫描

● 前提2:使用 @Component 注解进行标注

2. 自定义拦截路径

<mvc:interceptor>
<!--拦截所有路径-->
<mvc:mapping path="/**"/>
<!--放行指定路径-->
<mvc:exclude-mapping path="/ok"/>
<!--设置拦截器-->
<ref bean="interceptor1"/>
</mvc:interceptor>
</mvc:interceptors>
posted @   文采杰出  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器
· 面试官:你是如何进行SQL调优的?
点击右上角即可分享
微信分享提示