https://docs.spring.io/spring-framework/reference/web/webmvc.html
Spring Web MVC是基于Servlet API构建的原始Web框架,从一开始就包含在Spring Framework中。正式名称“Spring Web MVC”来自其源模块的名称( spring-webmvc
),但它通常被称为“Spring MVC”。
在控制层框架历经Strust、WebWork、Strust2等诸多产品的历代更迭之后,目前业界普遍选择了SpringMVC作为Java EE项目表述层开发的首选方案。之所以能做到这一点,是因为SpringMVC具备如下显著优势:
原生Servlet API开发代码片段
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String userName = request.getParameter("userName");
System.out.println("userName="+userName);
}
基于SpringMVC开发代码片段
@RequestMapping("/user/login")
public String login(@RequestParam("userName") String userName,Sting password){
log.debug("userName="+userName);
//调用业务即可
return "result";
}
SSM框架构建起单体项目的技术栈需求!其中的SpringMVC负责表述层(控制层)实现简化!
SpringMVC的作用主要覆盖的是表述层,例如:
最终总结:
Spring MVC与许多其他Web框架一样,是围绕前端控制器模式设计的,其中中央 Servlet
DispatcherServlet
做整体请求处理调度!
除了DispatcherServlet
SpringMVC还会提供其他特殊的组件协作完成请求处理和响应呈现。
SpringMVC处理请求流程:
SpringMVC涉及组件理解:
体验场景需求
配置分析
准备项目
创建项目
springmvc-base-quick
注意:需要转成maven/web程序!!
导入依赖
<properties>
<spring.version>6.0.6</spring.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- springioc相关依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- web相关依赖 -->
<!-- 在 pom.xml 中引入 Jakarta EE Web API 的依赖 -->
<!--
在 Spring Web MVC 6 中,Servlet API 迁移到了 Jakarta EE API,因此在配置 DispatcherServlet 时需要使用
Jakarta EE 提供的相应类库和命名空间。错误信息 “‘org.springframework.web.servlet.DispatcherServlet’
is not assignable to ‘javax.servlet.Servlet,jakarta.servlet.Servlet’” 表明你使用了旧版本的
Servlet API,没有更新到 Jakarta EE 规范。
-->
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-web-api</artifactId>
<version>9.1.0</version>
<scope>provided</scope>
</dependency>
<!-- springwebmvc相关依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
Controller声明
@Controller
public class HelloController {
//handlers
/**
* handler就是controller内部的具体方法
* @RequestMapping("/springmvc/hello") 就是用来向handlerMapping中注册的方法注解!
* @ResponseBody 代表向浏览器直接返回数据!
*/
@RequestMapping("/springmvc/hello")
@ResponseBody
public String hello(){
System.out.println("HelloController.hello");
return "hello springmvc!!";
}
}
SpringIoC配置
<?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">
<!-- 扫描controller对应的包,将handler加入到ioc-->
<context:component-scan base-package="com.atguigu.controller" />
<!-- 方案1: 手动配置handlerMapping 和 handlerAdapter -->
<!-- handlerMapping
RequestMappingHandlerMapping 就是springmvc提供的组件
支持@RequestMapping方式注册的handler
-->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" />
<!-- handlerAdapter
RequestMappingHandlerAdapter 就是springmvc提供的组件
支持@RequestMapping方式注册的handler
-->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter" />
<!-- 方案2: 不配置
解释: springmvc会在启动的时候,如果发现我们没有配置handlerMapping,handlerAdapter,viewResolver
就会加载spring-webmvc包下的配置DispatcherServlet.properties下的组件!
DispatcherServlet.properties:
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
org.springframework.web.servlet.function.support.RouterFunctionMapping
org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\
org.springframework.web.servlet.function.support.HandlerFunctionAdapter
........
所以,我们可以不同写,也会默认加载! 注意: 一旦你配置了,就不会加载对应的默认组件切记!!!
-->
<!-- viewResolver 不需要配置,因为我们不需要查找逻辑视图!!! -->
</beans>
web.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!-- 配置SpringMVC中负责处理请求的核心Servlet,也被称为SpringMVC的前端控制器 -->
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<!-- DispatcherServlet的全类名 -->
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 通过初始化参数指定SpringMVC配置文件位置 -->
<init-param>
<!-- 如果不记得contextConfigLocation配置项的名称,可以到DispatcherServlet的父类FrameworkServlet中查找 -->
<param-name>contextConfigLocation</param-name>
<!-- 使用classpath:说明这个路径从类路径的根目录开始才查找 -->
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<!-- 作为框架的核心组件,在启动过程中有大量的初始化操作要做,这些操作放在第一次请求时才执行非常不恰当 -->
<!-- 我们应该将DispatcherServlet设置为随Web应用一起启动 -->
<load-on-startup>100</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<!-- 对DispatcherServlet来说,url-pattern有两种方式配置 -->
<!-- 配置“/”,表示匹配整个Web应用范围内所有请求。这里有一个硬性规定:不能写成“/*”。
只有这一个地方有这个特殊要求,以后我们再配置Filter还是可以正常写“/*”。 -->
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
启动测试
注意: tomcat应该是10+版本!方可支持 Jakarta EE API!
@RequestMapping注解的作用就是将请求的 URL 地址和处理请求的方式(handler方法)关联起来,建立映射关系。
SpringMVC 接收到指定的请求,就会来找到在映射关系中对应的方法来处理这个请求。
精准路径匹配
在@RequestMapping注解指定 URL 地址时,不使用任何通配符,按照请求地址进行精确匹配。
@Controller
public class UserController {
/**
* 精准设置访问地址 /user/login
*/
@RequestMapping(value = {"/user/login"})
@ResponseBody
public String login(){
System.out.println("UserController.login");
return "login success!!";
}
/**
* 精准设置访问地址 /user/register
*/
@RequestMapping(value = {"/user/register"})
@ResponseBody
public String register(){
System.out.println("UserController.register");
return "register success!!";
}
}
模糊路径匹配
在@RequestMapping注解指定 URL 地址时,通过使用通配符,匹配多个类似的地址。
@Controller
public class ProductController {
/**
* 路径设置为 /product/*
* /* 为单层任意字符串 /product/a /product/aaa 可以访问此handler
* /product/a/a 不可以
* 路径设置为 /product/**
* /** 为任意层任意字符串 /product/a /product/aaa 可以访问此handler
* /product/a/a 也可以访问
*/
@RequestMapping("/product/*")
@ResponseBody
public String show(){
System.out.println("ProductController.show");
return "product show!";
}
}
单层匹配和多层匹配:
/*:只能匹配URL地址中的一层,如果想准确匹配两层,那么就写“/*/*”以此类推。
/**:可以匹配URL地址中的多层。
其中所谓的一层或多层是指一个URL地址字符串被“/”划分出来的各个层次
这个知识点虽然对于@RequestMapping注解来说实用性不大,但是将来配置拦截器的时候也遵循这个规则。
类和方法级别区别
@RequestMapping
注解可以用于类级别和方法级别,它们之间的区别如下:
@RequestMapping
注解可以设置在控制器类上,用于映射整个控制器的通用请求路径。这样,如果控制器中的多个方法都需要映射同一请求路径,就不需要在每个方法上都添加映射路径。@RequestMapping
注解也可以单独设置在控制器方法上,用于更细粒度地映射请求路径和处理方法。当多个方法处理同一个路径的不同操作时,可以使用方法级别的 @RequestMapping
注解进行更精细的映射。//1.标记到handler方法
@RequestMapping("/user/login")
@RequestMapping("/user/register")
@RequestMapping("/user/logout")
//2.优化标记类+handler方法
//类上
@RequestMapping("/user")
//handler方法上
@RequestMapping("/login")
@RequestMapping("/register")
@RequestMapping("/logout")
附带请求方式限制
HTTP 协议定义了八种请求方式,在 SpringMVC 中封装到了下面这个枚举类:
public enum RequestMethod {
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
}
默认情况下:@RequestMapping(“/logout”) 任何请求方式都可以访问!
如果需要特定指定:
@Controller
public class UserController {
/**
* 精准设置访问地址 /user/login
* method = RequestMethod.POST 可以指定单个或者多个请求方式!
* 注意:违背请求方式会出现405异常!
*/
@RequestMapping(value = {"/user/login"} , method = RequestMethod.POST)
@ResponseBody
public String login(){
System.out.println("UserController.login");
return "login success!!";
}
/**
* 精准设置访问地址 /user/register
*/
@RequestMapping(value = {"/user/register"},method = {RequestMethod.POST,RequestMethod.GET})
@ResponseBody
public String register(){
System.out.println("UserController.register");
return "register success!!";
}
}
注意:违背请求方式,会出现405异常!!!
进阶注解
还有 @RequestMapping
的 HTTP 方法特定快捷方式变体:
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
@PatchMapping
@RequestMapping(value="/login",method=RequestMethod.GET)
||
@GetMapping(value="/login")
注意:进阶注解只能添加到handler方法上,无法添加到类上!
常见配置问题
出现原因:多个 handler 方法映射了同一个地址,导致 SpringMVC 在接收到这个地址的请求时该找哪个 handler 方法处理。
There is already ‘demo03MappingMethodHandler’ bean method com.atguigu.mvc.handler.Demo03MappingMethodHandler#empGet() mapped.
在 HTTP 请求中,我们可以选择不同的参数类型,如 param 类型和 JSON 类型。下面对这两种参数类型进行区别和对比:
参数编码:
param 类型的参数会被编码为 ASCII 码。例如,假设 name=john doe
,则会被编码为 name=john%20doe
。而 JSON 类型的参数会被编码为 UTF-8。
参数顺序:
param 类型的参数没有顺序限制。但是,JSON 类型的参数是有序的。JSON 采用键值对的形式进行传递,其中键值对是有序排列的。
数据类型:
param 类型的参数仅支持字符串类型、数值类型和布尔类型等简单数据类型。而 JSON 类型的参数则支持更复杂的数据类型,如数组、对象等。
嵌套性:
param 类型的参数不支持嵌套。但是,JSON 类型的参数支持嵌套,可以传递更为复杂的数据结构。
可读性:
param 类型的参数格式比 JSON 类型的参数更加简单、易读。但是,JSON 格式在传递嵌套数据结构时更加清晰易懂。
总的来说,param 类型的参数适用于单一的数据传递,而 JSON 类型的参数则更适用于更复杂的数据结构传递。根据具体的业务需求,需要选择合适的参数类型。在实际开发中,常见的做法是:在 GET 请求中采用 param 类型的参数,而在 POST 请求中采用 JSON 类型的参数传递。
直接接值
客户端请求
handler接收参数
只要形参数名和类型与传递参数相同,即可自动接收!
@Controller
@RequestMapping("param")
public class ParamController {
/**
* 前端请求: http://localhost:8080/param/value?name=xx&age=18
*
* 可以利用形参列表,直接接收前端传递的param参数!
* 要求: 参数名 = 形参名
* 类型相同
* 出现乱码正常,json接收具体解决!!
* @return 返回前端数据
*/
@GetMapping(value="/value")
@ResponseBody
public String setupForm(String name,int age){
System.out.println("name = " + name + ", age = " + age);
return name + age;
}
}
@RequestParam注解
可以使用 @RequestParam
注释将 Servlet 请求参数(即查询参数或表单数据)绑定到控制器中的方法参数。
@RequestParam
使用场景:
/**
* 前端请求: http://localhost:8080/param/data?name=xx&stuAge=18
*
* 使用@RequestParam注解标记handler方法的形参
* 指定形参对应的请求参数@RequestParam(请求参数名称)
*/
@GetMapping(value="/data")
@ResponseBody
public Object paramForm(@RequestParam("name") String name,
@RequestParam("stuAge") int age){
System.out.println("name = " + name + ", age = " + age);
return name+age;
}
默认情况下,使用此批注的方法参数是必需的,但您可以通过将 @RequestParam
批注的 required
标志设置为 false
!
如果没有没有设置非必须,也没有传递参数会出现:
将参数设置非必须,并且设置默认值:
@GetMapping(value="/data")
@ResponseBody
public Object paramForm(@RequestParam("name") String name,
@RequestParam(value = "stuAge",required = false,defaultValue = "18") int age){
System.out.println("name = " + name + ", age = " + age);
return name+age;
}
特殊场景接值
一名多值
多选框,提交的数据的时候一个key对应多个值,我们可以使用集合进行接收!
/**
* 前端请求: http://localhost:8080/param/mul?hbs=吃&hbs=喝
*
* 一名多值,可以使用集合接收即可!但是需要使用@RequestParam注解指定
*/
@GetMapping(value="/mul")
@ResponseBody
public Object mulForm(@RequestParam List<String> hbs){
System.out.println("hbs = " + hbs);
return hbs;
}
实体接收
Spring MVC 是 Spring 框架提供的 Web 框架,它允许开发者使用实体对象来接收 HTTP 请求中的参数。通过这种方式,可以在方法内部直接使用对象的属性来访问请求参数,而不需要每个参数都写一遍。下面是一个使用实体对象接收参数的示例:
定义一个用于接收参数的实体类:
public class User {
private String name;
private int age = 18;
// getter 和 setter 略
}
在控制器中,使用实体对象接收,示例代码如下:
@Controller
@RequestMapping("param")
public class ParamController {
@RequestMapping(value = "/user", method = RequestMethod.POST)
@ResponseBody
public String addUser(User user) {
// 在这里可以使用 user 对象的属性来接收请求参数
System.out.println("user = " + user);
return "success";
}
}
在上述代码中,将请求参数name和age映射到实体类属性上!要求属性名必须等于参数名!否则无法映射!
使用postman传递参数测试:
路径传递参数是一种在 URL 路径中传递参数的方式。在 RESTful 的 Web 应用程序中,经常使用路径传递参数来表示资源的唯一标识符或更复杂的表示方式。而 Spring MVC 框架提供了 @PathVariable
注解来处理路径传递参数。
@PathVariable
注解允许将 URL 中的占位符映射到控制器方法中的参数。
例如,如果我们想将 /user/{id}
路径下的 {id}
映射到控制器方法的一个参数中,则可以使用 @PathVariable
注解来实现。
下面是一个使用 @PathVariable
注解处理路径传递参数的示例:
/**
* 动态路径设计: /user/{动态部分}/{动态部分} 动态部分使用{}包含即可! {}内部动态标识!
* 形参列表取值: @PathVariable Long id 如果形参名 = {动态标识} 自动赋值!
* @PathVariable("动态标识") Long id 如果形参名 != {动态标识} 可以通过指定动态标识赋值!
*
* 访问测试: /param/user/1/root -> id = 1 uname = root
*/
@GetMapping("/user/{id}/{name}")
@ResponseBody
public String getUser(@PathVariable Long id,
@PathVariable("name") String uname) {
System.out.println("id = " + id + ", uname = " + uname);
return "user_detail";
}
前端传递 JSON 数据时,Spring MVC 框架可以使用 @RequestBody
注解来将 JSON 数据转换为 Java 对象。@RequestBody
注解表示当前方法参数的值应该从请求体中获取,并且需要指定 value 属性来指示请求体应该映射到哪个参数上。其使用方式和示例代码如下:
前端发送 JSON 数据的示例:(使用postman测试)
{
"name": "张三",
"age": 18,
"gender": "男"
}
定义一个用于接收 JSON 数据的 Java 类,例如:
public class Person {
private String name;
private int age;
private String gender;
// getter 和 setter 略
}
在控制器中,使用 @RequestBody
注解来接收 JSON 数据,并将其转换为 Java 对象,例如:
@PostMapping("/person")
@ResponseBody
public String addPerson(@RequestBody Person person) {
// 在这里可以使用 person 对象来操作 JSON 数据中包含的属性
return "success";
}
在上述代码中,@RequestBody
注解将请求体中的 JSON 数据映射到 Person
类型的 person
参数上,并将其作为一个对象来传递给 addPerson()
方法进行处理。
完善配置
测试:
问题:
org.springframework.web.HttpMediaTypeNotSupportedException: Content-Type ‘application/json;charset=UTF-8’ is not supported]
原因:
<?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">
<!-- 扫描controller对应的包,将handler加入到ioc-->
<context:component-scan base-package="com.atguigu.controller" />
<!--
注意: 导入mvc命名空间!
mvc:annotation-driven 是一个整合标签
他会导入handlerMapping和handlerAdapter
他会导入json数据格式转化器等等!
-->
<mvc:annotation-driven />
<!-- viewResolver 不需要配置,因为我们不需要查找逻辑视图!!! -->
</beans>
pom.xml 加入jackson依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.0</version>
</dependency>
mvc:annotation说明
<mvc:annotation是一个整合配置标签,一个标签将springmvc必备组件都加入到ioc容器中!其中包含handlerMapping和handlerAdapter以及json转化器等等!
让我们来查看下<mvc:annotation具体的动作!
先查看<mvc:annotation标签最终对应解析的Java类
查看解析类中具体的动作即可
打开源码:org.springframework.web.servlet.config.MvcNamespaceHandler
打开源码:org.springframework.web.servlet.config.AnnotationDrivenBeanDefinitionParser
class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
public static final String HANDLER_MAPPING_BEAN_NAME = RequestMappingHandlerMapping.class.getName();
public static final String HANDLER_ADAPTER_BEAN_NAME = RequestMappingHandlerAdapter.class.getName();
static {
ClassLoader classLoader = AnnotationDrivenBeanDefinitionParser.class.getClassLoader();
javaxValidationPresent = ClassUtils.isPresent("jakarta.validation.Validator", classLoader);
romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader);
jaxb2Present = ClassUtils.isPresent("jakarta.xml.bind.Binder", classLoader);
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
}
@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext context) {
//handlerMapping加入到ioc容器
readerContext.getRegistry().registerBeanDefinition(HANDLER_MAPPING_BEAN_NAME, handlerMappingDef);
//添加jackson转化器
addRequestBodyAdvice(handlerAdapterDef);
addResponseBodyAdvice(handlerAdapterDef);
//handlerAdapter加入到ioc容器
readerContext.getRegistry().registerBeanDefinition(HANDLER_ADAPTER_BEAN_NAME, handlerAdapterDef);
return null;
}
//具体添加jackson转化对象方法
protected void addRequestBodyAdvice(RootBeanDefinition beanDef) {
if (jackson2Present) {
beanDef.getPropertyValues().add("requestBodyAdvice",
new RootBeanDefinition(JsonViewRequestBodyAdvice.class));
}
}
protected void addResponseBodyAdvice(RootBeanDefinition beanDef) {
if (jackson2Present) {
beanDef.getPropertyValues().add("responseBodyAdvice",
new RootBeanDefinition(JsonViewResponseBodyAdvice.class));
}
}
可以使用 @CookieValue
注释将 HTTP Cookie 的值绑定到控制器中的方法参数。
考虑使用以下 cookie 的请求:
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84
下面的示例演示如何获取 cookie 值:
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) {
//...
}
可以使用 @RequestHeader
批注将请求标头绑定到控制器中的方法参数。
请考虑以下带有标头的请求:
Host localhost:8080
Accept text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language fr,en-gb;q=0.7,en;q=0.3
Accept-Encoding gzip,deflate
Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive 300
下面的示例获取 Accept-Encoding
和 Keep-Alive
标头的值:
@GetMapping("/demo")
public void handle(
@RequestHeader("Accept-Encoding") String encoding,
@RequestHeader("Keep-Alive") long keepAlive) {
//...
}
下表描述了支持的控制器方法参数
Controller method argument 控制器方法参数 | Description |
---|---|
jakarta.servlet.ServletRequest , jakarta.servlet.ServletResponse | 请求/响应对象 |
jakarta.servlet.http.HttpSession | 强制存在会话。因此,这样的参数永远不会为 null 。 |
java.io.InputStream , java.io.Reader | 用于访问由 Servlet API 公开的原始请求正文。 |
java.io.OutputStream , java.io.Writer | 用于访问由 Servlet API 公开的原始响应正文。 |
@PathVariable | 接收路径参数注解 |
@RequestParam | 用于访问 Servlet 请求参数,包括多部分文件。参数值将转换为声明的方法参数类型。 |
@RequestHeader | 用于访问请求标头。标头值将转换为声明的方法参数类型。 |
@CookieValue | 用于访问Cookie。Cookie 值将转换为声明的方法参数类型。 |
@RequestBody | 用于访问 HTTP 请求正文。正文内容通过使用 HttpMessageConverter 实现转换为声明的方法参数类型。 |
java.util.Map , org.springframework.ui.Model , org.springframework.ui.ModelMap | 共享域对象,并在视图呈现过程中向模板公开。 |
Errors , BindingResult | 验证和数据绑定中的错误信息获取对象! |
获取原生对象示例:
/**
* 如果想要获取请求或者响应对象,或者会话等,可以直接在形参列表传入,并且不分先后顺序!
* 注意: 接收原生对象,并不影响参数接收!
*/
@GetMapping("api")
@ResponseBody
public String api(HttpSession session , HttpServletRequest request,
HttpServletResponse response){
String method = request.getMethod();
System.out.println("method = " + method);
return "api";
}
在 JavaWeb 中,共享域指的是在 Servlet 中存储数据,以便在同一 Web 应用程序的多个组件中进行共享和访问。常见的共享域有四种:ServletContext
、HttpSession
、HttpServletRequest
、PageContext
。
ServletContext
共享域:ServletContext
对象可以在整个 Web 应用程序中共享数据,是最大的共享域。一般可以用于保存整个 Web 应用程序的全局配置信息,以及所有用户都共享的数据。在 ServletContext
中保存的数据是线程安全的。HttpSession
共享域:HttpSession
对象可以在同一用户发出的多个请求之间共享数据,但只能在同一个会话中使用。比如,可以将用户登录状态保存在 HttpSession
中,让用户在多个页面间保持登录状态。HttpServletRequest
共享域:HttpServletRequest
对象可以在同一个请求的多个处理器方法之间共享数据。比如,可以将请求的参数和属性存储在 HttpServletRequest
中,让处理器方法之间可以访问这些数据。PageContext
共享域:PageContext
对象是在 JSP 页面Servlet 创建时自动创建的。它可以在 JSP 的各个作用域中共享数据,包括pageScope
、requestScope
、sessionScope
、applicationScope
等作用域。共享域的作用是提供了方便实用的方式在同一 Web 应用程序的多个组件之间传递数据,并且可以将数据保存在不同的共享域中,根据需要进行选择和使用。
@RequestMapping("/attr/request/model")
@ResponseBody
public String testAttrRequestModel(
// 在形参位置声明Model类型变量,用于存储模型数据
Model model) {
// 我们将数据存入模型,SpringMVC 会帮我们把模型数据存入请求域
// 存入请求域这个动作也被称为暴露到请求域
model.addAttribute("requestScopeMessageModel","i am very happy[model]");
return "target";
}
@RequestMapping("/attr/request/model/map")
@ResponseBody
public String testAttrRequestModelMap(
// 在形参位置声明ModelMap类型变量,用于存储模型数据
ModelMap modelMap) {
// 我们将数据存入模型,SpringMVC 会帮我们把模型数据存入请求域
// 存入请求域这个动作也被称为暴露到请求域
modelMap.addAttribute("requestScopeMessageModelMap","i am very happy[model map]");
return "target";
}
@RequestMapping("/attr/request/map")
@ResponseBody
public String testAttrRequestMap(
// 在形参位置声明Map类型变量,用于存储模型数据
Map<String, Object> map) {
// 我们将数据存入模型,SpringMVC 会帮我们把模型数据存入请求域
// 存入请求域这个动作也被称为暴露到请求域
map.put("requestScopeMessageMap", "i am very happy[map]");
return "target";
}
@RequestMapping("/attr/request/original")
@ResponseBody
public String testAttrOriginalRequest(
// 拿到原生对象,就可以调用原生方法执行各种操作
HttpServletRequest request) {
request.setAttribute("requestScopeMessageOriginal", "i am very happy[original]");
return "target";
}
@RequestMapping("/attr/request/mav")
public ModelAndView testAttrByModelAndView() {
// 1.创建ModelAndView对象
ModelAndView modelAndView = new ModelAndView();
// 2.存入模型数据
modelAndView.addObject("requestScopeMessageMAV", "i am very happy[mav]");
// 3.设置视图名称
modelAndView.setViewName("target");
return modelAndView;
}
@RequestMapping("/attr/session")
@ResponseBody
public String testAttrSession(HttpSession session) {
//直接对session对象操作,即对会话范围操作!
return "target";
}
解释:springmvc会在初始化容器的时候,讲servletContext对象存储到ioc容器中!
@Autowired
private ServletContext servletContext;
@RequestMapping("/attr/application")
@ResponseBody
public String attrApplication() {
servletContext.setAttribute("appScopeMsg", "i am hungry...");
return "target";
}
理解handler方法的作用和组成:
/**
* TODO: 一个controller的方法是控制层的一个处理器,我们称为handler
* TODO: handler需要使用@RequestMapping/@GetMapping系列,声明路径,在HandlerMapping中注册,供DS查找!
* TODO: handler作用总结:
* 1.接收请求参数(param,json,pathVariable,共享域等)
* 2.调用业务逻辑
* 3.响应前端数据(页面(不讲解模版页面跳转),json,转发和重定向等)
* TODO: handler如何处理呢
* 1.接收参数: handler(形参列表: 主要的作用就是用来接收参数)
* 2.调用业务: { 方法体 可以向后调用业务方法 service.xx() }
* 3.响应数据: return 返回结果,可以快速响应前端数据
*/
@GetMapping
public Object handler(简化请求参数接收){
调用业务方法
return 简化响应前端数据;
}
总结: 请求数据接收,我们都是通过handler的形参列表
前端数据响应,我们都是通过handler的return关键字快速处理!
springmvc简化了参数接收和响应!
开发模式回顾
在 Web 开发中,有两种主要的开发模式:前后端分离和混合开发。
前后端分离模式:
指将前端的界面和后端的业务逻辑通过接口分离开发的一种方式。开发人员使用不同的技术栈和框架,前端开发人员主要负责页面的呈现和用户交互,后端开发人员主要负责业务逻辑和数据存储。前后端通信通过 API 接口完成,数据格式一般使用 JSON 或 XML。前后端分离模式可以提高开发效率,同时也有助于代码重用和维护。
混合开发模式:
指将前端和后端的代码集成在同一个项目中,共享相同的技术栈和框架。这种模式在小型项目中比较常见,可以减少学习成本和部署难度。但是,在大型项目中,这种模式会导致代码耦合性很高,维护和升级难度较大。
对于混合开发,我们就需要使用动态页面技术,动态展示Java的共享域数据!!
jsp技术了解
JSP(JavaServer Pages)是一种动态网页开发技术,它是由 Sun 公司提出的一种基于 Java 技术的 Web 页面制作技术,可以在 HTML 文件中嵌入 Java 代码,使得生成动态内容的编写更加简单。
JSP 最主要的作用是生成动态页面。它允许将 Java 代码嵌入到 HTML 页面中,以便使用 Java 进行数据库查询、处理表单数据和生成 HTML 等动态内容。另外,JSP 还可以与 Servlet 结合使用,实现更加复杂的 Web 应用程序开发。
JSP 的主要特点包括:
准备jsp页面和依赖
pom.xml依赖
<!-- jsp需要依赖! jstl-->
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
<version>3.0.0</version>
</dependency>
jsp页面创建
建议位置:/WEB-INF/下,避免外部直接访问!
位置:/WEB-INF/views/home.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<!-- 可以获取共享域的数据,动态展示! jsp== 后台vue -->
${msg}
</body>
</html>
快速响应模版页面
配置jsp视图解析器
springmvc.xml
<!-- 配置动态页面语言jsp的视图解析器,快速查找jsp-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
handler返回视图
/**
* 跳转到提交文件页面 /save/jump
*
* 如果要返回jsp页面!
* 1.方法返回值改成字符串类型
* 2.返回逻辑视图名即可
* <property name="prefix" value="/WEB-INF/views/"/>
* + 逻辑视图名 +
* <property name="suffix" value=".jsp"/>
*/
@GetMapping("jump")
public String jumpJsp(Model model){
System.out.println("FileController.jumpJsp");
model.addAttribute("msg","request data!!");
return "home";
}
在 Spring MVC 中,Handler 方法返回值来实现快速转发,可以使用 redirect
或者 forward
关键字来实现重定向。
@RequestMapping("/redirect-demo")
public String redirectDemo() {
// 重定向到 /demo 路径
return "redirect:/demo";
}
@RequestMapping("/forward-demo")
public String forwardDemo() {
// 转发到 /demo 路径
return "forward:/demo";
}
//注意: 转发和重定向到项目下资源路径都是相同,都不需要添加项目根路径!填写项目下路径即可!
总结:
导入jackson依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.0</version>
</dependency>
添加json数据转化器
<!--
注意: 导入mvc命名空间!
mvc:annotation-driven 是一个整合标签
他会导入handlerMapping和handlerAdapter
他会导入json数据格式转化器等等!
-->
<mvc:annotation-driven />
方法上使用@ResponseBody
可以在方法上使用 @ResponseBody
注解,用于将方法返回的对象序列化为 JSON 或 XML 格式的数据,并发送给客户端。在前后端分离的项目中使用!
测试方法:
@GetMapping("/accounts/{id}")
@ResponseBody
public Object handle() {
// ...
return obj;
}
具体来说,@ResponseBody
注解可以用来标识方法或者方法返回值,表示方法的返回值是要直接返回给客户端的数据,而不是由视图解析器来解析并渲染生成响应体(viewResolver没用)。
测试方法:
@RequestMapping(value = "/user/detail", method = RequestMethod.POST)
@ResponseBody
public User getUser(@RequestBody User userParam) {
System.out.println("userParam = " + userParam);
User user = new User();
user.setAge(18);
user.setName("John");
//返回的对象,会使用jackson的序列化工具,转成json返回给前端!
return user;
}
返回结果:
类上使用@ResponseBody
如果类中每个方法上都标记了 @ResponseBody 注解,那么这些注解就可以提取到类上。
@ResponseBody //responseBody可以添加到类上,代表默认类中的所有方法都生效!
@Controller
@RequestMapping("param")
public class ParamController {
类上的 @ResponseBody 注解可以和 @Controller 注解合并为 @RestController 注解。所以使用了 @RestController 注解就相当于给类中的每个方法都加了 @ResponseBody 注解。
RestController源码:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
* @return the suggested component name, if any (or empty String otherwise)
* @since 4.0.1
*/
@AliasFor(annotation = Controller.class)
String value() default "";
}
静态资源概念
资源本身已经是可以直接拿到浏览器上使用的程度了,不需要在服务器端做任何运算、处理。典型的静态资源包括:
静态资源访问和问题解决
web应用加入静态资源
手动构建确保编译
访问静态资源
问题分析
对 SpringMVC 来说,必须有对应的 @RequestMapping 才能找到处理请求的方法
问题解决
在 SpringMVC 配置文件中增加配置:
<?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">
<!-- 扫描controller对应的包,将handler加入到ioc-->
<context:component-scan base-package="com.atguigu.controller" />
<!-- 加入这个配置,SpringMVC 就会在遇到没有 @RequestMapping 的请求时放它过去 -->
<!-- 所谓放它过去就是让这个请求去找它原本要访问的资源 -->
<mvc:default-servlet-handler/>
</beans>
再次测试访问图片:
新的问题:其他原本正常的handler请求访问不了了
handler无法访问
解决方案:
<!-- 开启 SpringMVC 的注解驱动功能。这个配置也被称为 SpringMVC 的标配。 -->
<!-- 标配:因为 SpringMVC 环境下非常多的功能都要求必须打开注解驱动才能正常工作。 -->
<mvc:annotation-driven/>
RESTful(Representational State Transfer)是一种软件架构风格,用于设计网络应用程序和服务之间的通信。它是一种基于标准 HTTP 方法的简单和轻量级的通信协议,广泛应用于现代的Web服务开发。
通过遵循 RESTful 架构的设计原则,可以构建出易于理解、可扩展、松耦合和可重用的 Web 服务。RESTful API 的特点是简单、清晰,并且易于使用和理解,它们使用标准的 HTTP 方法和状态码进行通信,不需要额外的协议和中间件。
RESTful 架构通常用于构建 Web API,提供数据的传输和操作。它可以用于各种应用场景,包括客户端-服务器应用、单页应用(SPA)、移动应用程序和微服务架构等。
总而言之,RESTful 是一种基于 HTTP 和标准化的设计原则的软件架构风格,用于设计和实现可靠、可扩展和易于集成的 Web 服务和应用程序!
学习RESTful设计原则可以帮助我们更好去设计HTTP协议的API接口!!
每一个URI代表1种资源(URI 是名词);
客户端使用GET、POST、PUT、DELETE 4个表示操作方式的动词对服务端资源进行操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源;
资源的表现形式是XML或者JSON;
客户端与服务端之间的交互在请求之间是无状态的,从客户端到服务端的每个请求都必须包含理解请求所必需的信息。
小结:rest 面向的是资源、而不是功能;rest操作要求符合幂等性
幂等性:执行一次操作和执行多次操作对系统的影响是一样的
HTTP协议请求方式要求
REST 风格主张在项目设计、开发过程中,具体的操作符合HTTP协议定义的请求方式的语义。
操作 | 请求方式 |
---|---|
查询操作 | GET |
保存操作 | POST |
删除操作 | DELETE |
更新操作 | PUT |
URL路径风格要求
REST风格下每个资源都应该有一个唯一的标识符,例如一个 URI(统一资源标识符)或者一个 URL(统一资源定位符)。资源的标识符应该能明确地说明该资源的信息,同时也应该是可被理解和解释的!
使用URL+请求方式确定具体的动作,他也是一种标准的HTTP协议请求!
操作 | 传统风格 | REST 风格 |
---|---|---|
保存 | /CRUD/saveEmp | URL 地址:/emp 请求方式:POST |
删除 | /CRUD/removeEmp?empId=2 | URL 地址:/emp/2 请求方式:DELETE |
更新 | /CRUD/updateEmp | URL 地址:/emp 请求方式:PUT |
查询 | /CRUD/editEmp?empId=2 | URL 地址:/emp/2 请求方式:GET |
总结
根据接口的具体动作,选择具体的HTTP协议请求方式
路径设计从原来携带动标识,改成名词,对应资源的唯一标识即可!
含蓄,安全
使用问号键值对的方式给服务器传递数据太明显,容易被人利用来对系统进行破坏。使用 REST 风格携带数据不再需要明显的暴露数据的名称。
风格统一
URL 地址整体格式统一,从前到后始终都使用斜杠划分各个单词,用简单一致的格式表达语义。
无状态
在调用一个接口(访问、操作资源)的时候,可以不用考虑上下文,不用考虑当前状态,极大的降低了系统设计的复杂度。
严谨,规范
严格按照 HTTP1.1 协议中定义的请求方式本身的语义进行操作。
简洁,优雅
过去做增删改查操作需要设计4个不同的URL,现在一个就够了。
操作 | 传统风格 | REST 风格 |
---|---|---|
保存 | /CRUD/saveEmp | URL 地址:/emp 请求方式:POST |
删除 | /CRUD/removeEmp?empId=2 | URL 地址:/emp/2 请求方式:DELETE |
更新 | /CRUD/updateEmp | URL 地址:/emp 请求方式:PUT |
查询 | /CRUD/editEmp?empId=2 | URL 地址:/emp/2 请求方式:GET |
丰富的语义
通过 URL 地址就可以知道资源之间的关系。它能够把一句话中的很多单词用斜杠连起来,反过来说就是可以在 URL 地址中用一句话来充分表达语义。
http://localhost:8080/shop
http://localhost:8080/shop/product
http://localhost:8080/shop/product/cellPhone
http://localhost:8080/shop/product/cellPhone/iPhone
接口设计
功能 | 接口和请求方式 | 请求参数 | 返回值 |
---|---|---|---|
分页查询 | GET /user | page=1&size=10 param | { 响应数据 } |
用户添加 | POST /user | { user 数据 } | {响应数据} |
用户详情 | GET /user/1 | 路径参数 | {响应数据} |
用户更新 | PUT /user | { user 更新数据} | {响应数据} |
用户删除 | DELETE /user/1 | 路径参数 | {响应数据} |
条件模糊 | GET /user/search | page=1&size=10&keywork=关键字 | {响应数据} |
问题讨论
为什么查询用户详情,就使用路径传递参数,多条件模糊查询,就使用请求参数传递?
误区:restful风格下,不是所有请求参数都是路径传递!可以使用其他方式传递!
在 RESTful API 的设计中,路径和请求参数和请求体都是用来向服务器传递信息的方式。
准备用户实体类:
package com.atguigu.pojo;
/**
* projectName: com.atguigu.pojo
* 用户实体类
*/
public class User {
private Integer id;
private String name;
private Integer age;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
准备用户Controller:
/**
* projectName: com.atguigu.controller
*
* description: 用户模块的控制器
*/
@RequestMapping("user")
@RestController
public class UserController {
/**
* 模拟分页查询业务接口
*/
@GetMapping
public Object queryPage(@RequestParam(name = "page",required = false,defaultValue = "1")int page,
@RequestParam(name = "size",required = false,defaultValue = "10")int size){
System.out.println("page = " + page + ", size = " + size);
System.out.println("分页查询业务!");
return "{'status':'ok'}";
}
/**
* 模拟用户保存业务接口
*/
@PostMapping
public Object saveUser(@RequestBody User user){
System.out.println("user = " + user);
System.out.println("用户保存业务!");
return "{'status':'ok'}";
}
/**
* 模拟用户详情业务接口
*/
@PostMapping("/{id}")
public Object detailUser(@PathVariable Integer id){
System.out.println("id = " + id);
System.out.println("用户详情业务!");
return "{'status':'ok'}";
}
/**
* 模拟用户更新业务接口
*/
@PutMapping
public Object updateUser(@RequestBody User user){
System.out.println("user = " + user);
System.out.println("用户更新业务!");
return "{'status':'ok'}";
}
/**
* 模拟条件分页查询业务接口
*/
@GetMapping("search")
public Object queryPage(@RequestParam(name = "page",required = false,defaultValue = "1")int page,
@RequestParam(name = "size",required = false,defaultValue = "10")int size,
@RequestParam(name = "keyword",required= false)String keyword){
System.out.println("page = " + page + ", size = " + size + ", keyword = " + keyword);
System.out.println("条件分页查询业务!");
return "{'status':'ok'}";
}
}
/*
需求说明
查询全部数据页数据
请求uri
schedule
请求方式
get
响应的json
{
"code":200,
"flag":true,
"data":[
{id:1,title:'学习java',completed:true},
{id:2,title:'学习html',completed:true},
{id:3,title:'学习css',completed:true},
{id:4,title:'学习js',completed:true},
{id:5,title:'学习vue',completed:true}
]
}
*/
/*
需求说明
根据id删除日程
请求uri
schedule/{id}
请求方式
delete
响应的json
{
"code":200,
"flag":true,
"data":null
}
*/
/*
需求说明
增加日程
请求uri
schedule
请求方式
post
请求体中的JSON
{
title: '',
completed: false
}
响应的json
{
"code":200,
"flag":true,
"data":null
}
*/
/*
需求说明
根据id修改数据
请求uri
schedule
请求方式
put
请求体中的JSON
{
id: 1,
title: '',
completed: false
}
响应的json
{
"code":200,
"flag":true,
"data":null
}
*/
导入前端工程
安装npm依赖
进入前端工程项目文件夹下!
npm i
启动前端程序
npm run dev
数据库怎么办?使用HashMap模拟,所以不涉及和MyBatis、Spring的整合!
搭建后台项目
pom.xml
<properties>
<spring.version>6.0.6</spring.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- springioc相关依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- web相关依赖 -->
<!-- 在 pom.xml 中引入 Jakarta EE Web API 的依赖 -->
<!--
在 Spring Web MVC 6 中,Servlet API 迁移到了 Jakarta EE API,因此在配置 DispatcherServlet 时需要使用
Jakarta EE 提供的相应类库和命名空间。错误信息 “‘org.springframework.web.servlet.DispatcherServlet’
is not assignable to ‘javax.servlet.Servlet,jakarta.servlet.Servlet’” 表明你使用了旧版本的
Servlet API,没有更新到 Jakarta EE 规范。
-->
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-web-api</artifactId>
<version>9.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.0</version>
</dependency>
<!-- springwebmvc相关依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
准备实体类
包:com.atguigu.pojo
/**
* projectName: com.atguigu.pojo
*
* description: 任务实体类
*/
public class Schedule {
private Integer id;
private String title;
private Boolean completed;
public Schedule() {
}
public Schedule(Integer id, String title, Boolean completed) {
this.id = id;
this.title = title;
this.completed = completed;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Boolean getCompleted() {
return completed;
}
public void setCompleted(Boolean completed) {
this.completed = completed;
}
@Override
public String toString() {
return "Schedule{" +
"id=" + id +
", title='" + title + '\'' +
", completed=" + completed +
'}';
}
}
准备R结果包装类
包:com.atguigu.utils
/**
* projectName: com.atguigu.utils
*
* description: 返回结果类
*/
public class R {
private int code = 200; //200成功状态码
private boolean flag = true; //返回状态
private Object data; //返回具体数据
public static R ok(Object data){
R r = new R();
r.data = data;
return r;
}
public static R fail(Object data){
R r = new R();
r.code = 500; //错误码
r.flag = false; //错误状态
r.data = data;
return r;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
准备业务类
包:com.atguigu.service
/**
* projectName: com.atguigu.service
*
* description: schedule业务接口
*/
public interface ScheduleService {
/**
* 返回全部学习计划
* @return
*/
List<Schedule> getAll();
/**
* 保存学习计划
* @param schedule
*/
void saveSchedule(Schedule schedule);
/**
* 更新学习计划
* @param schedule
*/
void updateSchedule(Schedule schedule);
/**
* 移除学习计划
* @param
*/
void removeById(Integer id);
}
/**
* projectName: com.atguigu.service.impl
*
* description:
*/
@Service
public class ScheduleServiceImpl implements ScheduleService {
//准备假数据
private static Map<Integer,Schedule> scheduleMap;
private static int maxId = 5;
static {
scheduleMap = new HashMap<>();
Schedule schedule = null;
schedule = new Schedule(1, "学习Java", false);
scheduleMap.put(1, schedule);
schedule = new Schedule(2, "学习H5", true);
scheduleMap.put(2, schedule);
schedule = new Schedule(3, "学习Css", false);
scheduleMap.put(3, schedule);
schedule = new Schedule(4, "学习JavaScript", false);
scheduleMap.put(4, schedule);
schedule = new Schedule(5, "学习Spring", true);
scheduleMap.put(5, schedule);
}
/**
* 返回全部学习计划
*
* @return
*/
@Override
public List<Schedule> getAll() {
return new ArrayList<>(scheduleMap.values());
}
/**
* 保存学习计划
*
* @param schedule
*/
@Override
public void saveSchedule(Schedule schedule) {
maxId++;
schedule.setId(maxId);
scheduleMap.put(maxId,schedule);
}
/**
* 更新学习计划
*
* @param schedule
*/
@Override
public void updateSchedule(Schedule schedule) {
scheduleMap.put(schedule.getId(),schedule);
}
/**
* 移除学习计划
*
* @param id
*/
@Override
public void removeById(Integer id) {
scheduleMap.remove(id);
}
}
准备spring-mvc.配置文件
位置:resources/spring-mvc.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">
<!-- 扫描controller对应的包,将handler加入到ioc-->
<context:component-scan base-package="com.atguigu.controller,com.atguigu.service" />
<!--
注意: 导入mvc命名空间!
mvc:annotation-driven 是一个整合标签
他会导入handlerMapping和handlerAdapter
他会导入json数据格式转化器等等!
-->
<mvc:annotation-driven />
<!-- viewResolver 不需要配置,因为我们不需要查找逻辑视图!!! -->
<!-- 加入这个配置,SpringMVC 就会在遇到没有 @RequestMapping 的请求时放它过去 -->
<!-- 所谓放它过去就是让这个请求去找它原本要访问的资源 -->
<mvc:default-servlet-handler/>
</beans>
准备 web.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!-- 配置SpringMVC中负责处理请求的核心Servlet,也被称为SpringMVC的前端控制器 -->
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<!-- DispatcherServlet的全类名 -->
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 通过初始化参数指定SpringMVC配置文件位置 -->
<init-param>
<!-- 如果不记得contextConfigLocation配置项的名称,可以到DispatcherServlet的父类FrameworkServlet中查找 -->
<param-name>contextConfigLocation</param-name>
<!-- 使用classpath:说明这个路径从类路径的根目录开始才查找 -->
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<!-- 作为框架的核心组件,在启动过程中有大量的初始化操作要做,这些操作放在第一次请求时才执行非常不恰当 -->
<!-- 我们应该将DispatcherServlet设置为随Web应用一起启动 -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<!-- 对DispatcherServlet来说,url-pattern有两种方式配置 -->
<!-- 方式一:配置“/”,表示匹配整个Web应用范围内所有请求。这里有一个硬性规定:不能写成“/*”。
只有这一个地方有这个特殊要求,以后我们再配置Filter还是可以正常写“/*”。 -->
<!-- 方式二:配置“*.扩展名”,表示匹配整个Web应用范围内部分请求 -->
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
因为前端项目设置了后台访问的项目根路径为 /rest
我们后台项目也对应的设置:
假设我们有一个网站 http://example.com
,现在需要跨域请求另外一个网站 http://api.example.com
中的数据。浏览器就会因为安全问题,拒绝客户端访问请求!
跨域问题是指在浏览器中发起跨域请求被浏览器拦截的问题。在同一个源域(同一协议、主机、端口),浏览器允许 JavaScript 发起跨域请求;在不同的源域下,浏览器对发起的异域请求会做出不同的限制。
常见的跨域问题的场景有:
基于CORS方式,解决跨域思路:
CORS(Cross-Origin Resource Sharing)是 W3C 制定的一种跨域解决方案,它给出了跨域请求和响应的标准。服务器端代码需要在响应头中设置 Access-Control-Allow-Origin,并指定访问来源域名名或 * 通配符,表示允许的跨域请求。浏览器可以根据响应头信息,判断是否允许该请求。
SpringMVC基于CORS思路解决跨域方案:
@CrossOrigin注解
@CrossOrigin
注释在带注释的【控制器方法】 / 【控制器类】上启用跨源请求
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
默认情况下, @CrossOrigin
允许:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {
/**
* 设置哪些客户端地址可以跨域访问! 格式为: 协议://主机地址:端口号
* @return
*/
@AliasFor("origins")
String[] value() default {};
@AliasFor("value")
String[] origins() default {};
/**
* 设置哪些客户端的[自定义请求头]可以跨域访问!
*/
String[] allowedHeaders() default {};
/**
* 设置哪些服务端的自定义响应头,可以被客户端读取!
*/
String[] exposedHeaders() default {};
/**
*设置哪些请求方法,可以跨域方式!
*/
RequestMethod[] methods() default {};
/**
* 值为 true 或者 false
* 客户端是否可以携带cookie!
*/
String allowCredentials() default "";
}
xml全局跨域配置
<mvc:cors>
<mvc:mapping path="/**"
allowed-origins="*"
allowed-methods="GET, PUT"
allowed-headers="header1, header2, header3"
exposed-headers="header1, header2"
allow-credentials="true"
/>
<mvc:mapping path="/**"
allowed-origins="https://domain1.com" />
</mvc:cors>
/**
* projectName: com.atguigu.controller
*
* description: 学习计划controller
*/
@CrossOrigin
/*
@CrossOrigin 注释在带注释的控制器方法上启用跨源请求
默认情况下,
@CrossOrigin 允许:
All origins 任何请求主机地址
All headers 任何请求头
All HTTP methods to which the controller method is mapped. 任何请求方式!
可以设置:
@CrossOrigin(origins = "https://domain2.com") 指定允许跨域请求的主机地址
*/
@RequestMapping("schedule")
@RestController
public class ScheduleController
{
@Autowired
private ScheduleService scheduleService;
@GetMapping
public R showList(){
List<Schedule> list = scheduleService.getAll();
return R.ok(list);
}
}
@PutMapping
public R changeSchedule(@RequestBody Schedule schedule){
scheduleService.updateSchedule(schedule);
return R.ok(null);
}
@DeleteMapping("/{id}")
public R removeSchedule(@PathVariable Integer id){
scheduleService.removeById(id);
return R.ok(null);
}
@PostMapping
public R saveSchedule(@RequestBody Schedule schedule){
scheduleService.saveSchedule(schedule);
return R.ok(null);
}
开发过程中是不可避免地会出现各种异常情况的,例如网络连接异常、数据格式异常、空指针异常等等。异常的出现可能导致程序的运行出现问题,甚至直接导致程序崩溃。因此,在开发过程中,合理处理异常、避免异常产生、以及对异常进行有效的调试是非常重要的。
对于异常的处理,一般分为两种方式:
@Throws
或 @ExceptionHandler
),就可以处理特定类型的异常。相较于编程式异常处理,声明式异常处理可以使代码更加简洁、易于维护和扩展。站在宏观角度来看待声明式事务处理:
整个项目从架构这个层面设计的异常处理的统一机制和规范。
一个项目中会包含很多个模块,各个模块需要分工完成。如果张三负责的模块按照 A 方案处理异常,李四负责的模块按照 B 方案处理异常……各个模块处理异常的思路、代码、命名细节都不一样,那么就会让整个项目非常混乱。
使用声明式异常处理,可以统一项目处理异常思路,项目更加清晰明了!
声明异常处理控制器类
异常处理控制类,统一定义异常处理handler方法!
/**
* projectName: com.atguigu.execptionhandler
*
* description: 全局异常处理器,内部可以定义异常处理Handler!
*/
/**
* @RestControllerAdvice = @ControllerAdvice + @ResponseBody
* @ControllerAdvice 代表当前类的异常处理controller!
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
}
声明异常处理hander方法
异常处理handler方法和普通的handler方法参数接收和响应都一致!
只不过异常处理handler方法要映射异常,发生对应的异常会调用!
普通的handler方法要使用@RequestMapping注解映射路径,发生对应的路径调用!
/**
* 异常处理handler
* @ExceptionHandler(HttpMessageNotReadableException.class)
* 该注解标记异常处理Handler,并且指定发生异常调用该方法!
*
*
* @param e 获取异常对象!
* @return 返回handler处理结果!
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public Object handlerJsonDateException(HttpMessageNotReadableException e){
return null;
}
/**
* 当发生空指针异常会触发此方法!
* @param e
* @return
*/
@ExceptionHandler(NullPointerException.class)
public Object handlerNullException(NullPointerException e){
return null;
}
/**
* 所有异常都会触发此方法!但是如果有具体的异常处理Handler!
* 具体异常处理Handler优先级更高!
* 例如: 发生NullPointerException异常!
* 会触发handlerNullException方法,不会触发handlerException方法!
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
public Object handlerException(Exception e){
return null;
}
配置文件扫描控制器类配置
确保异常处理控制类被扫描
<!-- 扫描controller对应的包,将handler加入到ioc-->
<context:component-scan base-package="com.atguigu.controller,
com.atguigu.exceptionhandler" />
拦截器和过滤器解决问题
生活中
为了提高乘车效率,在乘客进入站台前统一检票
程序中
在程序中,使用拦截器在请求到达具体 handler 方法前,统一执行检测
拦截器 VS 过滤器:
选择:
功能需要如果用 SpringMVC 的拦截器能够实现,就不使用过滤器。
创建拦截器类
public class Process01Interceptor implements HandlerInterceptor {
// 在处理请求的目标 handler 方法前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("request = " + request + ", response = " + response + ", handler = " + handler);
System.out.println("Process01Interceptor.preHandle");
// 返回true:放行
// 返回false:不放行
return true;
}
// 在目标 handler 方法之后,handler报错不执行!
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("request = " + request + ", response = " + response + ", handler = " + handler + ", modelAndView = " + modelAndView);
System.out.println("Process01Interceptor.postHandle");
}
// 渲染视图之后执行(最后),一定执行!
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("request = " + request + ", response = " + response + ", handler = " + handler + ", ex = " + ex);
System.out.println("Process01Interceptor.afterCompletion");
}
}
单个拦截器执行顺序:
拦截器配置
springmvc.xml
<!-- 配置拦截器-->
<mvc:interceptors>
<!-- 默认拦截器,拦截所有请求-->
<bean class="com.atguigu.interceptor.Process01Interceptor" />
</mvc:interceptors>
配置详解
<!-- 拦截全部的请求地址 -->
<mvc:interceptors>
<bean class="com.atguigu.mvc.interceptor.Process03Interceptor"/>
</mvc:interceptors>
<!-- 具体配置拦截器可以指定拦截的请求地址 -->
<mvc:interceptor>
<!-- 精确匹配 -->
<mvc:mapping path="/common/request/one"/>
<bean class="com.atguigu.mvc.interceptor.Process03Interceptor"/>
</mvc:interceptor>
<mvc:interceptor>
<!-- /*匹配路径中的一层 -->
<mvc:mapping path="/common/request/*"/>
<bean class="com.atguigu.mvc.interceptor.Process04Interceptor"/>
</mvc:interceptor>
<mvc:interceptor>
<!-- /**匹配路径中的多层 -->
<mvc:mapping path="/common/request/**"/>
<bean class="com.atguigu.mvc.interceptor.Process05Interceptor"/>
</mvc:interceptor>
排除配置
<mvc:interceptor>
<!-- /**匹配路径中的多层 -->
<mvc:mapping path="/common/request/**"/>
<!-- 使用 mvc:exclude-mapping 标签配置不拦截的地址 -->
<mvc:exclude-mapping path="/common/request/two/bbb"/>
<bean class="com.atguigu.mvc.interceptor.Process05Interceptor"/>
</mvc:interceptor>
一个网站有 56个资源,其中一个为登陆资源,两个无须登录即可访问,另外三个需要登录后才能访问。如果不登录就访问那三个资源,需要拦截,并且提示登录后访问访问!
提示:登陆为模拟登陆,存储一个user可以到session即可!
访问资源的请求地址可参考:
案例实现:
/**
* projectName: com.atguigu.controller
* description: 公有资源控制类
*/
@RestController
@RequestMapping("public/resource")
public class PublicController {
/**
* 模拟登录,将假用户数据存储到session中!
*/
@GetMapping("login")
public Object login(HttpSession session){
session.setAttribute("user","root");
return "login success!!";
}
@GetMapping("one")
public Object one(){
return "public one";
}
@GetMapping("two")
public Object two(){
return "public two";
}
}
@RestController
@RequestMapping("private/resource")
public class PrivateController {
@GetMapping("one")
public Object one(){
return "private one";
}
@GetMapping("two")
public Object two(){
return "private two";
}
@GetMapping("three")
public Object three(){
return "private two";
}
}
/**
* projectName: com.atguigu.interceptor
*
* description: 登录保护拦截器
*/
public class LoginProtectInterceptor implements HandlerInterceptor {
/**
* 登录保护方法
* @param request current HTTP request
* @param response current HTTP response
* @param handler chosen handler to execute, for type and/or instance evaluation
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Object user = request.getSession().getAttribute("user");
if (user == null){
response.setContentType("text/html;charset=utf-8");
//没有登录
response.getWriter().print("请先登录,再访问! <a href='/public/resource/login'>点击此处登录</a>");
//拦截,不到达目标地址
return false;
}
return true;
}
}
<!-- 配置拦截器-->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/private/**"/>
<bean class="com.atguigu.interceptor.LoginProtectInterceptor" />
</mvc:interceptor>
</mvc:interceptors>
在 Web 应用三层架构体系中,表述层负责接收浏览器提交的数据,业务逻辑层负责数据的处理。为了能够让业务逻辑层基于正确的数据进行处理,我们需要在表述层对数据进行检查,将错误的数据隔绝在业务逻辑层之外。
校验概述
JSR 303 是 Java 为 Bean 数据合法性校验提供的标准框架,它已经包含在 JavaEE 6.0 标准中。JSR 303 通过在 Bean 属性上标注类似于 @NotNull、@Max 等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证。
注解 | 规则 |
---|---|
@Null | 标注值必须为 null |
@NotNull | 标注值不可为 null |
@AssertTrue | 标注值必须为 true |
@AssertFalse | 标注值必须为 false |
@Min(value) | 标注值必须大于或等于 value |
@Max(value) | 标注值必须小于或等于 value |
@DecimalMin(value) | 标注值必须大于或等于 value |
@DecimalMax(value) | 标注值必须小于或等于 value |
@Size(max,min) | 标注值大小必须在 max 和 min 限定的范围内 |
@Digits(integer,fratction) | 标注值值必须是一个数字,且必须在可接受的范围内 |
@Past | 标注值只能用于日期型,且必须是过去的日期 |
@Future | 标注值只能用于日期型,且必须是将来的日期 |
@Pattern(value) | 标注值必须符合指定的正则表达式 |
JSR 303 只是一套标准,需要提供其实现才可以使用。Hibernate Validator 是 JSR 303 的一个参考实现,除支持所有标准的校验注解外,它还支持以下的扩展注解: |
注解 | 规则 |
---|---|
标注值必须是格式正确的 Email 地址 | |
@Length | 标注值字符串大小必须在指定的范围内 |
@NotEmpty | 标注值字符串不能是空字符串 |
@Range | 标注值必须在指定的范围内 |
Spring 4.0 版本已经拥有自己独立的数据校验框架,同时支持 JSR 303 标准的校验框架。Spring 在进行数据绑定时,可同时调用校验框架完成数据校验工作。在SpringMVC 中,可直接通过注解驱动 mvc:annotation-driven 的方式进行数据校验。Spring 的 LocalValidatorFactoryBean 既实现了 Spring 的 Validator 接口,也实现了 JSR 303 的 Validator 接口。只要在Spring容器中定义了一个LocalValidatorFactoryBean,即可将其注入到需要数据校验的 Bean中。Spring本身并没有提供JSR 303的实现,所以必须将JSR 303的实现者的jar包放到类路径下。
配置 mvc:annotation-driven 后,SpringMVC 会默认装配好一个 LocalValidatorFactoryBean,通过在处理方法的入参上标注 @Validated 注解即可让 SpringMVC 在完成数据绑定后执行数据校验的工作。
操作演示
导入依赖
<!-- 校验注解 -->
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-web-api</artifactId>
<version>9.1.0</version>
<scope>provided</scope>
</dependency>
<!-- 校验注解实现-->
<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.0.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator-annotation-processor -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator-annotation-processor</artifactId>
<version>8.0.0.Final</version>
</dependency>
应用校验注解
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;
import org.hibernate.validator.constraints.Length;
/**
* projectName: com.atguigu.pojo
*/
public class User {
//age 1 <= age < = 150
@Min(10)
private int age;
//name 3 <= name.length <= 6
@Length(min = 3,max = 10)
private String name;
//email 邮箱格式
@Email
private String email;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
handler标记和绑定错误收集
@RestController
@RequestMapping("user")
public class UserController {
/**
* @Validated 代表应用校验注解! 必须添加!
*/
@PostMapping("save")
public Object save(@Validated @RequestBody User user,
//在实体类参数和 BindingResult 之间不能有任何其他参数, BindingResult可以接受错误信息,避免信息抛出!
BindingResult result){
//判断是否有信息绑定错误! 有可以自行处理!
if (result.hasErrors()){
System.out.println("错误");
String errorMsg = result.getFieldError().toString();
return errorMsg;
}
//没有,正常处理业务即可
System.out.println("正常");
return user;
}
}
测试效果
易混总结
@NotNull、@NotEmpty、@NotBlank 都是用于在数据校验中检查字段值是否为空的注解,但是它们的用法和校验规则有所不同。
@NotNull (包装类型不为null)
@NotNull 注解是 JSR 303 规范中定义的注解,当被标注的字段值为 null 时,会认为校验失败而抛出异常。该注解不能用于字符串类型的校验,若要对字符串进行校验,应该使用 @NotBlank 或 @NotEmpty 注解。
@NotEmpty (集合类型长度大于0)
@NotEmpty 注解同样是 JSR 303 规范中定义的注解,对于 CharSequence、Collection、Map 或者数组对象类型的属性进行校验,校验时会检查该属性是否为 Null 或者 size()==0,如果是的话就会校验失败。但是对于其他类型的属性,该注解无效。需要注意的是只校验空格前后的字符串,如果该字符串中间只有空格,不会被认为是空字符串,校验不会失败。
@NotBlank (字符串,不为null,切不为" "字符串)
@NotBlank 注解是 Hibernate Validator 附加的注解,对于字符串类型的属性进行校验,校验时会检查该属性是否为 Null 或 “” 或者只包含空格,如果是的话就会校验失败。需要注意的是,@NotBlank 注解只能用于字符串类型的校验。
总之,这三种注解都是用于校验字段值是否为空的注解,但是其校验规则和用法有所不同。在进行数据校验时,需要根据具体情况选择合适的注解进行校验。
文件上传表单页面
位置:index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/save/picture" method="post" enctype="multipart/form-data">
昵称:<input type="text" name="nickName" value="龙猫" /><br/>
头像:<input type="file" name="headPicture" /><br/>
背景:<input type="file" name="backgroundPicture" /><br/>
<button type="submit">保存</button>
</form>
</body>
</html>
springmvc环境要求
pom.xml添加依赖
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
配置文件上传处理器(springmvc配置)
<!-- 文件上传处理器,可处理 multipart/* 请求并将其转换为 MultipartFile 对象-->
<bean id="multipartResolver"
class="org.springframework.web.multipart.support.StandardServletMultipartResolver">
</bean>
CommonsMultipartResolver的bean的id,必须是:multipartResolver
如果不是这个值,会在上传文件时报错
在web.xml
文件中添加 Multipart 配置
<servlet>
<servlet-name>yourAppServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<multipart-config>
<!-- 定义文件上传时所需的最大值,单位为字节 -->
<max-file-size>10485760</max-file-size>
<!-- 定义单个上传文件的最大值,单位为字节 -->
<max-request-size>20971520</max-request-size>
<!-- 定义内存中存储文件的最大值,超过此大小的文件会写入到硬盘中 -->
<file-size-threshold>5242880</file-size-threshold>
</multipart-config>
<load-on-startup>1</load-on-startup>
</servlet>
低版本web.xml约束文件,会爆红,不管担心,继续启动即可!
历史:Spring MVC 6之前,通常使用的是
CommonsMultipartResolver
来解析文件上传请求。但是在 Spring MVC 6中,此类已被移除,Spring 官方推荐使用StandardServletMultipartResolver
或MockMultipartResolver
来替代。
handler方法接收数据
/**
* 上传的文件使用 MultipartFile 类型接收其相关数据
* @param nickName
* @param picture
* @param backgroundPicture
* @return
* @throws IOException
*/
@PostMapping ("picture")
public String upload(String nickName, @RequestPart("headPicture") MultipartFile picture, @RequestPart("backgroundPicture")MultipartFile backgroundPicture) throws IOException {
System.out.println(nickName);
String inputName = picture.getName();
System.out.println("文件上传表单项的 name 属性值:" + inputName);
// 获取这个数据通常都是为了获取文件本身的扩展名
String originalFilename = picture.getOriginalFilename();
System.out.println("文件在用户本地原始的文件名:" + originalFilename);
String contentType = picture.getContentType();
System.out.println("文件的内容类型:" + contentType);
boolean empty = picture.isEmpty();
System.out.println("文件是否为空:" + empty);
long size = picture.getSize();
System.out.println("文件大小:" + size);
byte[] bytes = picture.getBytes();
System.out.println("文件二进制数据的字节数组:" + Arrays.asList(bytes));
InputStream inputStream = picture.getInputStream();
System.out.println("读取文件数据的输入流对象:" + inputStream);
Resource resource = picture.getResource();
System.out.println("代表当前 MultiPartFile 对象的资源对象" + resource);
return "home";
}
MultipartFile接口
文件转存
底层机制
本地转存
转存代码演示:
……
// 1、准备好保存文件的目标目录
// ①File 对象要求目标路径是一个物理路径(在硬盘空间里能够直接找到文件的路径)
// ②项目在不同系统平台上运行,要求能够自动兼容、适配不同系统平台的路径格式
// 例如:Window系统平台的路径是 D:/aaa/bbb 格式
// 例如:Linux系统平台的路径是 /ttt/uuu/vvv 格式
// 所以我们需要根据『不会变的虚拟路径』作为基准动态获取『跨平台的物理路径』
// ③虚拟路径:浏览器通过 Tomcat 服务器访问 Web 应用中的资源时使用的路径
String destFileFolderVirtualPath = "/head-picture";
// ④调用 ServletContext 对象的方法将虚拟路径转换为真实物理路径
String destFileFolderRealPath = servletContext.getRealPath(destFileFolderVirtualPath);
// 2、生成保存文件的文件名
// ①为了避免同名的文件覆盖已有文件,不使用 originalFilename,所以需要我们生成文件名
// ②我们生成文件名包含两部分:文件名本身和扩展名
// ③声明变量生成文件名本身
String generatedFileName = UUID.randomUUID().toString().replace("-","");
// ④根据 originalFilename 获取文件的扩展名
String fileExtname = originalFilename.substring(originalFilename.lastIndexOf("."));
// ⑤拼装起来就是我们生成的整体文件名
String destFileName = generatedFileName + "" + fileExtname;
// 3、拼接保存文件的路径,由两部分组成
// 第一部分:文件所在目录
// 第二部分:文件名
String destFilePath = destFileFolderRealPath + "/" + destFileName;
// 4、创建 File 对象,对应文件具体保存的位置
File destFile = new File(destFilePath);
// 5、执行转存
picture.transferTo(destFile);
……
缺陷
文件服务器转存(推荐)
好处
文件服务器类型
上传到其他模块
这种情况肯定出现在分布式架构中,常规业务功能不会这么做,采用这个方案的一定是特殊情况,这种情况极其少见。
在 MultipartFile 接口中有一个对应的方法:
/**
* Return a Resource representation of this MultipartFile. This can be used
* as input to the {@code RestTemplate} or the {@code WebClient} to expose
* content length and the filename along with the InputStream.
* @return this MultipartFile adapted to the Resource contract
* @since 5.1
*/
default Resource getResource() {
return new MultipartFileResource(this);
}
注释中说:这个 Resource 对象代表当前 MultipartFile 对象,输入给 RestTemplate 或 WebClient。而 RestTemplate 或 WebClient 就是用来在 Java 程序中向服务器端发出请求的组件。
在 Spring MVC 中,ResponseEntity
是用于表示 HTTP 响应的一个类,它既能设置响应体的内容,也能设置响应头相关的信息。
ResponseEntity
可以封装一个 HTTP 响应,包括响应体、响应头和响应状态码等属性,并将其发送回客户端。它提供了一种灵活的方式来表示 HTTP 响应,可以用于处理 RESTful API、文件下载、异常处理等应用场景。
演示json数据返回:
@GetMapping("/users/{age}")
public ResponseEntity<User> getUser(@PathVariable("age") int age) {
User user = new User();
user.setAge(age);
user.setEmail("test");
user.setName("二狗子");
return ResponseEntity.ok(user);
}
演示文件下载代码:
@Autowired
private ServletContext servletContext;
@RequestMapping("/download/file")
public ResponseEntity<byte[]> downloadFile() {
// 1.获取要下载的文件的输入流对象
// 这里指定的路径以 Web 应用根目录为基准
InputStream inputStream = servletContext.getResourceAsStream("/images/mi.jpg");
try {
// 2.将要下载的文件读取到字节数组中
// ①获取目标文件的长度
int len = inputStream.available();
// ②根据目标文件长度创建字节数组
byte[] buffer = new byte[len];
// ③将目标文件读取到字节数组中
inputStream.read(buffer);
// 3.封装响应消息头
// ①创建MultiValueMap接口类型的对象,实现类是HttpHeaders
MultiValueMap responseHeaderMap = new HttpHeaders();
// ②存入下载文件所需要的响应消息头
responseHeaderMap.add("Content-Disposition", "attachment; filename=mi.jpg");
// ③创建ResponseEntity对象
ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(buffer, responseHeaderMap, HttpStatus.OK);
// 4.返回responseEntity对象
return responseEntity;
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
生命周期环节 | 调用的方法 | 时机 | 次数 |
---|---|---|---|
创建对象 | 无参构造器 | 默认:第一次请求 修改:Web应用启动时 | 一次 |
初始化 | init(ServletConfig servletConfig) | 创建对象后 | 一次 |
处理请求 | service(ServletRequest servletRequest, ServletResponse servletResponse) | 接收到请求后 | 多次 |
清理操作 | destroy() | Web应用卸载之前 | 一次 |
类和接口之间的关系:
调用线路图:
调用线路图所示是方法调用的顺序,但是实际运行的时候本质上都是调用 DispatcherServlet 对象的方法。包括这里涉及到的接口的方法,也不是去调用接口中的『抽象方法』。毕竟抽象方法是没法执行的。抽象方法一定是在某个实现类中有具体实现才能被调用。
而对于最终的实现类:DispatcherServlet 来说,所有父类的方法最后也都是在 DispatcherServlet 对象中被调用的。
所在类:org.springframework.web.servlet.FrameworkServlet
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
Class<?> contextClass = getContextClass();
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException(
"Fatal initialization error in servlet with name '" + getServletName() +
"': custom WebApplicationContext class [" + contextClass.getName() +
"] is not of type ConfigurableWebApplicationContext");
}
// 通过反射创建 IOC 容器对象
ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
wac.setEnvironment(getEnvironment());
// 设置父容器
wac.setParent(parent);
String configLocation = getContextConfigLocation();
if (configLocation != null) {
wac.setConfigLocation(configLocation);
}
// 配置并且刷新:在这个过程中就会去读XML配置文件并根据配置文件创建bean、加载各种组件
configureAndRefreshWebApplicationContext(wac);
return wac;
}
所在类:org.springframework.web.servlet.FrameworkServlet
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
//springmvc容器认另一个容器(spring)作为父容器
cwac.setParent(rootContext);
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
wac = findWebApplicationContext();
}
if (wac == null) {
// 创建 IOC 容器
wac = createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
synchronized (this.onRefreshMonitor) {
onRefresh(wac);
}
}
if (this.publishContext) {
// 获取存入应用域时专用的属性名
String attrName = getServletContextAttributeName();
// 存入
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
看到这一点的意义:SpringMVC 有一个工具方法,可以从应用域获取 IOC 容器对象的引用。
工具类:org.springframework.web.context.support.WebApplicationContextUtils
工具方法:getWebApplicationContext()
@Nullable
public static WebApplicationContext getWebApplicationContext(ServletContext sc) {
return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}
作用:将来假如我们自己开发时,在IOC容器之外需要从IOC容器中获取bean,那么就可以通过这个工具方法获取IOC容器对象的引用。IOC容器之外的场景会有很多,比如在一个我们自己创建的Filter中。
FrameworkServlet.createWebApplicationContext()→configureAndRefreshWebApplicationContext()→wac.refresh()→触发刷新事件→org.springframework.web.servlet.DispatcherServlet.initStrategies()→org.springframework.web.servlet.DispatcherServlet.initHandlerMappings()
整个启动过程我们关心如下要点:
流程描述
核心代码
整个请求处理过程都是doDispatch()方法在宏观上协调和调度,把握了这个方法就理解了 SpringMVC 总体上是如何处理请求的。
所在类:DispatcherServlet
所在方法:doDispatch()
核心方法中的核心代码:
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
建立调用链
全类名:org.springframework.web.servlet.HandlerExecutionChain
拦截器索引默认是 -1,说明开始的时候,它指向第一个拦截器前面的位置。每执行一个拦截器,就把索引向前移动一个位置。所以这个索引每次都是指向当前拦截器。所以它相当于拦截器的指针。
对应操作
所在类:org.springframework.web.servlet.handler.AbstractHandlerMapping
所在方法:getHandlerExecutionChain()
关键操作:
把目标handler对象存入
把当前请求要经过的拦截器存入
结论:调用链是由拦截器和目标 handler 对象组成的。
调用拦截器preHandle()
所在类:org.springframework.web.servlet.DispatcherServlet
所在方法:doDispatch()
具体调用细节:正序调用
所在类:org.springframework.web.servlet.HandlerExecutionChain
所在方法:applyPreHandle
从这部分代码我们也能看到,为什么拦截器中的 preHandle() 方法通过返回布尔值能够控制是否放行。
注入请求参数
相关组件:
接口:org.springframework.web.servlet.HandlerAdapter
作用:字面含义是适配器的意思,具体功能有三个
所在类:org.springframework.web.servlet.DispatcherServlet
所在方法:doDispatch()
具体操作:调用目标 handler 方法
所在类:org.springframework.web.servlet.DispatcherServlet
所在方法:doDispatch()
具体操作:注入请求参数
通过反射给对应属性注入请求参数应该是下面的过程:
调用拦截器的 postHandle() 方法
所在类:org.springframework.web.servlet.DispatcherServlet
所在方法:doDispatch()
调用细节:从拦截器集合长度 - 1 开始循环,循环到 0 为止。所以是倒序执行,从而让各个拦截器形成嵌套执行的效果,和AOP有异曲同工之妙。
渲染视图
所有后续操作的入口:
所在类:org.springframework.web.servlet.DispatcherServlet
所在方法:doDispatch()
后续细节1:处理异常:
所在类:org.springframework.web.servlet.DispatcherServlet
所在方法:processDispatchResult()
后续细节2:渲染视图:
所在类:org.springframework.web.servlet.DispatcherServlet
所在方法:processDispatchResult()
补充细节:模型数据存入请求域的具体位置
所在类:org.thymeleaf.context.WebEngineContext.RequestAttributesVariablesMap
所在方法:setVariable()
调用拦截器的 afterCompletion() 方法
所在类:org.springframework.web.servlet.DispatcherServlet
所在方法:processDispatchResult()
调用细节:从拦截器索引开始循环,直到循环变量 i 被减到 0 为止。这样的效果是前面执行拦截器到哪里,就从哪里倒回去执行;前面没有执行的拦截器,现在也不执行。
断点位置基准:SpringMVC 版本采用 6.0.6 且源码已经下载,包含注释。
所在类 | 所在方法 | 断点行数 | 作用 |
---|---|---|---|
DispatcherServlet | doDispatch() | 1057 | 创建调用链对象 |
DispatcherServlet | doDispatch() | 1064 | 创建 HandlerAdapter 对象 |
DispatcherServlet | doDispatch() | 1076 | 调用拦截器 preHandle()方法 |
DispatcherServlet | doDispatch() | 1081 | 执行目标 handler 方法 |
DispatcherServlet | doDispatch() | 1088 | 调用拦截器 postHandle()方法 |
DispatcherServlet | doDispatch() | 1098 | 执行所有后续操作 |
DispatcherServlet | processDispatchResult() | 1145 | 处理异常 |
DispatcherServlet | processDispatchResult() | 1159 | 渲染视图 |
DispatcherServlet | processDispatchResult() | 1177 | 调用拦截器 afterCompletion()方法 |
目前情况:DispatcherServlet 加载 spring-mvc.xml,此时整个 Web 应用中只创建一个 IoC 容器。将来整合Mybatis、配置声明式事务,全部在 spring-mvc.xml 配置文件中配置也是可以的。可是这样会导致配置文件太长,不容易维护。
所以想到把配置文件分开:
配置文件分开之后,可以让 DispatcherServlet 加载多个配置文件。例如:
<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:spring-*.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
如果希望这两个配置文件使用不同的机制来加载:
此时会带来一个新的问题:在一个 Web 应用中就会出现两个 IOC 容器
注意:本节我们探讨的这个技术方案并不是『必须』这样做,而仅仅是『可以』这样做。
创建 spring-persist.xml
配置 ContextLoaderListener
<!-- 通过全局初始化参数指定 Spring 配置文件的位置 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-persist.xml</param-value>
</context-param>
<listener>
<!-- 指定全类名,配置监听器 -->
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
ContextLoaderListener
方法名 | 执行时机 | 作用 |
---|---|---|
contextInitialized() | Web 应用启动时执行 | 创建并初始化 IOC 容器 |
contextDestroyed() | Web 应用卸载时执行 | 关闭 IOC 容器 |
ContextLoader
ContextLoader 类是 ContextLoaderListener 类的父类。
指定配置文件位置的参数名:
/**
* Name of servlet context parameter (i.e., {@value}) that can specify the
* config location for the root context, falling back to the implementation's
* default otherwise.
* @see org.springframework.web.context.support.XmlWebApplicationContext#DEFAULT_CONFIG_LOCATION
*/
public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";
初始化 IOC 容器
方法名:initWebApplicationContext()
创建 IOC 容器
方法名:createWebApplicationContext()
打印两个 IOC 容器对象的 toString() 方法:
Object springIOC = servletContext.getAttribute("org.springframework.web.context.WebApplicationContext.ROOT");
log.debug(springIOC.toString());
Object springMVCIOC = servletContext.getAttribute("org.springframework.web.servlet.FrameworkServlet.CONTEXT.dispatcherServlet");
log.debug(springMVCIOC.toString());
打印效果是:
**Root **WebApplicationContext, started on Thu Jun 17 14:49:17 CST 2021
……
WebApplicationContext for namespace ‘dispatcherServlet-servlet’, started on Thu Jun 17 14:49:18 CST 2021, parent: Root WebApplicationContext
结论:两个组件分别创建的 IOC 容器是父子关系。
父子关系是如何决定的?
DispatcherServlet 创建的 IOC 容器设置父容器的源码截图:
所在类:org.springframework.web.servlet.FrameworkServlet
所在方法:createWebApplicationContext()
spring-mvc.xml配置方式:
<context:component-scan base-package="com.atguigu.spring.component.controller"/>
spring-persist.xml配置方式:
<context:component-scan base-package="com.atguigu.spring.component.service,com.atguigu.spring.component.dao"/>
所以bean所属IOC容器的关系:
结论:子容器中的 EmpController 装配父容器中的 EmpService 能够正常工作。说明子容器可以访问父容器中的bean。
分析:“子可用父,父不能用子”的根本原因是子容器中有一个属性 <span style=“color:blue;font-weight:bold;”>getParent()</span> 可以获取到父容器这个对象的引用。
源码依据:
查看日志确认是否重复创建了对象
Root WebApplicationContext: initialization started
……
Creating shared instance of singleton bean ‘helloDao’
Creating shared instance of singleton bean ‘helloHandler’
Creating shared instance of singleton bean ‘helloService’
……
Root WebApplicationContext initialized in 1150 ms
Refreshing WebApplicationContext for namespace ‘dispatcherServlet-servlet’
……
Creating shared instance of singleton bean ‘helloDao’
Creating shared instance of singleton bean ‘helloHandler’
Creating shared instance of singleton bean ‘helloService’
重复创建对象的问题
spring-mvc.xml:仅配置和处理请求相关的功能。所以不能给 service 类附加声明式事务功能。
结论:基于 spring-mvc.xml 配置文件创建的 EmpService 的 bean 不带有声明式事务的功能
影响:DispatcherServlet 处理浏览器请求时会调用自己创建的 EmpController,然后再调用自己创建的EmpService,而这个 EmpService 是没有事务的,所以处理请求时<span style=“color:blue;font-weight:bold;”>没有事务功能的支持</span>。
spring-persist.xml:配置声明式事务。所以可以给 service 类附加声明式事务功能。
结论:基于 spring-persist.xml 配置文件创建的 EmpService 有声明式事务的功能
影响:由于 DispatcherServlet 的 IOC 容器会优先使用自己创建的 EmpController,进而装配自己创建的EmpService,所以基于 spring-persist.xml 配置文件创建的有声明式事务的 EmpService 用不上。
解决重复创建对象的问题
解决方案一[建议使用]
让两个配置文件配置自动扫描的包时,各自扫描各自的组件。
解决方案二
如果由于某种原因,必须扫描同一个包,确实存在重复创建对象的问题,可以采取下面的办法处理。
具体spring-mvc.xml配置文件中的配置方式如下:
<!-- 两个Spring的配置文件扫描相同的包 -->
<!-- 为了解决重复创建对象的问题,需要进一步制定扫描组件时的规则 -->
<!-- 目标:『仅』包含@Controller注解标记的类 -->
<!-- use-default-filters="false"表示关闭默认规则,表示什么都不扫描,此时不会把任何组件加入IOC容器;
再配合context:include-filter实现“『仅』包含”效果 -->
<context:component-scan base-package="com.atguigu.spring.component" use-default-filters="false">
<!-- context:include-filter标签配置一个“扫描组件时要包含的类”的规则,追加到默认规则中 -->
<!-- type属性:指定规则的类型,根据什么找到要包含的类,现在使用annotation表示基于注解来查找 -->
<!-- expression属性:规则的表达式。如果type属性选择了annotation,那么expression属性配置注解的全类名 -->
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
具体spring-persist.xml配置文件中的配置方式如下:
<!-- 两个Spring的配置文件扫描相同的包 -->
<!-- 在默认规则的基础上排除标记了@Controller注解的类 -->
<context:component-scan base-package="com.atguigu.spring.component">
<!-- 配置具体排除规则:把标记了@Controller注解的类排除在扫描范围之外 -->
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
通过遵循 RESTful 架构的设计原则,可以构建出易于理解、可扩展、松耦合和可重用的 Web 服务。RESTful API 的特点是简单、清晰,并且易于使用和理解,它们使用标准的 HTTP 方法和状态码进行通信,不需要额外的协议和中间件。
RESTful 架构通常用于构建 Web API,提供数据的传输和操作。它可以用于各种应用场景,包括客户端-服务器应用、单页应用(SPA)、移动应用程序和微服务架构等。
总而言之,RESTful 是一种基于 HTTP 和标准化的设计原则的软件架构风格,用于设计和实现可靠、可扩展和易于集成的 Web 服务和应用程序