springboot
定义
springboot可以轻松创建独立的,生产级的基于spring的应用程序
springboot直接嵌入tomcat/jetty/undertow/, 可以直接运行springboot程序, 比如: java -jar myboot.jar
quickstart
继承父工程 spring-boot-starter-parent:2.5.3
引入依赖:spring-boot-starter-web
创建启动类
1
2
3
4
5
6
@SpringBootApplication
public class MainApp {
public static void main(String[] args) {
SpringApplication.run(MainApp.class, args);
}
}
controller
1
2
3
4
5
6
7
8
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "hello boot!";
}
}
直接运行启动类的main方法, 就这么简洁
spring-boot-starter-web
- 引入了很多框架 包括spring,springmvc, json, tomcat等
boot与spring与mvc的关系
他们的关系大概是: Spring Boot > Spring > Spring MVC主题 1
Spring MVC 只是 Spring 处理 WEB 层请求的一个模块/组件, Spring MVC 的基石是 Servlet
Spring 的核心是 IOC 和 AOP, IOC 提供了依赖注入的容器 , AOP 解决了面向切面编程
Spring Boot 是为了简化开发, 推出的封神框架(约定优于配置[COC],简化了 Spring 项目 的配置流程), SpringBoot包含很多组件/框架,Spring就是最核心的内容之一,也包含 Spring MVC
Spring 家族,有众多衍生框架和组件例如 boot、security、jpa 等, 他们的基础都是 Spring
约定优于配置
convention over configuration/coc,按约定编程
是一种转件设计规范, 本质上是对系统、类库或框架中一些东西假定一个大众化合理的默认值(缺省值)
例如在模型中存在一个名为 User 的类,那么对应到数据库会存在一个名为 user 的表, 只有在偏离这个约定时才需要做相关的配置 (例如你想将表名命名为t_user等非user时才 需要写关于这个名字的配置)
简单来说就是假如你所期待的配置与约定的配置一致,那么就可以不做任何配置,约 定不符合期待时, 才需要对约定进行替换配置
总结
- 约定其实就是一种规范,遵循了规范,那么就存在通用性,存在通用性,那么事情就会变 得相对简单,程序员之间的沟通成本会降低,工作效率会提升,合作也会变得更加简单
依赖管理和starter
- spring-boot-starter-parent 还有父项目spring-boot-dependencies, 声明了开发中常用的依赖的版本号
2.自动版本仲裁: , 即如果没有指定某个依赖 jar 的版本,则以父项目指定的版本为准
starter场景启动器
-
开发中我们引入了相关场景的 starter,这个场景中所有的相关依赖都引入进来了,比如 我们做 web 开发引入了,该 starter 将导入与 web 开发相关的所有包
-
依赖树 : 可以看到 spring-boot-starter-web ,帮我们引入了 spring-webmvc,spring-web 开发模块,还引入了 spring-boot-starter-tomcat 场景,spring-boot-starter-json 场景,这些 场景下面又引入了一大堆相关的包,这些依赖项可以快速启动和运行一个项目,提高开发 效率.
-
所有场景启动器最基本的依赖就是 spring-boot-starter , 前面的依赖树分析可以看到,这个依赖也就是 SpringBoot 自动配置的核心依赖
官方提供starter
-
文档 https://docs.spring.io/spring-boot/reference/using/build-systems.html#using.build-systems.starters
-
在开发中我们经常会用到 spring-boot-starter-xxx ,比如 spring-boot-starter-web,该场 景是用作 web 开发,也就是说 xxx 是某种开发场景。
-
我们只要引入 starter,这个场景的所有常规需要的依赖我们都自动引入
第三方 starter
-
SpringBoot 也支持第三方 starter
-
第三方 starter 不要从 spring-boot 开始,因为这是官方 spring-boot 保留的命名方式的。 第三方启动程序通常以项目名称开头。例如,名为 thirdpartyproject 的第三方启动程序项 目通常被命名为 thirdpartyproject-spring-boot-starter。
-
也就是说:xxx-spring-boot-starter 是第三方为我们提供的简化开发的场景启动器
自动配置
SSM 整合时,需要配置 Tomcat 、配置 SpringMVC、配置如 何扫描包、配置字符过滤器、配置视图解析器、文件上传等[如图],非常麻烦。而在 SpringBoot 中,存在自动配置机制,提高开发效率
spring-boot-starter-web自动配置了 Tomcat,自动配置 SpringMVC
默 认 扫 描 包 结 构: 启动类同一目录下的所有子包
-
指定包扫描,也可以指定多个包 @SpringBootApplication(scanBasePackages = “com.xxx”)
-
查看容器中有哪些bean代码
1 2 3
ConfigurableApplicationContext ctx = SpringApplication.run(MainApp.class, args); String[] definitionNames = ctx.getBeanDefinitionNames(); Arrays.stream(definitionNames).forEach(System.out::println);
resources\application.properties
-
SpringBoot 项目最重要也是最核心的配置文件就是application.properties,所有的框架配 置都可以在这个配置文件中说明
-
配置大全地址https://blog.csdn.net/pbrlovejava/article/details/82659702
-
使用时直接在里面搜索即可
resources\application.properties 自定义配置
在 properties 文件中自定义配置,通过@Value(“${}”)获取对应属性值
-
application.properties 文件 my.website=https://www.baidu.com //某个 Bean
1 2
@Value("${my.website}") private String bdUrl;
springboot在哪里读取application.properties配置
-
ConfigFileApplicationListener这个类指定配置文件的名字和位置
-
文件路径
- classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/
-
文件名
- application
-
自动配置 遵守按需加载原则
- 也就是说,引入了哪个场景 starter 就会加载该场景关联的 jar 包,没有引入的 starter 则不会加载其关联 jar
SpringBoot 所 有 的 自 动 配 置 功 能 都 在 spring-boot-autoconfigure 包 里 面
- 在 SpringBoot 的 自 动 配 置 包 , 一 般 是 XxxAutoConfiguration.java, 对 应 XxxxProperties.java,
容器功能
Spring注入组件的注解
-
@Component、@Controller、 @Service、@Repository
-
这些在 Spring 中的传统注解仍然有效,通过这些注解可以给容器注入组件
@Configuration注解
-
替代xml配置文件的作用(boot中也可以使用beans.xml
1 2
ClassPathXmlApplicationContext xmlContext = new ClassPathXmlApplicationContext("beans.xml"); Monster monster = xmlContext.getBean(Monster.class);
-
通过@Configuration 创建配置类来注入组件
1 2 3 4 5 6 7 8 9 10 11 12
@Configuration public class BeanConfig { // 把monsterBeanName作为id, Monster作为value放入到容器中 // 默认把方法名作为id // 也可以指定id // 默认是单例的, 使用Scope注解可指定为多例 @Bean(name = "monster01") @Scope("prototype") public Monster monsterBeanName() { return new Monster(1, "黑马喽", "立棍", 500); } }
-
Configuration标注的配置类自身也会被放入到容器中(它是一个@Component)
-
proxyBeanMethods属性
- proxyBeanMethods:代理 bean 的方法
- (1) Full(proxyBeanMethods = true)、【保证每个@Bean 方法被调用多少次返回的组件都是 单实例的, 是代理方式】
- (2) Lite(proxyBeanMethods = false)【每个@Bean 方法被调用多少次返回的组件都是新创 建的, 是非代理方式】
- (3) 特别说明: proxyBeanMethods 是在 调用@Bean 方法 才生效,因此,需要先获取 BeanConfig 组件,再调用方法
- 而不是直接通过 SpringBoot 主程序得到的容器来获取 bean, 注意观察直接通过 ioc.getBean() 获取 Bean, proxyBeanMethods 值并没有生效
- (4) 如何选择: 组件依赖必须使用 Full 模式默认(单例)。如果不需要组件依赖使用 Lite 模式
-
(5) Lite 模式 也称为轻量级模式,因为不检测依赖关系,运行速度快
- 测试
1
2
3
4
BeanConfig beanConfig = ctx.getBean(BeanConfig.class);
Monster monster = beanConfig.monsterBeanName();
Monster monster2 = beanConfig.monsterBeanName();
System.out.println(monster2 == monster);
-
@Configuration(proxyBeanMethods = true)
- true, 两个对象相等
-
@Configuration(proxyBeanMethods = false)
- false, 两个对象不等
-
配置类可以有多个 就和 Spring 可以有多个 ioc 配置文件是一个道理
-
可以在配置类上加@ComponentScan(“org.xxx.myboot”)注解, 指定扫描的包
@Import注解
-
导入其他类到容器中
-
public class Cat {} public class Dog {}
-
@Import({Dog.class, Cat.class})
-
-
注意@Import 方式注入的组件, 默认组件的名字id就是全类名 org.xxx.boot.bean.Dog org.xxx.boot.bean.Cat
@Conditional
-
- 条件装配:满足 Conditional 指定的条件,则进行组件注入
-
- @Conditional 是一个根注解,下面有很多扩展注解
- @ConditionalOnBean
1 2 3 4 5 6
@Bean // 表示只有当容器注入了id=monster01的bean时才进行注入 @ConditionalOnBean(name = "monster01") public Dog dog01() { return new Dog(); }
也可以放在类名上 则表示对该配置类中所有要注入的组件都进行条件约束
@ImportResource
- 作用:原生配置文件引入, 也就是可以直接导入 Spring 传统的 beans.xml ,可以认 为是 SpringBoot 对 Spring 容器文件的兼容
1
2
@ImportResource({"classpath:beans.xml"})
public class BeanConfig {}
配置绑定
@ConfigurationProperties(prefix = “zangxin.dog”)
-
读取到 SpringBoot 核心配置文件 application.properties 的内容,并且把它作为 JavaBean的属性,必须作为一个组件, 提供gettter/setter
-
#自定义
1 2 3
zangxin.dog.id=1 zangxin.dog.name=狗昊 zangxin.dog.color=white
1 2 3 4 5 6 7 8
@Data @ConfigurationProperties(prefix = "zangxin.dog") @Component public class Dog { private Integer id; private String name; private String color; }
-
bean的id是类名小写
-
-
注意加入依赖
1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
配置绑定方式二
@EnableConfigurationProperties(Dog.class)
-
注释掉实体类上的@Component 在配置类加上注解
1 2 3 4 5 6 7 8 9
@EnableConfigurationProperties(Dog.class) @Data @ConfigurationProperties(prefix = "zangxin.dog") // @Component public class Dog { private Integer id; private String name; private String color; }
1 2 3
@EnableConfigurationProperties({Dog.class}) @Configuration(proxyBeanMethods = true) public class BeanConfig {}
- bean的id变成了 zangxin.dog-org.xxx.boot.bean.Dog
[springboot底层机制]
Tomcat 启动分析 + Spring 容器初始化 +Tomcat 如何关联 Spring 容器
组件扫描原理:
@Configuration+@Bean注解底层还是IO/文件扫描+注解+反射+集合+映射
SpringBoot 是怎么启动 Tomcat
- 启动入口:通过SpringApplication.run(Application.class, args)启动应用。
上下文初始化:创建AnnotationConfigServletWebServerApplicationContext(Spring Boot 2.x+的默认Web应用上下文)。
触发onRefresh():在上下文刷新阶段,调用ServletWebServerApplicationContext.onRefresh()方法。
创建Web服务器:在onRefresh()中调用createWebServer(),通过ServletWebServerFactory(如TomcatServletWebServerFactory)创建并启动Tomcat。
启动Tomcat:最终调用Tomcat.start()启动嵌入式Tomcat。
自定义boot
1
2
3
4
5
6
7
8
9
10
11
//MyConfig
@Configuration
@ComponentScan("org.xxx.myboot")
public class MyConfig {
// 注入bean到容器中
@Bean
public Duck duck() {
return new Duck();
}
}
1
2
3
4
5
6
7
//MyApp
public class MyApp {
public static void main(String[] args) {
MySpringApplication.run();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// MySpringApplication
public class MySpringApplication {
// 创建tomcat对象, 并关联spring容器
public static void run() {
try {
Tomcat tomcat = new Tomcat();
tomcat.setPort(43999);
// 1.让tomcat将请求转发到SpringWeb容器, 因此需要关联
// 2. context path: /myboot
// 3.项目的路径: tomcat在这个路径找到MyWebApplicationInitializer调用onStartup方法
tomcat.addWebapp("/myboot", "/Users/qingsongcai/IdeaProjects/j2025/springboot_/boot_base");
// 启动tomcat
tomcat.start();
// 等待请求
tomcat.getServer().await();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
-
-
MyWebApplicationInitializer
- 初始化器
- 1.创建spring容器
- 2.加载spring容器的配置-按照注解的方式
- 3.完成spring容器配置的bean的创建,依赖注入
- 4.创建前端控制器 DispatcherServlet, 并让其持有Spring容器
- 6.当DispatcherServlet持有容器, 就可以进行分发映射
- 7.onStartup 是tomcat调用, 并把ServletContext对象传入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
System.out.println("startup.....启动");
// 创建容器
AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
// 在容器中注册MyConfig类, 让容器知道在哪里扫包
ctx.register(MyConfig.class);
ctx.refresh();// 完成bean的创建和依赖注入
// 创建dispatcherServlet, 并让其持有容器
DispatcherServlet dispatcherServlet = new DispatcherServlet(ctx);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", dispatcherServlet);
// 当tomcat启动时,加载dispatcherServlet
registration.setLoadOnStartup(1);
// tomcat拦截所有请求,并进行分发处理
registration.addMapping("/*");
```
}
}
### 依赖
```xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.3</version>
<relativePath></relativePath>
</parent>
<groupId>org.xxx</groupId>
<artifactId>boot_base</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<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>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.75</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>8.5.75</version>
</dependency>
</dependencies>
Spring Initailizr
Spring 官方提供的 Spring Initializr 来构建 Maven 项目,能完美支持 IDEA 和 Eclipse
yaml/yml
语法
-
- 形式为 key: value;注意: 后面有空格
-
- 区分大小写
-
- 使用缩进表示层级关系
-
- 缩进不允许使用 tab,只允许空格 [有些地方也识别 tab , 推荐使用空格]
-
- 缩进的空格数不重要,只要相同层级的元素左对齐即可
-
- 字符串无需加引号
-
- yml 中, 注释使用 #
数据类型
-
- 字面量:单个的、不可再分的值。date、boolean、string、number、null
-
- 键值对形式为 key: value
-
- 对象:键值对的集合, 比如 map、hash、set、object
-
行内写法: k: {k1:v1,k2:v2,k3:v3} monster: {id: 100,name: 牛魔王}
-
#或换行形式 k: k1: v1 k2: v2 k3: v3 monster: id: 100 name: 牛魔王
-
- 数组:一组按次序排列的值, 比如 array、list、queue
- k:
- v1
- v2
- v3
实例复杂对象的yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
@ConfigurationProperties(prefix = "monster")
@Component
public class Monster {
private Integer id;
private String name;
private String skill;
private Integer age;
private Boolean isMale;
private Date birth;
private Car car;
private String[] skills;
private List<String> hobby;
private Map<String, Object> wife;
private Set<Double> salaries;
private Map<String, List<Car>> cars;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
monster:
id: 100
name: 小魔女
age: 400
isMale: true
birth: 2000/11/12
car:
name: 宝马
price: 200000
skill: 疯疯熊
skills:
- 治疗
- 藤蔓
- 偏爱
- 我的剧场1a
hobby:
- 逛街
- 刷怪
wife:
f1: 一流灯光师
f2: 扫把很卡哇伊
salaries:
- 10000
- 30000
cars:
group1:
- name: 宝马
price: 1000000
- name: 奔驰
price: 2000000
group2:
- name: 宝马x
price: 1000000
- name: 奔驰g
price: 2000000
注意:
application.properties和application.yml同时配置时, application.properties的优先级高
字符串无需加引号
idea中编辑yml提示
- 加入依赖
1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
- 输入你在 Bean 配置的 prefix 名 字就会提示.
WEB 开发-静态资源访问
- 只要静态资源放在类路径下: /static 、 /public 、 /resources 、 /META-INF/resources
可以被直接访问-
-
在中WebProperties类中定义了资源路径:CLASSPATH_RESOURCE_LOCATIONS = { “classpath:/META-INF/resources/”, “classpath:/resources/”, “classpath:/static/”, “classpath:/public/” };
-
下面都是classes目录下的 META-INF/resources/1.webp public/2.jpeg static/3.jpeg resources/5.png 上面都可以访问到 4.jpeg这个不能访问到
-
访问地址,已经自动映射好了
- http://localhost/1.webp http://localhost/2.jpeg http://localhost/3.jpeg http://localhost/5.png
2. 静态资源访问注意事项和细节
-
1.静态映射是 /**, 也就是对所有请求拦截,请求进来,先看 Controller 能不能处理,不能处理的请求交给静态资源处理器,如果静态资源找不到则响应 404 页面
- 2.如果静态资源uri和controller冲突怎么办
-
META-INF/resources/1.webp
-
@RequestMapping(“/1.webp”) public String hi() { return “hi”; }
-
请求的http://localhost/1.webp结果是controller返回hi, 而不是图片
-
解决办法, 静态资源路径映射
-
#静态资源路径映射 spring.mvc.static-path-pattern=/dog/**
-
访问OK: http://localhost/dog/1.webp
-
-
3.如何自定义静态资源路径
- 自定义的路径会覆盖掉默认的路径, 所以配置时要加上默认的路径
1
spring.web.resources.static-locations=classpath:/myimg/,classpath:/META-INF/resources/,classpath:/resources/, classpath:/static/, classpath:/public/
Rest 风格请求处理
请求方法/数据库操作: get/查询, post/新增, put/修改, delete/删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
@RequestMapping("/rest")
public class RestStyleController {
@GetMapping
public String getById() {
return "查询根据id";
}
@PostMapping
public String insert() {
return "新增操作";
}
@PutMapping
public String update() {
return "修改操作";
}
@DeleteMapping
public String delete() {
return "删除操作";
}
}
- 用postman调用
注意
-
postman可以直接发送put/delete/PATCH请求,可不设置 Filte
-
要 SpringBoot 支持 页面表单的 Rest 功能,则需要配置
-
Rest 风格请求核心 Filter:HiddenHttpMethodFilter
- 表单请求会被 HiddenHttpMethodFilter 拦截 , 获取到表单 _method 的值, 再判断是 PUT/DELETE/PATCH
-
配置: spring.mvc.hiddenmethod.filter.enabled=true
-
post表单请求时加上参数_method=put/delete
1
<input type="hidden" name="_method" value="delete">
-
为什么返回的是字符串数据, 而不是跳转页面
-
因为使用@RestController
- RestController=@Controller+@ResponseBody
-
@ResponseBody
- 指定数据放入到响应体中,而不是跳转页面
-
如果使用@Controller, 但是没有配置视图解析器, 则会寻找返回String作为url对应Controller, 没有找到对应的Controller就会报错
-
如果配置了视图解析器,就会跳转页面
-
静态文件uri映射路径
spring.mvc.static-path-pattern=/dog/**
-
视图解析器前缀和后缀
spring.mvc.view.prefix=/dog/ spring.mvc.view.suffix=.html
如果视图名和Controller的uri相同, 则还是以视图解析器为准
接收参数注解
@PathVariable
-
使用
1 2 3 4 5
// /para/100/king @RequestMapping("/para/{id}/{name}") public String pathVariable(@PathVariable("id") Integer id, @PathVariable("name") String name, @PathVariable Map<String, String> map) {}
- 使用map可以全部接收
@RequestHeader
1
2
3
@GetMapping("/requestHeader")
public String requestHeader(@RequestHeader("HOST") String host,
@RequestHeader Map<String, String> header) {}
- 使用map可以全部接收
@RequestParam
1
2
3
4
@GetMapping("/requestParam")
public String requestParam(@RequestParam("name") String name,
@RequestParam("version") List<String> versions,
@RequestParam Map<String, String> map)
-
list/数组用来接收,复选框的值
-
map可以全部接收, 但是复选框只能接收一个, 因为,map的key是唯一的, 重复放入就是覆盖了
@CookieValue
1
2
3
@GetMapping("/cookieValue")
public String cookieValue(@CookieValue(value = "cookie_key") String cook_value,
@CookieValue(value = "username", required = false) Cookie cookie) {}
- 可以接收cookie的值也可以接收整个cookie键值对, required=false, 表示非强制要获取这个cookie, 但是注意这样cookie可能为null值
@RequestBody
1
2
3
@PostMapping(value = "/requestBody")
// conten这样接收的是: name=xxx&age=22
public String requestBody(@RequestBody String content) {}
// 从请求体中取出数据
-
todo: 以后再看(问题复现: 使用post请求, 返回视图, 不用@ResponseBody)注意post请求不能跳转页面(我了个草)
请求方法保留:转发是服务器内部的行为,完全保留原始请求的HTTP方法(还是不行呐!!)
@ModelAttribute
- 从请求域中获取数据
1
2
3
4
5
@GetMapping("/login")
public String login(HttpServletRequest request) {
request.setAttribute("user", "zangxin");
return "forward:/ok";
}
1
2
3
4
5
6
7
8
9
@GetMapping("/ok")
@ResponseBody
public String loginOk(@RequestAttribute(value = "user", required = false) String user,
HttpServletRequest request) {
// 这样的效果和@RequestAttribute一样
Object user2 = request.getAttribute("user");
log.info("user={},user2={}", user, user2);
return "success";
}
@SessionAttribute
- 获取Session域中数据
1
2
3
4
5
6
7
8
9
@GetMapping("/ok")
@ResponseBody
public String loginOk(@RequestAttribute(value = "user", required = false) String user,
HttpServletRequest request) {
// 这样的效果和@RequestAttribute一样
Object user2 = request.getAttribute("user");
log.info("user={},user2={}", user, user2);
return "success";
}
复杂参数
-
介绍
-
- 在开发中,SpringBoot 在响应客户端请求时,也支持复杂参数
-
- Map、Model、Errors/BindingResult、RedirectAttributes、ServletResponse、SessionStatus、 UriComponentsBuilder、ServletUriComponentsBuilder、HttpSession
-
- Map、Model 数据会被放在 request 域, 底层 request.setAttribute()
-
- RedirectAttributes 重定向携带数据
-
-
Map, model自动放入请求域中
1
2
3
4
5
6
7
8
9
10
11
// 响应一个register请求
@GetMapping("/register")
public String register(Map<String, Object> map, Model model, HttpServletResponse response) {
// map和model中数据会放入到请求域中
map.put("user", "zangxin");
map.put("age", 22);
model.addAttribute("gender", "male");
// 还可以设置cookie
response.addCookie(new Cookie("hobby", "gal"));
return "forward:/registerOk";
}
1
2
3
4
5
6
7
8
9
10
@GetMapping("/registerOk")
@ResponseBody
public String registerOk(HttpServletRequest request) {
// 从这里可以之前放入map和model中数据
Object user = request.getAttribute("user");
Object age = request.getAttribute("age");
Object gender = request.getAttribute("gender");
log.info("{},{},{},{}", user, age, gender, request.getCookies()[1].getName());
return "" + user + age + gender;
}
自定义对象参数
- 封装pojo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
public class Monster {
private Integer id;
private String name;
private Integer age;
private Boolean isMarried;
private Date birth;
private Car car;
@Data
public static class Car {
private String name;
private Double price;
}
}
- 表单
1
2
3
4
5
6
7
8
9
10
<form action="/saveMonster" method="post">
编号: <input name="id" value="100"><br/>
姓名: <input name="name" value="牛魔王"/> <br/>
年龄: <input name="age" value="120"/> <br/>
婚否: <input name="isMarried" value="true"/> <br/>
生日: <input name="birth" value="2000/11/11"/> <br/>
坐骑: <input name="car.name" value="法拉利"/><br/>
价格: <input name="car.price" value="99999.9"/>
<input type="submit" value="保存"/>
</form>
-
获取级联对象的属性
1 2 3 4 5 6 7
// 接收pojo对象 @PostMapping("/saveMonster") @ResponseBody public String saveMonster(Monster monster) { log.info("monster={}", monster); return "success"; }
-
不需要写@RequestBody, 写了反而出错 这个是x-www-form-urlencoded表单, 写了解析不了, 如果是json数据则需要写@RequestBody
- Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type ‘application/x-www-form-urlencoded;charset=UTF-8’ not supported]
参数转换器
-
定义
-
- SpringBoot 在响应客户端请求时,将提交的数据封装成对象时,使用了内置的转换器
-
SpringBoot 提供了 124 个内置转换器
- String转Integer String转Date等
-
支持自定义转换器
- 源码在: GenericConverter-ConvertiblePair
-
-
自定义转换器
-
需求:将把String类型的:法拉利,99999.9 转换成Car对象
-
写配置类自定义转换器
1
<input name="car" value="法拉利,99999.9"/>
1 2 3 4 5 6
private Car car; @Data public static class Car { private String name; private Double price; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
@Configuration(proxyBeanMethods = false) public class WebConfig { // 注入bean WebMvcConfigurer @Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { @Override public void addFormatters(FormatterRegistry registry) { // 增加一个自定义的转换器 // 把String类型的:法拉利,99999.9 转换成Car对象 // 增加的自定义转换器会注册到converts容器中 // converters 底层是ConcurrentHashMap内置有124个转换器 registry.addConverter(new Converter<String, Monster.Car>() { @Override public Monster.Car convert(String source) { if (ObjectUtils.isEmpty(source)) return null; // 把String类型的:法拉利,99999.9 转换成Car对象 String[] split = source.split(","); Monster.Car car = new Monster.Car(); car.setName(split[0]); car.setPrice(Double.valueOf(split[1])); return car; } }); } }; } }
注意点,两个一样类型转换器只会有一个生效, 一个会被覆盖掉,详细见converts的map-key设置:源类型->转换类型
java.lang.String -> org.xxx.bootweb.bean.Monster=java.lang.String -> org.xxx.bootweb.bean.Monster : org.xxx.bootweb.config.WebConfig$1$3@32a2b30,java.lang.String -> org.xxx.bootweb.bean.Monster : org.xxx.bootweb.config.WebConfig$1$2@3811cadd
-
处理json数据
-
在引入spring-boot-starter-web后,就引入了jackson
-
返回Json
- AbstractJackson2HttpMessageConverter#writeInternal 方法根据response的contentType来选择不同ObjectMapper(jackson)来我们的返回值(类型class,和对象)转换成html或者, json
1
2
3
4
5
6
7
8
9
10
11
@Override
protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
MediaType contentType = outputMessage.getHeaders().getContentType();
JsonEncoding encoding = getJsonEncoding(contentType);
Class<?> clazz = (object instanceof MappingJacksonValue ?
((MappingJacksonValue) object).getValue().getClass() : object.getClass());
ObjectMapper objectMapper = selectObjectMapper(clazz, contentType);
Assert.state(objectMapper != null, "No ObjectMapper for " + clazz.getName());
内容协商
-
文档
-
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Guides/Content_negotiation
-
在 HTTP 协议中,内容协商是一种机制,用于为同一 URI 提供资源不同的表示形式,以帮助用户代理指定最适合用户的表示形式(例如,哪种文档语言、哪种图片格式或者哪种内容编码)。
-
-
定义
-
1.根据客户端的接收能力不同, SpringBoot返回不同媒体类型的数据
-
- 比如: 客户端 Http 请求 Accept: application/xml 则返回 xml 数据,客户端 Http 请求 Accept: application/json 则返回 json 数据
-
postman请求 Accept:application/json
-
返回:
1
{"id":999,"name":"黑马喽","age":500,"isMarried":false,"birth":"2025-03-14T13:18:30.897+00:00","car":{"name":"精斗云","price":888888.0}}
-
postman请求 Accept:application/xml
-
返回:
1 2 3 4 5 6 7 8 9 10 11
<Monster> <id>999</id> <name>黑马喽</name> <age>500</age> <isMarried>false</isMarried> <birth>2025-03-14T13:19:42.148+00:00</birth> <car> <name>精斗云</name> <price>888888.0</price> </car> </Monster>
- 返回xml要引入依赖
1 2 3 4 5
<!--引入处理返回xml数据格式的--> <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </dependency>
-
-
原理
1 2 3
MediaType contentType = outputMessage.getHeaders().getContentType(); ObjectMapper objectMapper = selectObjectMapper(clazz, contentType); JsonGenerator generator = objectMapper.getFactory().createGenerator(outputStream, encoding)
-
一句话: 根据contentType来获取不同ObjectMapper的实现类,来把返回对象转换成不同xml和json
-
ObjectMapper
-
JsonMapper
-
XmlMapper
-
-
-
Accept请求头权重
-
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Accept
-
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
-
application/xhtml+xml和application/xml;q=0.9权重是0.9大于/;q=0.8, 所以应该优先返回xml, 如果不能返回xml, 就使用*/*
-
*/* 任何 MIME 类型
-
-
;q=(q 因子加权) 使用的值根据一个称为权重的相对质量价值来排序,表达了优先级顺序。
-
指定参数要求的类型,更为灵活的方式
-
- Postman 可以通过修改 Accept 的值,来返回不同的数据格式
-
- 对于浏览器,我们无法修改其 Accept 的值,怎么办?
-
解决方案: 开启支持基于请求参数 的内容协商功能
-
配置
- 开启基于请求参数的内容协商
1
spring.mvc.contentnegotiation.favor-parameter=true
1
- 使用
-
返回json http://localhost/returnJson?format=json
1
- 返回xml http://localhost/returnJson?format=xml
-
-
原理
-
org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties.Contentnegotiation#favorParameter
-
Whether a request parameter (“format” by default) should be used to determine the requested media type.
-
也可以改变参数的名字: format->改成其他的名字
- spring.mvc.contentnegotiation.parameter-name=ggstar
-
注意,参数 format 是规定好的 , 在开启请求参数的内容协商功能后,SpringBoot 底层 ParameterContentNegotiationStrategy 会通过 format 来接收参数,然后返回对应的媒体类型/ 数据格式 , 当然 format=xx 这个 xx 媒体类型/数据格式 是 SpringBoot 可以处理的才行,不 能乱写.
-
-
@MatrixVariable
拦截器
原理, 详见SpringMvc拦截器
作用
- 需求: 使用拦截器防止用户非法登录, 使用拦截器就不需要在每个方法验证了
使用步骤
- 编写一个拦截器实现 HandlerInterceptor 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("uri:{}", requestURI);
// 从Session中获取登录信息
Object admin = request.getSession().getAttribute("admin");
if (admin instanceof Admin a) {
log.info("{}登录成功, ", a.getName());
return true;// 放行
} else { // 返回null时,说明未登录
log.error("未登录!");
request.setAttribute("msg", "请登录之后再访问");
request.getRequestDispatcher("/login").forward(request, response);
return false;
}
}
- 拦截器注册到配置类中(实现 WebMvcConfigurer 的 addInterceptors)
1
2
3
4
5
6
7
8
9
@Configuration
public class Config implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/", "/login", "/images/**","/favicon.ico");
}
}
-
指定拦截规则
- 拦截所有请求 .addPathPatterns(“/**”)
-
指定放行规则
- 放行的请求 .excludePathPatterns(“/”, “/login”, “/images/**”,”/favicon.ico”);
注意 事项
-
uri和URL的区别
-
URI = Universal Resource Identifier
-
URL = Universal Resource Locator
-
Identifier:标识符,Locator:定位器 从字面上来看, URI 可以唯一标识一个资源, URL 可以 提供找到该资源的路径
-
-
注册拦截器的方式二
1 2 3 4 5 6 7 8 9 10 11
@Configuration public class WebConfig { @Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { @Override public void addInterceptors(InterceptorRegistry registry) { //加入我们的拦截器 registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/", "/login", "/images/**","/favicon.ico"); }
文件上传
后端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@PostMapping("/upload")
@ResponseBody
public String upload(
@RequestParam("name") String name,
@RequestParam("email") String email,
@RequestParam("age") String age,
@RequestParam("job") String job,
@RequestParam("header") MultipartFile header,
@RequestParam("photos") MultipartFile[] photos
) throws Exception {
log.info("入参:name={}, email = {}, age = {}, job = {}, header = {}, photos = {}", name, email, age, job, header, photos);
// 如果信息都成功得到, 那么就保存文件
File uploadDir = getUploadDir();
// 单个上传文件
if (header != null && !header.isEmpty()) {
String originalFilename = header.getOriginalFilename();
header.transferTo(new File(uploadDir, originalFilename));
}
// 处理多个上传文件
if (photos != null && photos.length > 0) {
for (MultipartFile photo : photos) {
if (!photo.isEmpty()) {
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File(uploadDir, originalFilename));
}
}
}
return "注册/上传成功";
}
// 获取上传目录路径
// 上传目录在类路径/static/images/upload
private File getUploadDir() {
try {
File path = ResourceUtils.getFile("classpath:");
// 路径也可以卸载配置文件中,用@Autowired注入
File file = new File(path, "static/images/upload");
if (!file.exists()) {
file.mkdir();
}
log.info("upload dir={}", file.getAbsolutePath());
return file;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
- 前端
1
2
3
4
5
6
7
8
9
10
11
<form action="#" method="post" th:action="@{/upload}" enctype="multipart/form-data">
用户名:<input type="text" style="width:150px" name="name"/><br/><br/>
电 邮:<input type="text" style="width:150px" name="email"/><br/><br/>
年 龄:<input type="text" style="width:150px" name="age"/><br/><br/>
职 位:<input type="text" style="width:150px" name="job"/><br/><br/>
头 像:<input type="file" style="width:150px" name="header"><br/><br/>
<!--multiple可以上传多个文件-->
宠 物:<input type="file" style="width:150px" name="photos" multiple><br/><br/>
<input type="submit" value="注册"/>
<input type="reset" value="重新填写"/>
</form>
注意事项
-
上传文件大小
-
找过大小就报异常The field header exceeds its maximum permitted size of 1048576 bytes
-
配置文件上传大小
1
spring.servlet.multipart.max-file-size=100MB
-
-
创建目录的命令mkdir, 而不是createNewFile
-
deepseek: 如果需要在打包后(jar/war)仍能正常工作,建议将上传目录设置为外部绝对路径(如:/var/uploads),而不是放在类路径中,因为jar包内的路径通常是只读的。
异常处理
注意登录拦截器的拦截所有/**, 先登录后再测试
默认异常/错误处理页面
-
Spring Boot提供/error处理所有错误的映射, 也就是当出现错误时, SpringBoot底层会提供请求转发到/error这个映射上
- 当你访问一个瞎写的url:http://localhost/xxx, 就会返回一个 Whitelabel Error Page
-
- 对于机器客户端,它将生成 JSON 响应,其中包含错误,HTTP 状态和异常消息的详细信 息。对于浏览器客户端,响应一个”whitelabel”错误视图,以 HTML 格式呈现相同的数据
-
原理
1 2 3 4 5 6 7 8
DefaultErrorViewResolver类 Default ErrorViewResolver implementation that attempts to resolve error views using well known conventions. Will search for templates and static assets under '/error' using the status code and the status series. For example, an HTTP 404 will search (in the specific order): '/<templates>/error/404.<ext>' '/<static>/error/404.html' '/<templates>/error/4xx.<ext>' '/<static>/error/4xx.html'
自定义错误页面
-
错误页面存放位置如DefaultErrorViewResolver类中
-
404.html>4xx.html的展示优先级
-
错误消息modelAndView
1
{"view":null,"model":{"timestamp":1742014303472,"status":500,"error":"Internal Server Error","path":"/manage.html"},"status":null,"empty":false,"reference":true,"modelMap":{"timestamp":1742014303472,"status":500,"error":"Internal Server Error","path":"/manage.html"},"viewName":"error/500"}
-
可以从错误消息modelAndView中取出信息给前端展示用
1 2 3
<h1>500错误</h1> 状态码:<h1 th:text="${status}"></h1> 错误信息:<h1 th:text="${error}"></h1>
-
在拦截器中给/error放行
全局异常处理
-
全局异常处理优先级高于错误页面
-
定义
-
- @ControllerAdvice+@ExceptionHandler 处理全局异常
-
- 底层是 ExceptionHandlerExceptionResolver 支持的
-
-
需求
-
- 需求: 全局异常使用, 当发生 ArithmeticException、NullPointerException 时,不使 用默认异常机制匹配的 xxx.html , 而是显示全局异常机制指定的错误页面
-
-
实现
1 2 3 4 5 6 7 8 9 10 11
@Slf4j @ControllerAdvice // 标识一个全局异常处理器 public class GlobalExceptionHandler { @ExceptionHandler({ArithmeticException.class, NullPointerException.class}) public String ArithmeticExceptionHandler(Exception e, Model model) { log.info("捕获到了异常~~😁: {}", e.getMessage()); // 放入错误消息给, 给前端展示使用 model.addAttribute("msg", e.getMessage()); return "/error/global"; // templates/error/global.html } }
-
原理
-
异常处理方法还可以传入参数handlerMethod, 即:发生异常的方法
-
org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#doResolveHandlerMethodException
- 根据异常类型在GlobalExceptionHandler中找对应异常类型的ExceptionHandler, 然后通过反射调用异常处理方法, 获取异常处理的modelAndView
-
自定义异常
-
- 如果 Spring Boot 提供的异常不能满足开发需求,也可以自定义异常
-
- @ResponseStatus+自定义异常
-
- 底 层 是 ResponseStatusExceptionResolver , 底 层 调 用 response.sendError(statusCode, resolvedReason);
- 他会遍历所有所有的异常解析器,来获取异常处理器
-
- 当抛出自定义异常后,仍然会根据状态码,去匹配使用 x.html 显示
-
使用
-
自定义403Forbidden异常
1 2 3 4 5 6 7 8 9
// 403异常状态码 @ResponseStatus(HttpStatus.FORBIDDEN) public class AccessException extends RuntimeException { public AccessException(String message) { super(message); } public AccessException() { } }
-
使用
1 2 3 4 5 6 7
@GetMapping("err403") public String forbidden(String name) { if (!"tom".equals(name)) { throw new AccessException(); } return "forward:/manage.html"; }
-
触发错误会跳转 4xx.html页面去, 这是使用了默认的处理方法
-
-
把自定义异常的处理放在自定义的全局异常处理器中
1 2 3 4 5 6 7
@ExceptionHandler({AccessException.class}) public String accessExceptionHandler(Exception e, Model model, HandlerMethod handlerMethod) { log.info("捕获到了非法访问异常~~😁: {},{},{}", e.getMessage(), model, handlerMethod.getMethod().getName()); // 放入错误消息给, 给前端展示使用 model.addAttribute("msg", e.getMessage()); return "/error/global"; }
-
拦截器和过滤器的区别
1、使用范围不同
-
过滤器 实现的是 javax.servlet.Filter 接口,而这个接口是在 Servlet 规范中定义的,也就是说过滤器 Filter 的使用要依赖于 Tomcat 等容器,Filter 只能在 web 程序中使用
-
拦截器(Interceptor) 它是一个 Spring 组件,并由 Spring 容器管理,并不依赖 Tomcat 等容 器,是可以单独使用的。不仅能应用在 web 程序中,也可以用于 Application 等程序中
2、过滤器 和 拦截器的触发时机也不同
-
request->tomcat->Filter->Servelt->Interceptor->Controller
-
过滤器 Filter 是在请求进入容器后, 但在进入 servlet 之前进行预处理, 请求结束是在 servlet 处理完以后
-
拦截器 Interceptor 是在请求进入 servlet 后, 在进入 Controller 之前进行预处理的, Controller 中渲染了对应的视图之后请求结束
3.过滤器不会处理(拦截)请求转发, 拦截器会处理请求转发
- 拦截器和过滤器的区别.png
JavaEE三大组件Servlet,Filter,Listener注入容器
定义
-
1.Spring-Boot 支持将 Servlet、Filter、Listener 注入Spring 容器, 成为 Spring bean
-
- 也就是说明 Spring-Boot 开放了和原生 WEB 组件(Servlet、Filter、Listener)的兼容
使用
-
方式一: 在启动类上加javaEE组件扫描
1 2 3
@SpringBootApplication @ServletComponentScan(basePackages = {"org.xxx.boothyme.servlet"}) public class App {}
-
方式二: ServletRegistrationBean注册
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration
public class RegisterConfig_ {
// 注册servlet
@Bean
public ServletRegistrationBean servlet_() {
BootAndServlet bootAndServlet = new BootAndServlet();
return new ServletRegistrationBean(bootAndServlet, "/servlet01", "/servlet02");
}
// 注册过滤器
@Bean
public FilterRegistrationBean filter_() {
Filter_ filter = new Filter_();
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(filter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/css/*", "/images/*"));
return filterRegistrationBean;
}
// 注册listener
@Bean
public ServletListenerRegistrationBean listener_() {
Listener listener = new Listener();
return new ServletListenerRegistrationBean(listener);
}
}
- servlet
1
2
3
4
5
6
@WebServlet(name = "BootAndServlet", value = "/BootAndServlet")
public class BootAndServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().write("BootAndServlet~~ 在boot中Servlet 完成OK~~~");
}
- filter
1
2
3
4
5
6
7
8
9
@Slf4j
@WebFilter(urlPatterns = {"/css/*","/images/*"})
public class Filter_ implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
log.info("doFilter调用, 过滤uri是: " + request.getRequestURI());
chain.doFilter(request, response);
}
-
listener
- 举例子: ServletContextListener
1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
@WebListener
public class Listener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
// 加入项目初始化相关的业务代码
log.info("项目初始化OK~");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("项目销毁了");
}
}
注意
-
1注意注入的原生 Servlet 不会被 Spring-Boot 拦截器拦截
-
DispacherServlet的请求路径是/
-
Servlet是优先精确匹配/最长路径匹配原则
- 注:url优先等级: 精确路径>目录路径>扩展名路径> /* > /
-
因此对于/ 和 /servlet01, 自然优先匹配/servlet01, 不走SpringMVC的DispatcherServlet那一套了
-
-
在 SpringBoot 中, 去调用@Controller 目标方法 是按照 DispatherServlet 分发匹配的机 制,
-
- DispatcherServletAutoConfiguration 完成对 DispatcherServlet 自动配置
- DispatcherServlet的路径是 /
-
过滤器配置的 urlPatterns 也会经过 Spring-Boot 拦截器
- 注意: 过滤器配置的 urlPatterns 也会经过 Spring-Boot 拦截器(根据拦截器的规则) 所以为了看到效果,请在拦截器配置放行 /css/** 在 servlet 匹配全部是 /*, 在 Spring-Boot 是/**
内置 Tomcat 配置和切换
默认容器
-
- SpringBoot 支持的 webServer: Tomcat, Jetty, or Undertow
- Uses Tomcat as the default embedded container
配置类的位置
- org.springframework.boot.autoconfigure.web.ServerProperties
tomcat的属性
1
2
3
4
5
6
7
8
9
10
11
#tomcat常用属性
#最大工作线程, 默认是200
server.tomcat.threads.max=200
#最小工作线程, 默认是10
server.tomcat.threads.min-spare=10
#tomcat线程达到最大时,接收排队的请求个数(默认是200)
server.tomcat.accept-count=200
#最大连接数, 并发数
server.tomcat.max-connections=2000
#建立连接的超时时间,单位ms
server.tomcat.connection-timeout=100000
-
也可以通过类来配置,不过配置项比较少
1 2 3 4 5 6 7 8
@Component public class CustomizationTomcat implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> { @Override public void customize(ConfigurableWebServerFactory factory) { factory.setPort(8848); } }
使用Undertown,maven
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<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>
数据库操作
JDBC+HikariDataSource(boot默认)
-
HikariDataSource : 目前市面上非常优秀的数据源, 是 springboot2 默认数据源
-
进行数据库开发,在 pom.xml 引入 data-jdbc starter和mysql驱动>mysql-connector-java 和jdbc配置
-
data-jdbc starter包括
- hikari数据源, 事务,jdbc等
-
jdbc配置
1 2 3 4 5
#数据源配置 spring.datasource.url=jdbc:mysql://localhost:3306/furn_ssm spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
-
Druid
-
定义
- Druid: 性能优秀,Druid 提供性能卓越的连接池功能外【Java 基础】,还集成了 SQL 监 控,黑名单拦截等功能,强大的监控特性,通过 Druid 提供的监控功能,可以清楚知道连 接池和 SQL 的工作情况
-
将 Spring-Boot 的数据源切换成 Druid
-
引入依赖
- druid:1.1.23
-
配置druid数据源
1 2 3 4 5 6 7 8 9
@Configuration public class DruidDataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource() { DruidDataSource ds = new DruidDataSource(); return ds; } }
- @ConfigurationProperties(prefix = “spring.datasource”) 可以通过这个注解从文件中读取数据源配置
-
-
为什么配置了Druid就自动替换了hikari呢?
1 2 3 4 5 6 7
// DataSourceAutoConfiguration类 @Configuration(proxyBeanMethods = false) @Conditional(PooledDataSourceCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) protected static class PooledDataSourceConfiguration {} @ConditionalOnMissingBean({ DataSource.class指出当Datasource缺失时才会注入, 现在我们有druidDatasource, 就会不会注入hikari了
-
druid监控功能
-
https://github.com/alibaba/druid/wiki/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98
-
SQL监控功能
- 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
@Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource() throws SQLException { DruidDataSource ds = new DruidDataSource(); ds.setFilters("stat"); return ds; } @Bean public ServletRegistrationBean statViewServlet() { // druid ui servlet StatViewServlet statViewServlet = new StatViewServlet(); ServletRegistrationBean<StatViewServlet> registrationBean = // 参数:servlet,urlPattern new ServletRegistrationBean<>(statViewServlet, "/druid/*"); registrationBean.addInitParameter("loginUsername", "root"); registrationBean.addInitParameter("loginPassword", "root"); return registrationBean; }
-
访问url:localhost/druid
-
Web 应用和 URI 监控页面功能开启
- WebStatFilter 用于采集 web-jdbc 关联监控的数据
1 2 3 4 5 6 7 8
@Bean public FilterRegistrationBean webStatFilter() { WebStatFilter webStatFilter = new WebStatFilter(); FilterRegistrationBean<WebStatFilter> registrationBean = new FilterRegistrationBean<>(webStatFilter); registrationBean.setUrlPatterns(Arrays.asList("/*")); registrationBean.addInitParameter("exclusions", "*.js,*.webp,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); return registrationBean; }
-
sql防火墙
- // 数据源加入wall配置 ds.setFilters(“stat,wall”);
-
session监控
-
-
Druid Spring Boot Starter更简单配置
- 依赖
1 2 3 4 5
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.17</version> </dependency>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
#druid spring.datasource.druid.stat-view-servlet.enabled=true spring.datasource.druid.stat-view-servlet.login-username=root spring.datasource.druid.stat-view-servlet.login-password=root spring.datasource.druid.stat-view-servlet.url-pattern=/druid/* #web监控功能 spring.datasource.druid.web-stat-filter.enabled=true spring.datasource.druid.web-stat-filter.url-pattern=/* spring.datasource.druid.web-stat-filter.exclusions=*.js,*.webp,*.gif,*.jpg,*.png,*.css,*.ico,/druid/* #sql监控-慢查询sql spring.datasource.druid.filter.stat.enabled=true spring.datasource.druid.filter.stat.slow-sql-millis=1000 spring.datasource.druid.filter.stat.log-slow-sql=true #防火墙-禁止drop table spring.datasource.druid.filter.wall.enabled=true spring.datasource.druid.filter.wall.config.drop-table-allow=false #禁止删除操作 spring.datasource.druid.filter.wall.config.delete-allow=false
- 在开启禁止delete后, 去删除会报异常
单元测试
依赖
1
2
3
4
5
<!--unit test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
测试类
1
2
3
4
5
6
7
8
9
10
11
// App.class是启动类
@SpringBootTest(classes = App.class)
public class CrudTest {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
void SelectAll() throws Exception {
String sql = "select * from furn";
List<Furn> furnList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Furn.class));
furnList.forEach(System.out::println);
}
Spring Boot 整合 MyBatis
依赖
- spring-boot-starter-parent:2.5.3 spring-boot-starter-web mybatis-spring-boot-starter:2.2.2 spring-boot-starter-data-jdbc mysql-connector-java
配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#mybatis
#指定mapper.xml位置
mybatis.mapper-locations=classpath:mapper/*.xml
#别名
mybatis.type-aliases-package=org.xxx.bootmybatis.bean
#日志-控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#下划线->驼峰转换
mybatis.configuration.map-underscore-to-camel-case=true
#使用原始的mybatis.xml配置
#mybatis.config-location=
#或者直接在这里application.properties中配置
#如果mybatis配置比较多可以使用mybatis-config.xml(比如说插件,缓存等等)
#建议在application.properties中指定mapper位置,可以使用通配符方便
Mapper接口上加@Mapper
1
2
3
4
@Mapper
public interface FurnMapper {
Furn getFurnById(Integer id);
}
使用时,直接@Autowired即可
Mysql时间json序列化设置格式
1
2
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
private Timestamp createTime;
@Mapper注解说明以及优化
-
这个注解指定类是一个@Mapper接口, 所有dao包下的接口全都要加上@Mapper
-
如果有多个包或者类, 一个一个的加@Mapper太繁琐了, 这时可以在启动类上加个@MapperScan(basePackages = “org.xxx.bootmybatis.mapper”),指定包下面的所有Mapper类
MybatisPlus
功能
- 无侵入,强大的 CRUD 操作 支持 Lambda 形式调用, 支持主键自动生成,内置代码生成器, 支持自定义全局通用操作,内置分页插件, 内置性能分析插件,内置全局拦截插件
使用
-
依赖,引入mybatisplus就不用引mybatis了,千万要注释掉mybatis的依赖,否则启动不了
1 2 3 4 5 6
<!--mybatisplus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.10.1</version> </dependency>
-
配置
-
mybatisplus配置mapper.xml的默认值
- classpath:/mapper/**/.xml
-
基本和mybatis一致,只是前缀变成了mybatis-plus
1 2 3 4 5
#mybatisplus mybatis-plus.mapper-locations=classpath:mapper/*.xml mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl mybatis-plus.type-aliases-package=org.xxx.bootmybatis.bean mybatis-plus.configuration.map-underscore-to-camel-case=true
-
-
Mapper接口继承BaseMapper<>
- BaseMapper提供了很多方法, 现成的直接用,不用写简单sql了
1 2 3 4
@Mapper public interface FurnMapper extends BaseMapper<Furn> { Furn getFurnById(Integer id); }
-
Service接口继承父IService
1
public interface FurnService extends IService<Furn> {}
-
ServiceImpl实现类继承ServiceImpl<BaseMapper,实体类> 实现 Service接口
1
public class FurnServiceImpl extends ServiceImpl<FurnMapper,Furn> implements FurnService {}
-
@TableName注解,解决实体类和表名不一致的问题
- 如果在表tbl_user和User类进行查询, 会报错 使用@TableName注解可以解决 @TableName(“tbl_user”) public class User {}
-
注意点
-
mybatisplus也可以在xml文件中写sql语句
-
// mybatis-plus不能自动填充表不存在的字段 @TableField(exist = false)
-
Wrapper条件构造器
-
https://baomidou.com/guides/wrapper/
-
查询QueryWrapper/LambdaQueryWrapper
-
DDL,UpdateWrapper/LambdaUpdateWrapper
分页插件
-
配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
@Configuration public class MyBatisPlusConfig { /** * 添加分页插件 */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pi = new PaginationInnerInterceptor(DbType.MYSQL); interceptor.addInnerInterceptor(pi); // 如果配置多个插件, 切记分页最后添加 // 如果有多数据源可以不配具体类型, 否则都建议配上具体的 DbType return interceptor; } }
-
分页查询代码
- 这个使用的是IService的方法
1 2 3 4 5 6 7 8 9 10
@GetMapping("/page") public Result page( @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum, @RequestParam(value = "pageSize", defaultValue = "5") Integer pageSize, @RequestParam(value = "name", defaultValue = "") String name) { LambdaQueryWrapper<Furn> lw = new LambdaQueryWrapper<>(); lw.like(StringUtils.hasText(name), Furn::getName, name); Page<Furn> page = furnService.page(new Page<>(pageNum, pageSize), lw); return Result.ok(page); }
MybatisX插件/MybatisCodeHelperPro
- 可以生成代码/sql之类,跳转sql.xml和mapper接口
springboot整合redis
定义
- 在 springboot 中 , 整合 redis 可以通过 RedisTemplate 完成对 redis 的操作, 包括设置数据/获取数据
依赖
- spring-boot-starter-data-redis commons-pool2 jackson-databind
配置文件
1
2
3
4
5
6
7
8
#Redis 服务器地址
spring.redis.host=192.168.2.85
#Redis 服务器连接端口
spring.redis.port=6379
#Redis 如果有密码,需要配置, 没有密码就不要写
spring.redis.password=root
#Redis 数据库索引(默认为 0)
spring.redis.database=0
连接超时时间(毫秒)
spring.redis.timeout=1800000
连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-wait=-1
连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
redis配置类
-
是对要使用的 RedisTemplate bean 对象的配置, 可以理解成是一个常规配置.
-
如果不配置, springboot 会使用默认配置, 这个默认配置, 会出现一些问题, 比如: redisTemplate 的 key 序列化等问题, 所以通常我们需要配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
@Configuration @EnableCaching @SuppressWarnings("all") public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); System.out.println("template=>" + template); RedisSerializer<String> redisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY); jackson2JsonRedisSerializer.setObjectMapper(om); template.setConnectionFactory(factory); // key 序列化方式 template.setKeySerializer(redisSerializer); // value 序列化 template.setValueSerializer(jackson2JsonRedisSerializer); // value hashmap 序列化 template.setHashValueSerializer(jackson2JsonRedisSerializer); return template; } @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { RedisSerializer<String> redisSerializer = new StringRedisSerializer(); // 解决查询缓存转换异常的问题 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY); jackson2JsonRedisSerializer.setObjectMapper(om); // 配置序列化(解决乱码的问题),过期时间 600 秒 RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(600)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) .disableCachingNullValues(); RedisCacheManager cacheManager = RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); return cacheManager; } }
RedisTemplate使用
-
@Autowired private RedisTemplate redisTemplate;
-
操作string
1 2
redisTemplate.opsForValue().set("book", "金瓶梅"); String book = (String) redisTemplate.opsForValue().get("book");
-
操作list
1 2 3 4 5 6 7
redisTemplate.opsForList().leftPush("books", "水浒传"); redisTemplate.opsForList().leftPush("books", "三国演义"); // 一次性放多个 redisTemplate.opsForList().leftPushAll("books", "西游记", "红楼梦", "道德经"); // list-取出 List<String> books = redisTemplate.opsForList().range("books", 0, -1); return String.join(",", books);
-
set
- redisTemplate.opsForSet();
-
zset
- redisTemplate.opsForHash();
-
hash
- redisTemplate.opsForZSet();
注意事项,序列化问题
-
WRONGTYPE Operation against a key holding the wrong kind of value
-
看报错,是 json 转换异常,实际上是因为 redisTemplate 在做数据存储的时候会把存 储的内容序列化,所以,redisTemplate 读取的时候也会反序列化,而在 redis 客户端 set 的时候并不会做序列化,因此 set 的进去的值在用 redisTemplate 读的时候就会报类 型转换异常了
-
解决方案 : 最简单的就是用程序重新 set 一遍即可,或者不要用其他客户端设置值, 用什么写,就用什么读