SpringBoot源码刨析C
- (三)、SpringBoot核心功能
- 2.Web
- 4.数据响应与内容协商
- (1).响应JSON
- (1.1)jackson.jar+@ResponseBody
- (1.1.1)、返回值解析器
- (1.1.2)、返回值解析器原理
- (1.2).SpringMVC到底支持哪些返回值
- (1.3)/HTTPMessageConverter原理
- (1.3.1)、MessageConverter规范
- (1.3.2)、默认的MessageConverter
- (2).内容协商 (MessageConverter)
- (2.1)、引入XML文件
- (2.2)、postman分别测试返回json和xml
- (2.3)、基于请求参数的内容协商 ⭐⭐
- (2.4)、内容协商原理
- (2.5)、自定义 MessageConverter
- (2.6)、运用参数的方式请求自定义内容协商
- 5.视图解析与模板引擎
- (1).视图解析
- (1.1)、视图解析原理流程
- (2).Thymeleaf基本语法
- (2.1)、表达式
- (2.2)、字面量
- (2.3)、文本操作
- (2.4)、数学运算
- (2.5)、布尔运算
- (2.6)、比较运算
- (2.7)、条件运算
- (2.8)、特殊操作
- (2.9)、设置属性值-th:attr
- (2.10)、迭代
- (2.11)、条件运算
- (3).Thymeleaf的使用
- (3.1)、引入依赖
- (3.2)、自动配置好了thymeleaf
- (3.2)、页面开发
- (4).后台管理系统总结
- (4.1)、举列子(公共方)
- (4.2)、配置拦截器
- (4.3)、文件上传(表单)
- 6.异常处理
- (1).错误处理
- (5.1)、默认规则
- (5.2)、定制错误处理逻辑 (==三种方法 ==)
- (5.3)、异常处理自动配置原理
- (5.4)、异常处理步骤流程
- 7.Web原生组件注入(Servlet、Filter、Listener)
- (1).使用Servlet API (第一种方式)
- (2).使用RegistrationBean (第二种方式)
- 8.嵌入式Servlet容器
- (1).切换嵌入式Servlet容器
- (2).定制Servlet容器
- 9.定制化原理
- (1).定制化的常见方式
- (2).原理分析套路
- 3.数据访问
- 1.SQL
- (1).数据源的自动配置-HikariDataSource
- (1.1) 、导入JDBC场景
- (1.2) 、分析自动配置JDBC引入了啥
- (1.3) 、修改配置项
- 2.使用Druid数据源
- (1).druid官方github地址
- (2).自定义方式 (使用德鲁伊数据源)
- (2.1)、创建数据源
- (2.2)、Spring时代配置德鲁伊数据源
- (2.3)、 配置类SpringBoot时代
- (3).使用官方starter方式
- (3.1)、引入druid-starter
- (3.2)、分析自动配置
- (3.3)、示列
- 3.Mybatis 操作数据库
- (1).原始(Spring) - 配置模式
- (2).非注解的配置模式
- (3).纯注解的配置模式
- (4).非注解和纯注解可以混合使用
- 4.Mybatis-Plus 操作数据库
- (1).什么是MybatisPlus
- (2).整合Mybatisplus
- 5.整合Redis 非SQL数据库
- (1).Redis 自动配置
- (2).RedisTemplate与Lettuce
- (3).切换至jedis
(三)、SpringBoot核心功能
2.Web
4.数据响应与内容协商
(1).响应JSON
(1.1)jackson.jar+@ResponseBody
在SpringMVC场景中,并不会自动给我们返回Json字符串的也没有Json字符串的过滤器,在SpringBoot中
只要我们使用了@ResponseBody (就会利用返回值处理器里面的消息转换器进行处理)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
web场景自动引入了json场景
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>
给前端自动返回json数据;
package com.jsxs.controller;
import com.jsxs.bean.Person;
import com.jsxs.bean.Pet;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.text.ParseException;
import java.text.SimpleDateFormat;
/**
* @Author Jsxs
* @Date 2023/7/6 10:16
* @PackageName:com.jsxs.controller
* @ClassName: ResponseTestController
* @Description: TODO
* @Version 1.0
*/
@Controller
@ResponseBody
public class ResponseTestController {
@GetMapping("/test/person")
public Person person() throws ParseException {
Person person = new Person("jsxs", 12, new SimpleDateFormat("yyyy-MM-dd").parse("2012-02-01"), new Pet("哈吉米", 2));
return person;
}
}
(1.1.1)、返回值解析器
15个方法返回值解析器
try {
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
RequestResponseBodyMethodProcessor 类。
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// 使用消息转换器进行调用
// Try even with null return value. ResponseBodyAdvice could get involved.
⭐ writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
(1.1.2)、返回值解析器原理
- 1、返回值处理器判断是否支持这种类型返回值 supportsReturnType
- 2、返回值处理器调用 handleReturnValue 进行处理
-
- 3、
RequestResponseBodyMethodProcessor
可以处理返回值标了@ResponseBody
注解的。 -
-
- 利用
MessageConverters
进行处理 将数据写为json
- 利用
-
- 1、内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型 浏览器接受7种)
- 1、内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型 浏览器接受7种)
-
- 2、服务器最终根据自己自身的能力,
决定服务器能生产出什么样内容类型的数据
,
- 2、服务器最终根据自己自身的能力,
-
-
3、SpringMVC会挨个遍历所有容器底层的
HttpMessageConverter (消息转换器)
,看谁能处理?
-
- 1、得到MappingJackson2HttpMessageConverter可以将对象写为json
-
- 2、利用MappingJackson2HttpMessageConverter将对象转为json再写出去。
-
-
-
- 3、
(1.2).SpringMVC到底支持哪些返回值
这里对应着15种返回值解析器
ModelAndView
Model
View
ResponseEntity
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
有 @ModelAttribute 且为对象类型的
@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor;
(1.3)/HTTPMessageConverter原理
(1.3.1)、MessageConverter规范
HttpMessageConverter: 看是否支持将 此 Class类型的对象,转为MediaType类型的数据。
例子:Person对象转为JSON。或者 JSON转为Person
(1.3.2)、默认的MessageConverter
0 - 只支持Byte类型的
1 - String
2 - String
3 - Resource
4 - ResourceRegion
5 - DOMSource.class \ SAXSource.class) \ StAXSource.class \StreamSource.class \Source.class
6 - MultiValueMap
7 - true (不管是谁直接为true,也就是说不管是啥文件直接接受)
8 - true (不管是谁直接为true,也就是说不管是啥文件直接接受)
9 - 支持注解方式 xml处理的。
最终 MappingJackson2HttpMessageConverter 把对象转为JSON(利用底层的jackson的objectMapper转换的)
·AbstractGenericHttpMessageConverter类种的·
outputMessage.getBody().flush();
(2).内容协商 (MessageConverter)
根据客户端接收能力不同,返回不同媒体类型的数据。
(2.1)、引入XML文件
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
(2.2)、postman分别测试返回json和xml
只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器本客户端可以接收的数据类型。
(2.3)、基于请求参数的内容协商 ⭐⭐
为了方便内容协商,开启基于请求参数的内容协商功能。
spring:
mvc:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式
通过点开源码我们发现我们需要什么样的类型,我们只需要在路径后面添加上 format='xxx'
格式即可。前提是我们需要什么类型的时候要有依赖在 pom.xml 中。比如我们需要使用xml的,我们要有上面的那个 jackson-dataformat-xml
依赖。
获取json: http://localhost:8080/test/person?format=json
获取xml格式: http://localhost:8080/test/person?format=xml
源码中发现相比于以前的请求头的内容协商多了一个参数的请求协商。
确定客户端接收什么样的内容类型;
1、Parameter策略优先确定是要返回json数据(获取请求头中的format的值
)
2、最终进行内容协商返回给客户端xml即可。
(2.4)、内容协商原理
- 1.判断当前响应头种是否已经有已经确定的媒体类型。MediateType
- 2.获取客户端(浏览器或者PostMan)支持的请求的
Accept头(浏览器)
。通过 contentNegotiationManager内容协商管理器
默认使用基于请求头的策略 -
- (1).先得到客户端能够接受的所有媒体类型是什么。
- 3.获取服务器能够生产的媒体类型。(
服务器方
) - 4.遍历服务器所有支持的媒体类型 进行 与客户端能够接受的类型进行匹配的操作,选择最佳匹配。
- 5.用支持将对象转为最佳媒体类型的converter,调用它进行转化。
为什么说引入xml的转环包就会被底层接受的原理
WebMvcConfigurationSupport 类下
917行 jackson2XmlPresent
(2.5)、自定义 MessageConverter
实现多协议数据兼容。json、xml、x-guigu
0、@ResponseBody
响应数据出去 调用 RequestResponseBodyMethodProcessor
处理
1、Processor 处理方法返回值。通过 MessageConverter 处理
2、所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
3、内容协商找到最终的 messageConverter;
package com.jsxs.controller;
import com.jsxs.bean.Person;
import com.jsxs.bean.Pet;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.text.ParseException;
import java.text.SimpleDateFormat;
/**
* @Author Jsxs
* @Date 2023/7/6 10:16
* @PackageName:com.jsxs.controller
* @ClassName: ResponseTestController
* @Description: TODO
* @Version 1.0
*/
@Controller
@ResponseBody
public class ResponseTestController {
/**
*
* @return
* @throws ParseException
*
* @TODO: 1.浏览器请求返回xml文件。2.ajax请求返回json文件。3.硅谷app请求返回自定义文件
* 在以前一个请求完成这项工作这是不可能完成的任务,但是在现在我们有了内容协商我们可以完成这个任务。
* 步骤: 1.添加自定义的MessageConverter进入系统底层。2.系统底层就会统计出所有MessageConverter
* 3.进行内容客户端与服务器内容协商匹配。
*/
@GetMapping("/test/person")
public Person person() throws ParseException {
Person person = new Person("jsxs", 12, new SimpleDateFormat("yyyy-MM-dd").parse("2012-02-01"), new Pet("哈吉米", 2));
return person;
}
}
SpringMVC的什么功能。一个入口给容器中添加一个 WebMvcConfigurer
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
}
}
首先配置协议转换器
package com.jsxs.convert;
import com.jsxs.bean.Person;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
/**
* @Author Jsxs
* @Date 2023/7/7 12:38
* @PackageName:com.jsxs.convert
* @ClassName: GguiGuMessageConverter
* @Description: TODO 自定义的消息转换器
* @Version 1.0
*/
public class GuiGuMessageConverter implements HttpMessageConverter<Person> {
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
}
/**
* 条件是什么
* @param clazz
* @param mediaType
* @return
*/
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return clazz.isAssignableFrom(Person.class); //只有返回的类型是Person类行就能进行读写
}
/**
* 服务器要统计所有的MessageConverter 都能写哪些内容类型
*
* application/x-jsxs
* @return
*/
@Override
public List<MediaType> getSupportedMediaTypes() {
return MediaType.parseMediaTypes("application/x-jsxs");
}
@Override
public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
// 自定义协议的写出: (也就是在返回的格式)
String data =person.getUserName()+";"+person.getAge()+";"+person.getAge()+";"+person.getPet();
// 写出去
OutputStream body = outputMessage.getBody();
body.write(data.getBytes());
}
}
package com.jsxs.config;
import com.jsxs.bean.Pet;
import com.jsxs.convert.GuiGuMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.util.UrlPathHelper;
import java.util.List;
/**
* @Author Jsxs
* @Date 2023/7/3 11:13
* @PackageName:com.jsxs.config
* @ClassName: WebConfig
* @Description: TODO
* @Version 1.0
*/
@Configuration(proxyBeanMethods = false)
// 第一种方式 @Configuration + 实现WebMvcConfigurer接口 (因为JDK8允许接口的默认方法和默认实现所以我们不需要将所有方法全部重写)
// 第二种方式: @Configuration +@Bean 重新注入我们的组件
public class WebConfig /*implements WebMvcConfigurer */{
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
hiddenHttpMethodFilter.setMethodParam("aaaa");
return hiddenHttpMethodFilter;
}
// @Override
// public void configurePathMatch(PathMatchConfigurer configurer) {
// UrlPathHelper helper = new UrlPathHelper();
// helper.setRemoveSemicolonContent(false);
// configurer.setUrlPathHelper(helper);
// }
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer(){
// 配置支持我们的矩阵注解
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper helper = new UrlPathHelper();
helper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(helper);
}
// 配置支持我们的自定义converter转换器
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Pet>() {
@Override
public Pet convert(String source) { //source 就是页面提交过来的值。只获得过来的值
if (!StringUtils.isEmpty(source)){ // 假如说提交的数据不为空
Pet pet = new Pet();
String[] split = source.split(",");
pet.setName(split[0]); // 逗号之前的设置成姓名
pet.setAge(Integer.parseInt(split[1]));
return pet;
}
return null;
}
});
}
// 扩展 内容消息转换器
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new GuiGuMessageConverter());
}
};
}
}
上面的内容可以通过 PostMan 进行处理。浏览器因为我们自己设置不了请求头,所以目前测试不了我们自定义的内容协商。
(2.6)、运用参数的方式请求自定义内容协商
我们通过debug的方式进入到了我们浏览器接受的内容协议上,并查看到有两种接收方式,并在请求参数的方式上没有看到 自定义的格式,所以我们要进行自定义的操作。
因为只兼容上面两种 所以我们要进行配置内容协商功能
package com.jsxs.config;
import com.jsxs.bean.Pet;
import com.jsxs.convert.GuiGuMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.StringUtils;
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
import org.springframework.web.accept.ParameterContentNegotiationStrategy;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.util.UrlPathHelper;
import java.util.*;
/**
* @Author Jsxs
* @Date 2023/7/3 11:13
* @PackageName:com.jsxs.config
* @ClassName: WebConfig
* @Description: TODO
* @Version 1.0
*/
@Configuration(proxyBeanMethods = false)
// 第一种方式 @Configuration + 实现WebMvcConfigurer接口 (因为JDK8允许接口的默认方法和默认实现所以我们不需要将所有方法全部重写)
// 第二种方式: @Configuration +@Bean 重新注入我们的组件
public class WebConfig /*implements WebMvcConfigurer */{
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
hiddenHttpMethodFilter.setMethodParam("aaaa");
return hiddenHttpMethodFilter;
}
// @Override
// public void configurePathMatch(PathMatchConfigurer configurer) {
// UrlPathHelper helper = new UrlPathHelper();
// helper.setRemoveSemicolonContent(false);
// configurer.setUrlPathHelper(helper);
// }
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer(){
// 配置支持我们的矩阵注解
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper helper = new UrlPathHelper();
helper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(helper);
}
// 配置支持我们的自定义converter转换器
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Pet>() {
@Override
public Pet convert(String source) { //source 就是页面提交过来的值。只获得过来的值
if (!StringUtils.isEmpty(source)){ // 假如说提交的数据不为空
Pet pet = new Pet();
String[] split = source.split(",");
pet.setName(split[0]); // 逗号之前的设置成姓名
pet.setAge(Integer.parseInt(split[1]));
return pet;
}
return null;
}
});
}
// 扩展 内容消息转换器
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new GuiGuMessageConverter());
}
// 自定义(重写)内容协商 ⭐⭐⭐
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
// 要求需要为String
HashMap<String, MediaType> mediaTypeHashMap = new HashMap<>();
// 配置支持的请求参数
mediaTypeHashMap.put("json",MediaType.APPLICATION_JSON);
mediaTypeHashMap.put("xml",MediaType.APPLICATION_XML);
mediaTypeHashMap.put("jsxs",MediaType.parseMediaType("application/x-jsxs"));
// 支持解析哪些参数对应的哪些媒体类型 -》 参数内容协商支持
ParameterContentNegotiationStrategy parameterContentNegotiationStrategy = new ParameterContentNegotiationStrategy(mediaTypeHashMap);
// 支持解析哪些参数对应的哪些媒体类型 -》 请求头内容协商支持 (这里通过PostMan进行测试)
HeaderContentNegotiationStrategy headerContentNegotiationStrategy = new HeaderContentNegotiationStrategy();
// 真正执行
configurer.strategies(Arrays.asList(parameterContentNegotiationStrategy,headerContentNegotiationStrategy));
}
};
}
}
有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。
大家考虑,上述功能除了我们完全自定义外?SpringBoot有没有为我们提供基于配置文件的快速修改媒体类型功能?怎么配置呢?【提示:参照SpringBoot官方文档web开发内容协商章节】
5.视图解析与模板引擎
视图解析:SpringBoot默认不支持 JSP
,需要引入第三方模板引擎技术实现页面渲染。
(1).视图解析
(1.1)、视图解析原理流程
1、目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer 里面。包括数据和视图地址
2、方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在 ModelAndViewContainer
3、任何目标方法执行完成以后都会返回 ModelAndView(数据和视图地址)。
4、processDispatchResult
处理派发结果(页面该如何响应)
DisplatchServlet的 第1078行
- 1、
render(mv, request, response)
; 进行页面渲染逻辑 -
-
1、根据
方法的String返回值
得到 View 对象【定义了页面的渲染逻辑】 -
- 1、所有的视图解析器尝试是否能根据当前返回值得到View对象 (for遍历尝试)
-
- 2、得到了 redirect:/main.html --> Thymeleaf new RedirectView()
-
- 3、ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。
- 3、ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。
-
- 4、view.render(mv.getModelInternal(), request, response); 视图对象调用自定义的render进行页面渲染工作
-
view.render(mv.getModelInternal(), request, response); 1393行
-
- RedirectView类 如何渲染【重定向到一个页面】
-
- 1、获取目标url地址
-
- 2、response.sendRedirect(encodedURL);
-
-
视图解析:
- 返回值以 forward: 开始: new InternalResourceView(forwardUrl); --> 转发request.getRequestDispatcher(path).forward(request, response);
- 返回值以 redirect: 开始: new RedirectView() --》 render就是重定向
(2).Thymeleaf基本语法
(2.1)、表达式
(2.2)、字面量
文本值: ‘one text’ , ‘Another one!’ ,…数字: 0 , 34 , 3.0 , 12.3 ,…布尔值: true , false
空值: null
变量: one,two,… 变量不能有空格
(2.3)、文本操作
字符串拼接: +
变量替换: |The name is ${name}|
(2.4)、数学运算
运算符: + , - , * , / , %
(2.5)、布尔运算
运算符: and , or
一元运算: ! , not
(2.6)、比较运算
比较: > , < , >= , <= ( gt , lt , ge , le )等式: == , != ( eq , ne )
(2.7)、条件运算
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
(2.8)、特殊操作
无操作: _
(2.9)、设置属性值-th:attr
设置单个值
<form action="subscribe.html" th:attr="action=@{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
</fieldset>
</form>
设置多个值
<img src="../../images/gtvglogo.png" th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
以上两个的代替写法 th:xxxx
<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
<form action="subscribe.html" th:action="@{/subscribe}">
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#setting-value-to-specific-attributes
行内写法
1. 假如要写的内容不在标签中而在行内,那么就用这个方式。(非session)
<h1>[[${xxx}]]</h1>
2. 假如是取Session的值
<h1>[[$session.name.xxx}]]</h1>
(2.10)、迭代
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
(2.11)、条件运算
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p>
<p th:case="#{roles.manager}">User is a manager</p>
<p th:case="*">User is some other thing</p>
</div>
(3).Thymeleaf的使用
(3.1)、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
(3.2)、自动配置好了thymeleaf
ThymeleafAutoConfiguration 类
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration { }
自动配好的策略
- 1、所有thymeleaf的配置值都在
ThymeleafProperties 类
- 2、配置好了
SpringTemplateEngine
- 3、配好了
ThymeleafViewResolver
- 4、我们只需要直接开发页面
(3.2)、页面开发
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Success</h1>
<h1 th:text="${A}"></h1>
<a th:href="${baidu}">点击我去金橘社区 ${baidu}</a>
<br>
<br>
<a th:href="@{baidu}">点击我去金橘社区 @{baidu}</a>
<br>
<br>
<a th:href="@{/baidu}">点击我去金橘社区 @{/baidu}</a>
</body>
</html>
package com.jsxs.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
/**
* @Author Jsxs
* @Date 2023/7/7 17:30
* @PackageName:com.jsxs.controller
* @ClassName: ViewTestController
* @Description: TODO
* @Version 1.0
*/
@Controller
public class ViewTestController {
@GetMapping("/toTest")
public String toTest(Model model){
// model 会自动放入到请求域中和HttpRequest是一样的,只能接受一次请求的操作
model.addAttribute("A","a");
model.addAttribute("baidu","https://www.jsxs1.cn");
return "success"; // 假如说没有模板解析器的话,这里的路径会报黄。
}
}
# 给整个服务器添加前缀
server:
servlet:
context-path: /jsxs
(4).后台管理系统总结
1. 假如在template中再新建包的话,我们只需要在 controller 的返回值中添加上新建包路径即可 /新建包名/xxx。
2. controller 页面跳转的实质是转发;不是重定向。
3. return: 的值会默认拼接 templates/xxxx.html; return forward: return redirect 找的都是请求的路径不是页面。
4. 静态资源只要放在四大区域就行,前端调用的时候可以省略掉前面的四大区域路径只写相对路径即可。
5. 抽取公共模板(第一种)
(1). 公共页(top.html): 在标签中设置 th:fragment="AAAA"
eg: <div th:fragment="AAAA"></div>
(2). 使用公共页方: th:insert="~{公共页的HTML名字 :: AAAA}"
eg: 1. <div th:insert="~{top :: AAAA}"></div>
或 2.<div th:insert="top :: AAAA"></div>
或 3.<div th:replace="~{top :: utopbar}"></div>
或 4.<div th:include="~{top :: topbar}"></div>
假如说公共页面和被添加公共页面不再同一个包中那么就要加上路径指定在哪
eg:<div th:include="~{commons/top :: topbar}"></div>
6.抽取公共模板(第二种 ->选择器方式)
(1). 公共页(top.html): 在标签中设置 id="BBB"
(2). 使用方: <div th:replace="top :: #BBB"> (当然以上的几种方法都适用)
公共方:
<footer th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</footer>
使用公共方
<body>
...
会带上div(全部都要)
<div th:insert="~{footer :: copy}"></div>
引入的东西不会在div里面
<div th:replace="~{footer :: copy}"></div>
引入的中西会在div里面
<div th:include="~{footer :: copy}"></div>
</body>
实际效果:
假如引入的是css、js。也是一样的只不过把div改为link或script
<body>
...
<div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
</div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
<div>
© 2011 The Good Thymes Virtual Grocery
</div>
</body>
(4.1)、举列子(公共方)
1.公共方的页面
2.replace->引入的东西会在link标签里面内嵌link标签
<link th:replace="~{}">
3.include->引入的东西会在link标签里面内嵌link标签
<link th:include="~{}">
4.标签改为div,div是万能的。
<div th:replace="~{}"></div>
(4.2)、配置拦截器
SpringMvc的拦截器配置
1.Filter: 是Servlet定义的原生组件。好处:脱离Spring也能够使用
2.Interceptor: 是Spring定义的接口。可以使用Spring的自动装配功能
第一种拦截机制
package com.jsxs.servlet;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
/**
* @Author Jsxs
* @Date 2023/7/8 17:58
* @PackageName:com.jsxs.servlet
* @ClassName: MyFilter
* @Description: TODO 配置过滤器
* @Version 1.0
*/
@Slf4j
//@WebFilter(urlPatterns = {"/css/*","/imag/*"}) //设置拦截我们CSS和imag的所有文件
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("MyFilter初始化完成.....");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("MyFilter工作中.....");
chain.doFilter(request,response);
}
@Override
public void destroy() {
log.info("MyFilter初始化销毁.....");
}
}
第二种拦截机制
在这里进行拦截的操作
package com.jsxs.config;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LoginHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 登入成功之后存在登入后的session
if (request.getSession().getAttribute("LoginUser")!=null){
return true;
}else {
request.setAttribute("msg","没有权限请先进行登入");
request.getRequestDispatcher("/index.html").forward(request,response);
return false;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
在这里设定规则
/**
* 1、编写一个拦截器实现HandlerInterceptor接口
* 2、拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors)
* 3、指定拦截规则【如果是拦截所有,静态资源也会被拦截】
*/
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") //所有请求都被拦截包括静态资源
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); //放行的请求
}
}
拦截器原理:
- 根据当前请求,找到HandlerExecutionChain【可以处理请求的handler以及handler的所有 拦截器】
- 先来
顺序
执行所有拦截器的preHandle
方法
-
- 如果当前拦截器prehandler返回为
true(通行)
。则执行下一个拦截器的preHandle
- 如果当前拦截器prehandler返回为
-
- 如果当前拦截器返回为
false(不通行)
。直接倒序
执行所有已经执行了的拦截器的 afterCompletion;
- 如果当前拦截器返回为
- 如果任何一个拦截器返回false。
直接跳出不执行目标方法
所有拦截器都返回True。执行目标方法
倒序执行
所有拦截器的postHandle方法。- 前面的步骤有任何异常都会直接倒序触发
afterCompletion
- 页面成功渲染完成以后,也会倒序触发 afterCompletion
如图:
(4.3)、文件上传(表单)
1. 需要指定 enctype
2. 单个文件上传 什么也不加
3. 多个文件上传 需要加上: multiple 属性
<form method="post" action="/upload" enctype="multipart/form-data">
单文件 <input type="file" name="file"><br>
多文件 <input type="file" name="file" multiple><br>
<input type="submit" value="提交">
</form>
1.一定要加上参数注解 @RequestPart
/**
* MultipartFile 自动封装上传过来的文件,以后文件上传用这个类型和注解
* @param email
* @param username
* @param headerImg
* @param photos
* @return
*/
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,
@RequestPart("photos") MultipartFile[] photos) throws IOException {
log.info("上传的信息:email={},username={},headerImg={},photos={}",
email,username,headerImg.getSize(),photos.length);
if(!headerImg.isEmpty()){
//保存到文件服务器,OSS服务器
String originalFilename = headerImg.getOriginalFilename(); //获取上传的文件名
headerImg.transferTo(new File("H:\\cache\\"+originalFilename)); //这个方法直接传输了,内部封装了InputStrean和OutStream流(需要指定路径+文件名即可)
}
if(photos.length > 0){
for (MultipartFile photo : photos) {
if(!photo.isEmpty()){
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File("H:\\cache\\"+originalFilename)); //遍历传输各个文件
}
}
}
return "main";
}
原理
MultipartAutoConfiguration 类
MultipartProperties 类
- 自动配置原理
文件上传自动配置类-MultipartAutoConfiguration-MultipartProperties
- 自动配置好了
StandardServletMultipartResolver
【文件上传解析器】 - 原理步骤
-
- 1、请求进来使用文件上传解析器判断(
isMultipart
)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求`
- 1、请求进来使用文件上传解析器判断(
-
- 2、参数解析器来解析请求中的文件内容封装成`MultipartFile
-
- 3、将request中文件信息封装为一个
Map
;MultiValueMap<String, MultipartFile>
FileCopyUtils。实现文件流的拷贝
- 3、将request中文件信息封装为一个
6.异常处理
(1).错误处理
(5.1)、默认规则
- 默认情况下,Spring Boot提供
/error
处理所有错误的映射 - 对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据
要对其进行自定义,添加View解析为error
- 要完全替换默认行为,可以实现 ErrorController 并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制但替换其内容。
静态资源或templates目录下放 error/下的4xx,5xx页面会被自动解析
(5.2)、定制错误处理逻辑 (==三种方法 ==)
- 自定义错误页 (第一种)
-
- error/404.html error/5xx.html;
有精确的错误状态码页面就匹配精确,没有就找 4xx.htm
l;如果都没有就触发白页
- error/404.html error/5xx.html;
在自定义的4xx页面或者5xx页面的某个提示标签中可以这么写
th:text="${status}" ->在配置的错误页面中会获得返回的状态码
th:text="${message}" ->在配置的错误页面中会获得返回的信息
@ControllerAdvice+@ExceptionHandler
处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的 (第二种⭐)
- @ResponseStatus+自定义异常 ;底层是 ResponseStatusExceptionResolver ,把responsestatus注解的信息底层调用 response.sendError(statusCode, resolvedReason);tomcat发送的/error (第三种)
先定义: 编写有参构造和无参构造,并不是继承父类的方法
后使用
- Spring底层的异常,如 参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常。
-
- response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
- ErrorViewResolver 实现自定义处理异常;
-
- response.sendError 。error请求就会转给controller
-
- 你的异常没有任何人能处理。tomcat底层 response.sendError。error请求就会转给controller
-
- basicErrorController 要去的页面地址是 ErrorViewResolver ;
(5.3)、异常处理自动配置原理
ErrorMvcAutoConfiguration
自动配置异常处理规则-
- 容器中的组件:类型:
DefaultErrorAttributes
-> id:errorAttributes -
- public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
-
- DefaultErrorAttributes:定义错误页面中可以包含哪些数据。
- 容器中的组件:类型:
容器中的组件:类型:BasicErrorController --> id:basicErrorController(json+白页 适配响应)
-
- 处理默认 /error 路径的请求;页面响应 new ModelAndView(“error”, model);
-
- 容器中有组件 View->id是error;(响应默认错误页)
-
- 容器中放组件 BeanNameViewResolver(视图解析器);按照返回的视图名作为组件的id去容器中找View对象。
-
- 容器中的组件:类型:DefaultErrorViewResolver -> id:conventionErrorViewResolver
-
- 如果发生错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面
-
- error/404、5xx.html
-
(5.4)、异常处理步骤流程
1、执行目标方法,目标方法运行期间有任何异常都会被catch、而且标志当前请求结束;并且用 dispatchException
2、进入视图解析流程(页面渲染?)
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
3、mv = processHandlerException;处理handler发生的异常,处理完成返回ModelAndView;
- 1、遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】
- 系统默认的 异常解析器;
- 1、DefaultErrorAttributes先来处理异常。把异常信息保存到rrequest域,并且返回null;
- 2、默认没有任何人能处理异常,所以异常会被抛出
-
- 1、如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController处理
-
- 2、解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。
-
- 3、默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html
-
- 4、模板引擎最终响应这个页面 error/500.html
-
7.Web原生组件注入(Servlet、Filter、Listener)
(1).使用Servlet API (第一种方式)
首先要在主类中设置需要扫描的servlet包在哪。
1. @ServletComponentScan(basePackages = "com.atguigu.admin") :指定原生Servlet组件都放在那里,默认是扫描主类所在包的子包
然后我们在servlet包中配置我们的原生servlet
2. @WebServlet(urlPatterns = "/my"):效果:直接响应,没有经过Spring的拦截器?
3. @WebFilter(urlPatterns={"/css/*","/images/*"})
4. @WebListener
扩展:DispatchServlet 如何注册进来
-
容器中自动配置了
DispatcherServlet
属性绑定到WebMvcProperties
;对应的配置文件配置项是spring.mvc
。
-
实质上通过
ServletRegistrationBean<DispatcherServlet>
把 DispatcherServlet 配置进来。
-
默认映射的是 / 路径。
Tomcat-Servlet;
多个Servlet都能处理到同一层路径,精确优选原则
A: /my/ ->(假如说这个路径可以处理my/下的所有路径)
B: /my/1 -> (假如说这个路径可以处理my/1这个准确路径)
主类
package com.jsxs;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@SpringBootApplication
@ServletComponentScan
public class SpringBootLs02Application {
public static void main(String[] args) {
SpringApplication.run(SpringBootLs02Application.class, args);
}
}
servlet
package com.jsxs.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author Jsxs
* @Date 2023/7/8 17:24
* @PackageName:com.jsxs.servlet
* @ClassName: MyServlet
* @Description: TODO 1.添加上注解. 2.重写get和post方法
* @Version 1.0
*/
@WebServlet(urlPatterns = "/my") //标注是原生的API,并指定拦截路径
public class MyServlet extends HttpServlet {
// 请求后的真实业务
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("666666"); //拦截后展示666
}
// 请求后的真实业务
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
Filter
package com.jsxs.servlet;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
/**
* @Author Jsxs
* @Date 2023/7/8 17:58
* @PackageName:com.jsxs.servlet
* @ClassName: MyFilter
* @Description: TODO 配置过滤器
* @Version 1.0
*/
@Slf4j
@WebFilter(urlPatterns = {"/css/*","/imag/*"}) //设置拦截我们CSS和imag的所有文件
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("MyFilter初始化完成.....");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("MyFilter工作中.....");
chain.doFilter(request,response);
}
@Override
public void destroy() {
log.info("MyFilter初始化销毁.....");
}
}
Listener
package com.jsxs.servlet;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
/**
* @Author Jsxs
* @Date 2023/7/8 18:05
* @PackageName:com.jsxs.servlet
* @ClassName: MyServletContextListener
* @Description: TODO
* @Version 1.0
*/
@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MyServletContextListener监听器初始化完成.....");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MyServletContextListener监听器销毁完成.....");
}
}
(2).使用RegistrationBean (第二种方式)
去掉以上方法的各个注解(但保留类),并添加一个配置类实现三个原生API。
主类
package com.jsxs;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@SpringBootApplication
@ServletComponentScan
public class SpringBootLs02Application {
public static void main(String[] args) {
SpringApplication.run(SpringBootLs02Application.class, args);
}
}
servlet: 将注册的组件删除
package com.jsxs.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author Jsxs
* @Date 2023/7/8 17:24
* @PackageName:com.jsxs.servlet
* @ClassName: MyServlet
* @Description: TODO 1.添加上注解. 2.重写get和post方法
* @Version 1.0
*/
//@WebServlet(urlPatterns = "/my") //标注是原生的API,并指定拦截的路径(就是不放行)
public class MyServlet extends HttpServlet {
// 请求后的真实业务
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("666666"); //拦截后展示666
}
// 请求后的真实业务
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
Filter:将注册的组件删除
package com.jsxs.servlet;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
/**
* @Author Jsxs
* @Date 2023/7/8 17:58
* @PackageName:com.jsxs.servlet
* @ClassName: MyFilter
* @Description: TODO 配置过滤器
* @Version 1.0
*/
@Slf4j
//@WebFilter(urlPatterns = {"/css/*","/imag/*"}) //设置拦截我们CSS和imag的所有文件
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("MyFilter初始化完成.....");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("MyFilter工作中.....");
chain.doFilter(request,response);
}
@Override
public void destroy() {
log.info("MyFilter初始化销毁.....");
}
}
Listener:将注册的组件删除
package com.jsxs.servlet;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
/**
* @Author Jsxs
* @Date 2023/7/8 18:05
* @PackageName:com.jsxs.servlet
* @ClassName: MyServletContextListener
* @Description: TODO
* @Version 1.0
*/
@Slf4j
//@WebListener
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MyServletContextListener监听器初始化完成.....");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MyServletContextListener监听器销毁完成.....");
}
}
MyRegisterConfig 配置类实现
package com.jsxs.config;
import com.jsxs.servlet.MyFilter;
import com.jsxs.servlet.MyServlet;
import com.jsxs.servlet.MyServletContextListener;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
/**
* @Author Jsxs
* @Date 2023/7/8 18:14
* @PackageName:com.jsxs.config
* @ClassName: MyServlet
* @Description: TODO
* @Version 1.0
*/
@Configuration
public class MyRegisterConfig {
@Bean
public ServletRegistrationBean myServlet() {
MyServlet myServlet = new MyServlet(); // //这个是我们写在Servlet包中的 MyServlet类
return new ServletRegistrationBean(myServlet, "/my01", "/my02"); // 第一个参数是myServlet实列对象,第二个参数是映射的路径
}
@Bean
public FilterRegistrationBean myFilter() {
MyFilter myFilter = new MyFilter(); //这个是我们写在Servlet包中的 MyFilter类
// 1.第一种
// return new FilterRegistrationBean(myFilter,myServlet()); //第一个参数是MyFilter实列对象,第二个参数是ServletRegistrationBean对象 ->就是只拦截"/my01","/my02"这两个路径
// 2.第二种
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter); // MyFilter实列对象
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my", "/css/*")); // 拦截的路径
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener() {
MyServletContextListener mySwervletContextListener = new MyServletContextListener(); //这个是我们写在Servlet包中的 MyServletContextListener类
return new ServletListenerRegistrationBean(mySwervletContextListener); // MyServletContextListener实列对象
}
}
8.嵌入式Servlet容器
(1).切换嵌入式Servlet容器
- 默认支持的
webServer
Tomcat
,Jetty
, orUndertow
ServletWebServerApplicationContext
容器启动寻找ServletWebServerFactory
并引导创建服务器
ServletWebServerApplicationContext: 174行
private void createWebServer() {
WebServer webServer = this.webServer;
// 1.获取servletContext容器
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
// 2.创建服务器容器
StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
ServletWebServerFactory factory = getWebServerFactory();
createWebServer.tag("factory", factory.getClass().toString());
// ⭐⭐3.获取Tomcate容器
this.webServer = factory.getWebServer(getSelfInitializer());
createWebServer.end();
getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(this.webServer));
getBeanFactory().registerSingleton("webServerStartStop",
new WebServerStartStopLifecycle(this, this.webServer));
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}
- 切换服务器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
- 原理
-
SpringBoot应用启动发现当前是Web应用。web场景包-导入tomcat
-
web应用会创建一个web版的ioc容器 ServletWebServerApplicationContext
-
ServletWebServerApplicationContext 启动的时候
寻找 ServletWebServerFactory
(Servlet 的web服务器工厂—> Servlet 的web服务器) -
SpringBoot底层默认有3个的WebServer工厂;
TomcatServletWebServerFactory
,JettyServletWebServerFactory
, orUndertowServletWebServerFactory
-
底层直接会有一个自动配置类 。
ServletWebServerFactoryAutoConfiguration类
-
ServletWebServerFactoryAutoConfiguration
导入了ServletWebServerFactoryConfiguration
(配置类) -
ServletWebServerFactoryConfiguration 配置类
根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包
),容器中就有 TomcatServletWebServerFactory -
TomcatServletWebServerFactory 创建出Tomcat服务器并启动
;TomcatWebServer 的构造器拥有初始化方法initialize—this.tomcat.start();
-
内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)
-
(2).定制Servlet容器
-
实现
WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>
- 把配置文件的值和
ServletWebServerFactory
进行绑定
- 把配置文件的值和
-
修改配置文件 server.xxx (
第一种方式
)
-
直接自定义 ConfigurableServletWebServerFactory (
第二种
)
因为我们项目已启动ServletWebServerFactory就会引导创建服务器,所以我们直接引入子类集大成者即可。
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;
@Component
public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
@Override
public void customize(ConfigurableServletWebServerFactory server) {
server.setPort(9000);
}
}
9.定制化原理
(1).定制化的常见方式
- 修改配置文件;
- xxxxxCustomizer;
- 编写自定义的配置类 xxxConfiguration;+ @Bean替换、增加容器中默认组件;视图解析器
- Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能;+ @Bean给容器中再扩展一些组件
@Configuration
public class AdminWebConfig implements WebMvcConfigurer
@EnableWebMvc + WebMvcConfigurer + @Configuration
—— @Bean 可以全面接管SpringMVC,所有规则全部自己重新配置; 实现定制和扩展功能-
原理
-
- 1、
WebMvcAutoConfiguration
默认的SpringMVC的自动配置功能类。静态资源、欢迎页…
- 1、
-
- 2、一旦使用
@EnableWebMvc
、。会 @Import(DelegatingWebMvcConfiguration.class)
- 2、一旦使用
-
- 3、DelegatingWebMvcConfiguration 的 作用,
只保证SpringMVC最基本的使用
-
- 把所有系统中的 WebMvcConfigurer 拿过来。
所有功能的定制都是这些 WebMvcConfigurer 合起来一起生效 (也就是说手动生效了)
- 把所有系统中的 WebMvcConfigurer 拿过来。
-
- 自动配置了一些非常底层的组件。RequestMappingHandlerMapping、这些组件依赖的组件都是从容器中获取
-
- public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
- 3、DelegatingWebMvcConfiguration 的 作用,
-
- 4、WebMvcAutoConfiguration 里面的配置要能生效 必须
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
- 4、WebMvcAutoConfiguration 里面的配置要能生效 必须
-
- 5、@EnableWebMvc 导致了
WebMvcAutoConfiguration
没有生效。
● … …
- 5、@EnableWebMvc 导致了
-
(2).原理分析套路
场景starter - xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties – 绑定配置文件项
3.数据访问
1.SQL
(1).数据源的自动配置-HikariDataSource
(1.1) 、导入JDBC场景
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
数据库驱动?
为什么导入JDBC场景,官方不导入驱动?
官方不知道我们接下要操作什么数据库(MySQL.Orangle)。
数据库版本和驱动版本对应
SpringBoot默认版本:<mysql.version>8.0.22</mysql.version>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!-- <version>5.1.49</version>-->
</dependency>
想要修改版本
1、直接依赖引入具体版本(maven的就近依赖原则)
2、重新声明版本(maven的属性的就近优先原则)
<properties>
<java.version>1.8</java.version>
<mysql.version>5.1.6</mysql.version>
</properties>
(1.2) 、分析自动配置JDBC引入了啥
自动配置的类
- DataSourceAutoConfiguration :
数据源的自动配置
- 修改数据源相关的配置:
spring.datasource
- 数据库连接池的配置,是自己容器中没有
DataSource
才自动配置的 - 底层配置好的连接池是:HikariDataSource
- 修改数据源相关的配置:
@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration
-
DataSourceTransactionManagerAutoConfiguration:
事务管理器的自动配置
-
JdbcTemplateAutoConfiguration:
JdbcTemplate的自动配置,可以来对数据库进行crud
-
可以修改这个配置项@ConfigurationProperties(prefix = “spring.jdbc”) 来修改JdbcTemplate
-
@Bean@Primary JdbcTemplate;
容器中有这个组件(所以我们自己使用即可)
-
-
JndiDataSourceAutoConfiguration:
jndi的自动配置
-
XADataSourceAutoConfiguration:
分布式事务相关的
(1.3) 、修改配置项
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_account
username: root
password: ******
driver-class-name: com.mysql.jdbc.Driver
配置数据源成功且各项信息正确
引入了JDBC和驱动 对数据源没有进行配置
2.使用Druid数据源
HikariDataSource: 是世界上性能最好的数据源。
Druid: 是世界上安全属性最高的数据源。
(1).druid官方github地址
德鲁伊帮助文档
https://github.com/alibaba/druid
整合第三方技术的两种方式
- 自定义
- 找starter
(2).自定义方式 (使用德鲁伊数据源)
(2.1)、创建数据源
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.17</version>
</dependency>
(2.2)、Spring时代配置德鲁伊数据源
1. dataSource
主要作用: 链接数据库和开启监控和防火墙等功能。
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="maxActive" value="20" />
<property name="initialSize" value="1" />
<property name="maxWait" value="60000" />
<property name="minIdle" value="1" />
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<property name="minEvictableIdleTimeMillis" value="300000" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<property name="poolPreparedStatements" value="true" />
<property name="maxOpenPreparedStatements" value="20" />
/**
*
* @return 主要用途链接数据库和配置数据库信息
*/
// @ConditionalOnMissingBean(DataSource.class) ->当数据中没有的话,才会使用默认的数据源
@Bean // 这里有数据源了所以使用我们配置的数据源
// @ConfigurationProperties("spring.datasource") // 因为德鲁伊的链接名和Hik的连接名一致,我们可以使用这个
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
// 1.链接数据库
druidDataSource.setUrl("jdbc:mysql://localhost:3306/demo1");
druidDataSource.setUsername("root");
druidDataSource.setPassword("121788");
druidDataSource.setDriverClassName("com.mysql.jdbc.Driver");
// 2.打开SQL监控和防火墙
druidDataSource.setFilters("stat,wall");
System.out.println(druidDataSource.getClass());
return druidDataSource;
}
2. 配置 StatViewServle
StatViewServlet的用途包括:
提供监控信息展示的html页面
提供监控信息的JSON API
<servlet>
<servlet-name>DruidStatView</servlet-name>
<servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DruidStatView</servlet-name>
<url-pattern>/druid/*</url-pattern>
</servlet-mapping>
/**
*
* @return : 主要提供监控视图 和 JSON API的操作
*/
@Bean
public ServletRegistrationBean StatViewServlet(){
StatViewServlet statViewServlet = new StatViewServlet();
ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<>(statViewServlet, "/druid/*");
return registrationBean;
}
3.StatFilter
用于统计监控信息;如WEB监控
、URI监控
<filter>
<filter-name>DruidWebStatFilter</filter-name>
<filter-class>com.alibaba.druid.support.http.WebStatFilter</filter-class>
<init-param>
<param-name>exclusions</param-name>
<param-value>*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>DruidWebStatFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
/**
*
* @return WebStatFilter用于采集web-URL关联监控的数据。
*/
@Bean
public FilterRegistrationBean WebStatFilter(){
WebStatFilter webStatFilter = new WebStatFilter();
FilterRegistrationBean<WebStatFilter> registrationBean = new FilterRegistrationBean<>(webStatFilter);
registrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); //排除这些路径
registrationBean.setUrlPatterns(Arrays.asList("/*")); // 拦截这个路径
return registrationBean;
}
(2.3)、 配置类SpringBoot时代
package com.jsxs.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
/**
* @Author Jsxs
* @Date 2023/7/9 14:28
* @PackageName:com.jsxs.config
* @ClassName: MyDuridConfig
* @Description: TODO 德鲁伊数据源配置
* @Version 1.0
*/
@Configuration
public class MyDruidConfig {
/**
*
* @return 主要用途链接数据库和配置数据库信息
*/
// @ConditionalOnMissingBean(DataSource.class) ->当数据中没有的话,才会使用默认的数据源
@Bean // 这里有数据源了所以使用我们配置的数据源
// @ConfigurationProperties("spring.datasource") // 因为德鲁伊的链接名和Hik的连接名一致,我们可以使用这个
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
// 1.链接数据库
druidDataSource.setUrl("jdbc:mysql://localhost:3306/demo1");
druidDataSource.setUsername("root");
druidDataSource.setPassword("121788");
druidDataSource.setDriverClassName("com.mysql.jdbc.Driver");
// 2.打开SQL监控和防火墙
druidDataSource.setFilters("stat,wall");
System.out.println(druidDataSource.getClass());
return druidDataSource;
}
/**
*
* @return : 主要提供监控视图 和 JSON API的操作
*/
@Bean
public ServletRegistrationBean StatViewServlet(){
StatViewServlet statViewServlet = new StatViewServlet();
ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<>(statViewServlet, "/druid/*");
// 1.添加监控的密码和账户
registrationBean.addInitParameter("loginUsername","1");
registrationBean.addInitParameter("loginPassword","1");
return registrationBean;
}
/**
*
* @return WebStatFilter用于采集web-URL关联监控的数据。
*/
@Bean
public FilterRegistrationBean WebStatFilter(){
WebStatFilter webStatFilter = new WebStatFilter();
FilterRegistrationBean<WebStatFilter> registrationBean = new FilterRegistrationBean<>(webStatFilter);
registrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); //排除这些路径
registrationBean.setUrlPatterns(Arrays.asList("/*")); // 拦截这个路径
return registrationBean;
}
}
(3).使用官方starter方式
(3.1)、引入druid-starter
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
@Deprecated // 标注已经过时,
(3.2)、分析自动配置
我们发现德鲁伊的包中自己带了自动配置的包
并且按需导入的方式进行导入 (即屏蔽掉以前SpringBoot默认的数据源)
- 原来的项:
spring.datasource
依然存在 - 扩展配置项
spring.datasource.druid
DruidSpringAopConfiguration.class
, 监控SpringBean的配置;配置项:spring.datasource.druid.aop-patterns
DruidStatViewServletConfiguration.class
, 监控页的配置:spring.datasource.druid.stat-view-servlet
;默认开启DruidWebStatFilterConfiguration.class
, web监控配置;spring.datasource.druid.web-stat-filter
;默认开启DruidFilterConfiguration.class
所有Druid自己filter的配置
private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat";
private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config";
private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding";
private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j";
private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j";
private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2";
private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log";
private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall";
(3.3)、示列
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_account
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
druid:
aop-patterns: com.atguigu.admin.* #监控SpringBean
filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙)
stat-view-servlet: # 配置监控页功能
enabled: true
login-username: admin
login-password: admin
resetEnable: false
web-stat-filter: # 监控web
enabled: true
urlPattern: /*
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
filter:
stat: # 对上面filters里面的stat的详细配置
slow-sql-millis: 1000
logSlowSql: true
enabled: true
wall:
enabled: true
config:
drop-table-allow: false
https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter
Druid 高级配置属性列表…
3.Mybatis 操作数据库
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
(1).原始(Spring) - 配置模式
- 全局配置文件
- SqlSessionFactory: 自动配置好了
- SqlSession:自动配置了 SqlSessionTemplate 组合了SqlSession
- @Import(AutoConfiguredMapperScannerRegistrar.class);
- Mapper: 只要我们写的操作MyBatis的接口标准了
@Mapper
就会被自动扫描进来
@EnableConfigurationProperties(MybatisProperties.class) : MyBatis配置项绑定类。
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration{}
@ConfigurationProperties(prefix = "mybatis") 前缀
public class MybatisProperties
MybatisX 插件。 这个插件可以实现映射跳转
命名空间指向的是 接口。 返回值结果是 实体类 。
(2).非注解的配置模式
application.yaml
mybatis:
config-location: classpath:mybatis/mybatis-config.xml #指定mybatis的核心文件位置
mapper-locations: classpath:mybatis/mapper/*.xml #指定mybatis的映射文件位置
实体类
package com.jsxs.bean;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Author Jsxs
* @Date 2023/7/17 11:01
* @PackageName:com.jsxs.bean
* @ClassName: Admin
* @Description: TODO
* @Version 1.0
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Admin {
Integer id;
String name;
String password;
}
mapper层接口: mapper注解是为了让mybatis发现,respoity目的是为了让
package com.jsxs.mapper;
import com.jsxs.bean.Admin;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* @Author Jsxs
* @Date 2023/7/17 11:01
* @PackageName:com.jsxs.mapper
* @ClassName: AdminMapper
* @Description: TODO
* @Version 1.0
*/
@Mapper
@Repository
public interface AdminMapper {
// 1.查询所有的用户
public List<Admin> allAdmin();
}
mybatis.xml映射SQL文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jsxs.mapper.AdminMapper">
<select id="allAdmin" resultType="com.jsxs.bean.Admin">
select *from admin;
</select>
</mapper>
接口
package com.jsxs.service;
import com.jsxs.bean.Admin;
import com.jsxs.mapper.AdminMapper;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @Author Jsxs
* @Date 2023/7/17 13:39
* @PackageName:com.jsxs.service
* @ClassName: AdminService
* @Description: TODO
* @Version 1.0
*/
public interface AdminService {
public List<Admin> allAdmin();
}
接口实现类
package com.jsxs.service.impl;
import com.jsxs.bean.Admin;
import com.jsxs.mapper.AdminMapper;
import com.jsxs.service.AdminService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
/**
* @Author Jsxs
* @Date 2023/7/17 13:44
* @PackageName:com.jsxs.service.impl
* @ClassName: AdminServiceImpl
* @Description: TODO
* @Version 1.0
*/
@Service
public class AdminServiceImpl implements AdminService {
@Resource
AdminMapper adminMapper;
@Override
public List<Admin> allAdmin() {
return adminMapper.allAdmin();
}
}
测试实验
package com.jsxs.service.impl;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import static org.junit.jupiter.api.Assertions.*;
/**
* @Author Jsxs
* @Date 2023/7/17 13:47
* @PackageName:com.jsxs.service.impl
* @ClassName: AdminServiceImplTest
* @Description: TODO
* @Version 1.0
*/
@SpringBootTest
class AdminServiceImplTest {
@Resource
AdminServiceImpl adminService;
@Test
void test(){
System.out.println(adminService.allAdmin());
}
}
Mybatis 默认是不支持我们的数据库字段的驼峰命名的。假如说数据库的字段名的间隔以下划线分割,那么实体类应该写成驼峰命名。那么如果我们没有配置mybatis使用驼峰命名那么我们就会查询到的数据为null。
第一种: 使用全局配置文件配置
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="multipleResultSetsEnabled" value="true"/>
</settings>
</configuration>
第二种: 使用application.yaml
mybatis:
config-location: classpath:mybatis/mybatis-config.xml #指定mybatis的核心文件位置
mapper-locations: classpath:mybatis/mapper/*.xml #指定mybatis的映射文件位置
configuration:
multiple-result-sets-enabled: true ⭐
注意: 全局配置文件(mybatis-config)与application.yaml中只能存在一个对mybatis进行配置。
基本步骤:
- 导入mybatis官方starter
- 编写mapper接口。标准@Mapper注解
- 编写sql映射文件并绑定mapper接口
- 在application.yaml中指定Mapper配置文件的位置,以及指定全局配置文件的信息 (建议;配置在mybatis.configuration)
(3).纯注解的配置模式
如果我们使用注解开发的话,我们就可以省略掉xml映射文件而不是xml全局配置文件(mybatis-config.xml)
package com.jsxs.mapper;
import com.jsxs.bean.Admin;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* @Author Jsxs
* @Date 2023/7/17 11:01
* @PackageName:com.jsxs.mapper
* @ClassName: AdminMapper
* @Description: TODO
* @Version 1.0
*/
@Mapper
@Repository
public interface AdminMapper {
// 1.查询所有的用户
public List<Admin> allAdmin();
// 2.根据id进行查找数据 ⭐⭐
@Select("select *from admin where id = #{id}")
public Admin getAdminById(Long id);
}
Service 接口实现类
package com.jsxs.service;
import com.jsxs.bean.Admin;
import com.jsxs.mapper.AdminMapper;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @Author Jsxs
* @Date 2023/7/17 13:39
* @PackageName:com.jsxs.service
* @ClassName: AdminService
* @Description: TODO
* @Version 1.0
*/
public interface AdminService {
public List<Admin> allAdmin();
public Admin getAdminById(Long id);
}
Service接口
package com.jsxs.service.impl;
import com.jsxs.bean.Admin;
import com.jsxs.mapper.AdminMapper;
import com.jsxs.service.AdminService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
/**
* @Author Jsxs
* @Date 2023/7/17 13:44
* @PackageName:com.jsxs.service.impl
* @ClassName: AdminServiceImpl
* @Description: TODO
* @Version 1.0
*/
@Service
public class AdminServiceImpl implements AdminService {
@Resource
AdminMapper adminMapper;
@Override
public List<Admin> allAdmin() {
return adminMapper.allAdmin();
}
@Override
public Admin getAdminById(Long id) {
return adminMapper.getAdminById(id);
}
}
测试结果
package com.jsxs.service.impl;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import static org.junit.jupiter.api.Assertions.*;
/**
* @Author Jsxs
* @Date 2023/7/17 13:47
* @PackageName:com.jsxs.service.impl
* @ClassName: AdminServiceImplTest
* @Description: TODO
* @Version 1.0
*/
@SpringBootTest
class AdminServiceImplTest {
@Resource
AdminServiceImpl adminService;
@Test
void test(){
System.out.println(adminService.allAdmin());
System.out.println(adminService.getAdminById(1L));
}
}
(4).非注解和纯注解可以混合使用
我们只需要各自写各自的即可,即非注解的要写 xml映射文件和全局配置文件;非注解的不需要xml映射文件但需要全局文件
package com.jsxs.mapper;
import com.jsxs.bean.Admin;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* @Author Jsxs
* @Date 2023/7/17 11:01
* @PackageName:com.jsxs.mapper
* @ClassName: AdminMapper
* @Description: TODO
* @Version 1.0
*/
@Mapper
@Repository
public interface AdminMapper {
// 1.查询所有的用户
public List<Admin> allAdmin();
// 2.根据id进行查找数据
@Select("select *from admin where id = #{id}")
public Admin getAdminById(Long id);
// 3.xinz
@Insert("insert into admin(name,password) values(#{name},#{password})")
@Options(useGeneratedKeys = true,keyColumn = "id") // 指定主键是那个?
public int add(String name,String password);
}
最佳实践
- 引入mybatis-starter
- 配置application.yaml中,指定
mapper-location
位置即可;config-location:
核心配置文件就不需要了。核心配置文件最好直接再application.yaml中使用。 - 编写
Mapper接口并标注@Mapper注解
- 简单方法直接注解方式
- 复杂方法编写mapper.xml进行绑定映射
@MapperScan("com.atguigu.admin.mapper")
简化,其他的Mapper接口就可以不用标注@Mapper注解
4.Mybatis-Plus 操作数据库
(1).什么是MybatisPlus
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
mybatis plus 官网 https://baomidou.com/
建议安装 MybatisX 插件
(2).整合Mybatisplus
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
帮助我们自动引入了JDBC和Mybatis的包,所以我们不需要再引入JDBC和Mybatis了
自动配置
- MybatisPlusAutoConfiguration 配置类,MybatisPlusProperties 配置项绑定。
mybatis-plus:xxx 就是对mybatis-plus的定制
- SqlSessionFactory 自动配置好。
底层是容器中默认的数据源
mapperLocations 自动配置好的。有默认值 (3.1.2下才有默认值)
。classpath*:/mapper/**/*.xml;任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。 建议以后sql映射文件,放在 mapper下- 容器中也自动配置好了 SqlSessionTemplate
- @Mapper 标注的接口也会被自动扫描;建议直接
@MapperScan("com.atguigu.admin.mapper") 批量扫描就行
优点:
- 只需要我们的
Mapper继承 BaseMapper
就可以拥有crud能力。
- 数据库层继承的是: BaseMapper
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml #3.1.2版本之后有默认的值了。
实体类
为什么说:我们这里写类名为 Admin ; 然后Mybatisplus是怎么找到数据库的具体表的? 因为默认有一个注解 @TableName(“xxx”) xxx默认是写的是类名,就会通过这个类名去数据库中寻找。如果表名更改了,那么我们可以通过这个注解更改映射的关系。
package com.jsxs.bean;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Author Jsxs
* @Date 2023/7/17 11:01
* @PackageName:com.jsxs.bean
* @ClassName: Admin
* @Description: TODO
* @Version 1.0
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Admin {
@TableField(exist = true) // 假如我们需要临时的属性,但这个属性不存在我们可以再上面添加一个注解 @TableField(exist = false)表示在表中不存在
Integer id;
String name;
String password;
}
Mapper接口-> 需要继承一个接口
package com.jsxs.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jsxs.bean.Admin;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* @Author Jsxs
* @Date 2023/7/17 11:01
* @PackageName:com.jsxs.mapper
* @ClassName: AdminMapper
* @Description: TODO 继承接口之后我们不需要再编写 CRUD 就自带的已经存在了
* @Version 1.0
*/
@Mapper
@Repository
public interface AdminMapper extends BaseMapper<Admin> { // 需要继承BaseMapper,因为这个父类有很多已经分装好的方法
}
测试实现
package com.jsxs.mapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import static org.junit.jupiter.api.Assertions.*;
/**
* @Author Jsxs
* @Date 2023/7/18 11:03
* @PackageName:com.jsxs.mapper
* @ClassName: AdminMapperTest
* @Description: TODO
* @Version 1.0
*/
@SpringBootTest
class AdminMapperTest {
@Resource
AdminMapper adminMapper;
@Test
void test(){
System.out.println(adminMapper.selectById(1));
}
}
- 业务层接口
继承的是 IService<操作的实体类名>
; 业务层实现类继承业务层的实现类要继承ServiceImpl<要实现表的BaseMapper,操作表对应的实体类名> 且 要实现业务层的接口
实体类
package com.jsxs.bean;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Author Jsxs
* @Date 2023/7/17 11:01
* @PackageName:com.jsxs.bean
* @ClassName: Admin
* @Description: TODO
* @Version 1.0
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Admin {
@TableField(exist = true) // 假如我们需要临时的属性,但这个属性不存在我们可以再上面添加一个注解 @TableField(exist = false)表示在表中不存在
Integer id;
String name;
String password;
}
Mapper接口
package com.jsxs.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jsxs.bean.Admin;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* @Author Jsxs
* @Date 2023/7/17 11:01
* @PackageName:com.jsxs.mapper
* @ClassName: AdminMapper
* @Description: TODO 继承接口之后我们不需要再编写 CRUD 就自带的已经存在了
* @Version 1.0
*/
@Mapper
@Repository
public interface AdminMapper extends BaseMapper<Admin> { // 需要继承BaseMapper,因为这个父类有很多已经分装好的方法
}
Service 接口
package com.jsxs.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jsxs.bean.Admin;
/**
* @Author Jsxs
* @Date 2023/7/18 11:26
* @PackageName:com.jsxs.service
* @ClassName: AdminService
* @Description: TODO 我们业务层的接口只需要实现IService<Admin>
* @Version 1.0
*/
public interface AdminService extends IService<Admin> {
}
业务层实现类接口:
package com.jsxs.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jsxs.bean.Admin;
import com.jsxs.mapper.AdminMapper;
import com.jsxs.service.AdminService;
import org.springframework.stereotype.Service;
import java.io.Serializable;
/**
* @Author Jsxs
* @Date 2023/7/18 11:27
* @PackageName:com.jsxs.service.impl
* @ClassName: AdminServiceImpl
* @Description: TODO 业务层的实现类要继承ServiceImpl<要实现表的BaseMapper,操作表对应的实体类名> 且 要实现业务层的接口
* @Version 1.0
*/
@Service
public class AdminServiceImpl extends ServiceImpl<AdminMapper,Admin> implements AdminService {
}
测试类
package com.jsxs.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jsxs.bean.Admin;
import com.jsxs.service.AdminService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import static org.junit.jupiter.api.Assertions.*;
/**
* @Author Jsxs
* @Date 2023/7/18 11:44
* @PackageName:com.jsxs.service.impl
* @ClassName: AdminServiceImplTest
* @Description: TODO
* @Version 1.0
*/
@SpringBootTest
@Slf4j
class AdminServiceImplTest {
@Resource
AdminService adminService;
@Test
void test(){
System.out.println(adminService.getById(1));
Page<Admin> page = new Page<>(1, 2); //当前页(初始页为1),一页几条
IPage<Admin> iPage = adminService.page(page, null); // Page分页的实列
log.info("全部数据为: {}",iPage.getRecords()); // 分页遍历的话,遍历这个
log.info("当前业为: {}",iPage.getCurrent());
log.info("总多少页: {}",iPage.getPages());
log.info("总多少条: {}",iPage.getTotal());
}
}
假如说查询到的数据都为0,那么就是没有配置分页插件
package com.jsxs.config;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author Jsxs
* @Date 2023/7/18 13:05
* @PackageName:com.jsxs.config
* @ClassName: MybatisPlusConfig
* @Description: TODO
* @Version 1.0
*/
@Configuration
public class MybatisPlusConfig {
// 分页插件
@Bean
public PaginationInterceptor paginationinterceptor(){
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
return paginationInterceptor;
}
}
为何Controller中调用接口就能实现接口实现类里的方法?
- 如何在删除之后仍然在原来的页面 ?
我们只需要在前端的删除按钮上绑定上当前页的页码,然后在后端进行页码的重定向跳转。
前端的操作:
后端的操作
5.整合Redis 非SQL数据库
Redis是一个开源(BSD许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。
(1).Redis 自动配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
自动配置:
- RedisAutoConfiguration 自动配置类。
RedisProperties 属性类 --> spring.redis.xxx是对redis的配置
连接工厂是准备好的
。LettuceConnectionConfiguration、JedisConnectionConfiguration (默认配置好了客户端)自动注入了RedisTemplate<Object, Object>
: xxxTemplate;自动注入了StringRedisTemplate
;k:v都是String- key:value
- 底层只要我们使用
StringRedisTemplate、RedisTemplate就可以操作redis
redis环境搭建
如果我们使用window的话,我们在启动redis服务端的时候需要指定使用哪个配置 redis.windows-service.conf
redis.windows.conf
。在这里我们使用redis.windows.conf
设定了密码。
两种打开方式:
-
键入“cmd”切到安装目录后输出redis-server.exe redis.windows.conf,回车,就可以了。
-
在redis安装目录下新建文件startup.bat后,右击“编辑”,或者先用记事本建立该文件,再把扩展名改一下,文件里面写上:redis-server.exe redis.windows.conf。保存,以后再运行就直接运行这个文件,不要再直接运行redis-server.exe了,就可以了
(2).RedisTemplate与Lettuce
spring:
redis:
host: 127.0.0.1
port: 6379
password: xxxxx
package com.jsxs.mapper;
import com.alibaba.fastjson2.JSON;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import javax.annotation.Resource;
import static org.junit.jupiter.api.Assertions.*;
/**
* @Author Jsxs
* @Date 2023/7/18 11:03
* @PackageName:com.jsxs.mapper
* @ClassName: AdminMapperTest
* @Description: TODO
* @Version 1.0
*/
@SpringBootTest
class AdminMapperTest {
@Resource
RedisTemplate redisTemplate;
@Test
void test2(){
ValueOperations<String, String> opsForValue = redisTemplate.opsForValue();
opsForValue.set(JSON.toJSONString("hello5"),JSON.toJSONString("hello5"));
System.out.println(opsForValue.get("hello5"));
}
}
需要进行序列化处理
没有对 redisTemplate
进行序列化处理,java 代码会将 key
转化成对应的十六进制
的数值进行操作.
注意事项: 我们不管通过 Java代码/Redis客户端 进行设置值还是获取值,我们都需要对其进行JSON
(如果不是java会获取不到值)处理并且序列化
(序列化可以避免默认的16进制)
package com.jsxs.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.net.UnknownHostException;
/**
* @Author Jsxs
* @Date 2023/7/20 13:56
* @PackageName:com.jsxs.config
* @ClassName: RedisConfig
* @Description: TODO
* @Version 1.0
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
// 将template 泛型设置为 <String, Object>
RedisTemplate<String, Object> template = new RedisTemplate();
// 连接工厂,不必修改
template.setConnectionFactory(redisConnectionFactory);
/*
* 序列化设置
*/
// key、hash的key 采用 String序列化方式
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// value、hash的value 采用 Jackson 序列化方式
template.setValueSerializer(RedisSerializer.json());
template.setHashValueSerializer(RedisSerializer.json());
template.afterPropertiesSet();
return template;
}
}
假如说key键值对的值是用的JOSN字符串,那么Java端获取的值就是JSON字符串。如果键值对的值用的是非JSON字符串,那么Java后端获取的值就是非JSON字符串。
总结: 以后开发全部使用JSON字符串。
(3).切换至jedis
客户端类型有: lettuce 和 jedis 这两种。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 导入jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
redis:
host: 127.0.0.1
port: 6379
password: 121788
client-type: jedis #指定客户端的类型为jedis
@Test
void test3(){
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.auth("121788");
System.out.println(jedis.ping());
jedis.set("username1","123456");
System.out.println(jedis.get("username1"));
}
总结: 不管是 Letture 还是Jedis客户端,这两个客户端都需要用到序列化和 JSON 。