1. 1. Web容器基本配置
  2. 2. Tomcat配置
    1. 2.1. 日志
    2. 2.2. HTTPS证书
  3. 3. 配置文件名称和路径
    1. 3.1. 位置问题
    2. 3.2. 文件名问题
  4. 4. 普通的属性注入
    1. 4.1. 类型安全的属性注入
    2. 4.2. 在配置文件中引用maven配置
  5. 5. 短命令行参数
  6. 6. YAML配置
    1. 6.1. 属性注入
  7. 7. Profile问题
  8. 8. 日志配置
  9. 9. SpingBoot+Thymeleaf
    1. 9.1. 实践
    2. 9.2. 手动渲染
    3. 9.3. 表达式语法
      1. 9.3.1. ${}
      2. 9.3.2. *{}
      3. 9.3.3. #{}
      4. 9.3.4. @{}
      5. 9.3.5. 字面量
      6. 9.3.6. 文本运算
      7. 9.3.7. 布尔运算
      8. 9.3.8. 比较和相等
      9. 9.3.9. 条件运算符
      10. 9.3.10. 内置对象
    4. 9.4. 设置属性值
    5. 9.5. 遍历
    6. 9.6. 分支语句
      1. 9.6.1. 本地变量
      2. 9.6.2. 内联
  10. 10. SpingBoot+Freemarker
  11. 11. Json
    1. 11.1. Jackson
      1. 11.1.1. 指定属性序列化/反序列化时的名称
      2. 11.1.2. 忽略字段
        1. 11.1.2.1. 忽略某一个字段
        2. 11.1.2.2. 忽略某些字段
      3. 11.1.3. 日期格式化
        1. 11.1.3.1. 局部配置
        2. 11.1.3.2. 全局配置
    2. 11.2. Gson
    3. 11.3. FastJson
  12. 12. 静态资源
    1. 12.1. 自定义配置
      1. 12.1.1. application.properties
      2. 12.1.2. Java代码定义
  13. 13. 文件上传
    1. 13.1. 单文件上传
    2. 13.2. 多文件上传
      1. 13.2.1. 合并多文件上传
      2. 13.2.2. 独立多文件上传
    3. 13.3. Ajax上传
  14. 14. ControllerAdvice注解的使用
    1. 14.1. 全局异常处理
    2. 14.2. 全局数据绑定
      1. 14.2.1. ModelAttribute
      2. 14.2.2. 定义全局数据
    3. 14.3. 全局数据预处理
  15. 15. 关于异常
    1. 15.1. 异常页面定义
    2. 15.2. 自定义异常
  16. 16. 跨域问题
  17. 17. 拦截器
  18. 18. 系统启动任务
    1. 18.1. CommandLineRunner
    2. 18.2. ApplicationRunner
  19. 19. 整合Web组件
    1. 19.1. Servlet
    2. 19.2. Filter
    3. 19.3. Listener
    4. 19.4. 扫描
  20. 20. 注册过滤器
  21. 21. 参数类型转换
  22. 22. 项目首页和角标
  23. 23. AOP
  24. 24. JdbcTemplate
    1. 24.1. 配置多数据源
  25. 25. MyBatis
    1. 25.1. 配置多数据源
  26. 26. Redis
  27. 27. Session共享
  28. 28. Redis处理接口幂等性
  29. 29. RESTful
    1. 29.1. 用法
      1. 29.1.1. 查询
      2. 29.1.2. 添加
      3. 29.1.3. 修改
      4. 29.1.4. 分页查询
    2. 29.2. 实现
  30. 30. Spring Cache
    1. 30.1. @CacheConfig
    2. 30.2. @Cacheable
    3. 30.3. @CachePut
    4. 30.4. @CacheEvict
  31. 31. SpringSecurity
  32. 32. WebSocket
  33. 33. ActiveMQ
  34. 34. RabbitMQ
    1. 34.1. Direct模式
    2. 34.2. Fanout模式
    3. 34.3. Topic模式
    4. 34.4. Header模式
  35. 35. Mail
  36. 36. 定时任务
    1. 36.1. @Scheduled
    2. 36.2. @Quartz
  37. 37. Swagger
  38. 38. 数据校验
    1. 38.1. 普通校验
    2. 38.2. 分组校验
  39. 39. 应用监控
  40. 40. 打包问题

SpringBoot随记

为什么叫随记呢

因为前面的笔记过于详细,从头到尾完整记下来会花掉很多时间…

so这篇就记得随意一点吧

Spring和SpringMVC都只是快速过了一下(跟没看一样 ^_^

Web容器基本配置

image-20230331195718253

Tomcat配置

日志

image-20230331195631263

HTTPS证书

生成秘钥复制到resource目录下

1
keytool -genkey -alias myhttps -keyalg RSA -keysize 2048 -keystore zhengru_key.p12 -validity 365

application.properties中配置证书

1
2
3
server.ssl.key-alias=myhttps
server.ssl.key-store=classpath:zhengru_key.p12
server.ssl.key-store-password=123456

配置文件名称和路径

位置问题

位置可以在一下四个地方

当然也可以用spring.config.location来指定其他位置

配置文件位置时,值一定要以/结尾

例如:spring.config.location=classpath:/javaboy/

image-20230401131512162

文件名问题

默认是叫application但是也可以指定其他的文件名

spring.config.name来指定

例如:spring.config.name=app.properties

image-20230401132239361

普通的属性注入

spring里的东西,跟springboot没有关系

image-20230401133923691

使用Value注解属性注入,数组也可以注入但是注意要用英文逗号隔开

image-20230401134049944

这里注意要在设置中更改文件编码为UTF-8

类型安全的属性注入

使用@ConfigurationProperties(prefix = "xxx")设置前缀是xxx,就会自动注入

1
2
3
4
5
6
7
8
9
@Component
@PropertySource("classpath:book.properties")
@ConfigurationProperties(prefix = "book")
public class Book {
private String name;
private String author;
private String[] tags;
//...
}

在配置文件中引用maven配置

需要使用@xxx@,因为会和${}冲突

短命令行参数

1
java -jar properties-0.0.1-SNAPSHOT.jar

改端口

1
java -jar properties-0.0.1-SNAPSHOT.jar --server.port=8081

YAML配置

Spring Boot 中的配置文件有两种格式,properties(默认)或者 yaml(yml也可)

属性注入

英文冒号后面需要加一个空格!

注意数组对象注入的形式

1
2
3
4
5
6
7
8
9
10
11
book:
name: 三国演义
tags:
- 历史
- 小说
- 明代
authors:
- name: 罗贯中
age: 88
- name: 施耐庵
age: 99

Profile问题

生产环境切换

1
2
3
spring.profiles.active=dev
spring.profiles.active=prod
spring.profiles.active=test

image-20230401143424135

日志配置

SpringBoot默认的日志实现是Logback

详细配置可以看松哥的这篇文章 点此跳转

SpingBoot+Thymeleaf

松哥的文章 点此跳转

与传统Java模板引擎不同的是,Thymeleaf支持HTML原型

实践

创建项目时就可以添加依赖

image-20230401165235093

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# THYMELEAF (ThymeleafAutoConfiguration)
# 开启模板缓存(默认值: true )
spring.thymeleaf.cache=true
# 检查模板是否存在,然后再呈现
spring.thymeleaf.check-template=true
# 检查模板位置是否正确(默认值 :true )
spring.thymeleaf.check-template-location=true
#Content-Type 的值(默认值: text/html )
spring.thymeleaf.content-type=text/html
# 开启 MVC Thymeleaf 视图解析(默认值: true )
spring.thymeleaf.enabled=true
# 模板编码
spring.thymeleaf.encoding=UTF-8
# 要被排除在解析之外的视图名称列表,⽤逗号分隔
spring.thymeleaf.excluded-view-names=
# 要运⽤于模板之上的模板模式。另⻅ StandardTemplate-ModeHandlers( 默认值: HTML5)
spring.thymeleaf.mode=HTML5
# 在构建 URL 时添加到视图名称前的前缀(默认值: classpath:/templates/ )
spring.thymeleaf.prefix=classpath:/templates/
# 在构建 URL 时添加到视图名称后的后缀(默认值: .html )
spring.thymeleaf.suffix=.html

写一个UserController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Controller
public class UserController {
@GetMapping("/hello")
public String index(Model model){
List<User> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
User u = new User();
u.setId(i);
u.setUsername("zhengru:"+i);
u.setAddress("zhengru.top:"+i);
users.add(u);
}
model.addAttribute("users",users);
return "hello";
}
}

在template目录下创建hello.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<table border="1">
<tr th:each = "u : ${users}">
<td th:text = "${u.id}"></td>
<td th:text = "${u.username}"></td>
<td th:text = "${u.address}"></td>
</tr>
</table>
</body>
</html>

手动渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootTest
class ThymeleafApplicationTests {
@Autowired
TemplateEngine templateEngine;
@Test
void contextLoads() {
Context ctx = new Context();
ctx.setVariable("username","邹川瑞");
ctx.setVariable("position","Java高级开发工程师");
ctx.setVariable("salary","600000");
String mail = templateEngine.process("mail", ctx);
System.out.println(mail);
}
}

表达式语法

说一下${}、*{}、#{}、@{}

${}

普通引用

*{}

可以实现以下操作

1
2
3
4
5
<div th:object="${user}">
<div th:text="*{username}"></div>
<div th:text="*{address}"></div>
<div th:text="*{id}"></div>
</div>

#{}

一般用于国际化

image-20230401211329154

1
<div th:text="#{hello}"></div>

@{}

1
2
3
4
5
<script th:src="@{https://localhost:8080/hello.js}"></script>
<script th:src="@{~/hello.js}"></script>
<script th:src="@{//localhost:8080/hello.js}"></script>
<script th:src="@{//localhost:8080/hello.js(name='zhengru',age=99)}"></script>
<script th:src="@{/hello.js}"></script>

字面量

  • 文本字面量:’one text’, ‘Another one!’,…
  • 数字字面量:0, 34, 3.0, 12.3,…
  • 布尔字面量:true, false
  • Null字面量:null
  • 字面量标记:one, sometext, main,…

如果文本是英文,并且不包含空格、逗号等字符,可以不用加单引号

文本运算

如果字符串中包含变量,也可以使用另一种简单的方式,叫做字面量置换,用 | 代替 '...' + '...',如下:

th:with 定义了一个局部变量 age,在其所在的 div 中可以使用该局部变量

布尔运算

  • 二元运算符:and, or
  • 布尔非(一元运算符):!, not

比较和相等

表达式里的值可以使用 >, <, >=<= 符号比较。==!= 运算符用于检查相等(或者不相等)。注意 XML规定 <> 标签不能用于属性值,所以应当把它们转义为 <>

如果不想转义,也可以使用别名:gt (>);lt (<);ge (>=);le (<=);not (!)。还有 eq (==), neq/ne (!=)

条件运算符

类似于我们 Java 中的三目运算符

内置对象

基本内置对象:

  • #ctx:上下文对象。
  • #vars: 上下文变量。
  • #locale:上下文区域设置。
  • #request:(仅在 Web 上下文中)HttpServletRequest 对象。
  • #response:(仅在 Web 上下文中)HttpServletResponse 对象。
  • #session:(仅在 Web 上下文中)HttpSession 对象。
  • #servletContext:(仅在 Web 上下文中)ServletContext 对象。

实用内置对象:

  • #execInfo:有关正在处理的模板的信息。
  • #messages:在变量表达式中获取外部化消息的方法,与使用#{…}语法获得的方式相同。
  • #uris:转义URL / URI部分的方法
  • #conversions:执行配置的转换服务(如果有)的方法。
  • #dates:java.util.Date对象的方法:格式化,组件提取等
  • #calendars:类似于#dates但是java.util.Calendar对象。
  • #numbers:用于格式化数字对象的方法。
  • #strings:String对象的方法:contains,startsWith,prepending / appending等
  • #objects:一般对象的方法。
  • #bools:布尔评估的方法。
  • #arrays:数组方法。
  • #lists:列表的方法。
  • #sets:集合的方法。
  • #maps:地图方法。
  • #aggregates:在数组或集合上创建聚合的方法。
  • #ids:处理可能重复的id属性的方法(例如,作为迭代的结果)。

设置属性值

1
<img th:attr="src=@{/1.png},title=${user.username},alt=${user.username}">
1
<img src="/myapp/1.png" title="javaboy" alt="javaboy">
1
<img th:src="@{/1.png}" th:alt="${user.username}" th:title="${user.username}">
1
<img th:src="@{/1.png}" th:alt-title="${user.username}">

遍历

1
2
3
4
5
6
<table border="1">
<tr th:each="u : ${users}">
<td th:text="${u.username}"></td>
<td th:text="${u.address}"></td>
</tr>
</table>

还有很多细节可以看松哥的文章

分支语句

th:if

th:unless

th:switch…

本地变量

这个我们前面已经涉及到了,使用 th:with 可以定义一个本地变量

内联

1
<div>hello [[${user.username}]]</div>

[[...]] 对应于 th:text (结果会是转义的 HTML)

[(...)]对应于 th:text,它不会执行任何的 HTML 转义

1
2
3
4
<div th:with="str='hello <strong>javaboy</strong>'">
<div>[[${str}]]</div>
<div>[(${str})]</div>
</div>

显示效果

image-20230401220303266

详细看松哥的文章 点此跳转

SpingBoot+Freemarker

文章地址 点此跳转

FreeMarkerProperties中则配置了Freemarker的基本信息,例如模板位置在 classpath:/templates/ ,再例如模板后缀为 .ftl,那么这些配置我们以后都可以在application.properties中进行修改

Json

SpringMVC框架中已经自动配置了jacksongsonHTTPMessageConverter

只需要添加依赖就能使用

序列化:对象 -> JSON(响应JSON)

反序列化:JSON -> 对象(请求参数是JSON)

Jackson

指定属性序列化/反序列化时的名称

1
2
@JsonProperty("name")
private String username;

忽略字段

忽略某一个字段

1
2
@JsonIgnore
private Integer id;

忽略某些字段

1
2
3
4
5
6
@JsonIgnoreProperties({"id","username"})
public class User {
private Integer id;
private String username;
private String address;
}

日期格式化

局部配置

1
2
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
private Date birthday;

全局配置

1
2
3
4
5
6
7
8
9
@Configuration
public class WebMvcConfig {
@Bean
ObjectMapper ObjectMapper(){
ObjectMapper om = new ObjectMapper();
om.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
return om;
}
}

Gson

就放两个图因为我也没试过

一个是application.properties

image-20230403192630985

另一个是WebMvcConfig

image-20230403193038355

FastJson

需要自己配,可以在方法中或者是自己配一个bean

image-20230403193308308

静态资源

一共有五个位置可以放置静态资源(优先级)

  1. classpath:/META-INF/resources/
  2. classpath:/resources/
  3. classpath:/static/
  4. classpath:/public/
  5. /

第五个用的比较少

自定义配置

application.properties

第一行配置表示定义资源位置,第二行配置表示定义请求 URL 规则

1
2
spring.web.resources.static-locations=classpath:/
spring.mvc.static-path-pattern=/**

Java代码定义

这里按照我的理解就是在用addResourceHandler映射实际资源的位置也就是addResourceLocations

我可以通过/123/01.html来访问/zhengru/01.html这个资源

1
2
3
4
5
6
7
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/123/**").addResourceLocations("classpath:/zhengru/");
}
}

文件上传

单文件上传

写一个简单的页面

1
2
3
4
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit">
</form>

写一个controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RestController
public class FileUploadController {
SimpleDateFormat sdf = new SimpleDateFormat("/yyyy/MM/dd/");
@PostMapping("/upload")
public String upload(MultipartFile file, HttpServletRequest req){
String realPath = req.getServletContext().getRealPath("/");
String format = sdf.format(new Date());
String path = realPath + format;
File folder = new File(path);
if (!folder.exists()){
folder.mkdirs();
}
String oldName = file.getOriginalFilename();
String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));
try {
file.transferTo(new File(folder,newName));
String s = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + format + newName;
return s;
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
}

可以在application.properties中配置其他信息

比如限制单个文件的大小和限制所有文件的大小

1
2
spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB

多文件上传

合并多文件上传

1
2
3
4
<form action="/upload2" method="post" enctype="multipart/form-data">
<input type="file" name="files" multiple>
<input type="submit">
</form>

部分代码

1
2
3
4
5
6
7
8
9
10
11
try {
for (MultipartFile file : files) {
String oldName = file.getOriginalFilename();
String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));
file.transferTo(new File(folder,newName));
String s = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + format + newName;
System.out.println(s);
}
} catch (IOException e) {
e.printStackTrace();
}

独立多文件上传

1
2
3
4
5
6
<form action="/upload3" method="post" enctype="multipart/form-data">
<input type="file" name="file1">
<input type="file" name="file2">
<input type="file" name="file3">
<input type="submit">
</form>
1
2
3
public String upload(MultipartFile file1,MultipartFile file2,MultipartFile file3, HttpServletRequest req){
//...
}

Ajax上传

记得引入jQuery

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="result"></div>
<input type="file" id="file">
<input type="button" value="上传" onclick="uploadFile()">
<script language="JavaScript">
function uploadFile(){
var file = $("#file")[0].files[0];
var formData = new FormData();
formData.append("file",file);
$.ajax({
type:'post',
url:'/upload',
processData:false,
contentType:false,
data:formData,
success:function (msg) {
$("#result").html(msg)
}
})
}
</script>

ControllerAdvice注解的使用

全局异常处理

1
2
3
4
5
6
7
8
9
10
//@ControllerAdvice
@RestControllerAdvice
public class MyGlobalException {
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ModelAndView customException(MaxUploadSizeExceededException e) throws IOException {
ModelAndView mv = new ModelAndView("myerror");
mv.addObject("error",e.getMessage());
return mv;
}
}

全局数据绑定

ModelAttribute

后面用到了ModelAttribute注解,是MVC里的内容,先学一下

主要有两个作用

  1. 在数据回显时,给变量定义别名
  2. 定义全局数据

当用户访问当前Controller中的任意一个方法,在返回数据时,都会将添加了@ModelAttribute注解的方法的返回值,一起返回给前端

定义全局数据

1
2
3
4
5
6
7
8
9
10
@ControllerAdvice
public class MyGlobalData {
@ModelAttribute("info")
public Map<String,String> mydata(){
Map<String,String> info = new HashMap<>();
info.put("username","dzr");
info.put("address","zhengru.top");
return info;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class HelloController {
@GetMapping("/hello")
public void hello(Model model){
Map<String,Object> asMap = model.asMap();
Map<String,String> info = (Map<String,String>) asMap.get("info");
Set<String> keySet = info.keySet();
for (String s : keySet) {
System.out.println(s + "————" + info.get(s));
}
}
}

全局数据预处理

1
2
3
4
5
6
7
8
9
10
11
12
@ControllerAdvice
public class MyGlobalData {
@InitBinder("a")
public void a(WebDataBinder binder){
binder.setFieldDefaultPrefix("a.");
}

@InitBinder("b")
public void b(WebDataBinder binder){
binder.setFieldDefaultPrefix("b.");
}
}
1
2
3
4
5
6
7
8
@RestController
public class BookController {
@PostMapping("/book")
public void addBook(@ModelAttribute("b") Book book,@ModelAttribute("a") Author author){
System.out.println(book);
System.out.println(author);
}
}

关于异常

异常页面定义

404问题自动寻找异常页面的优先级,其他同理

  1. templates/error/404.html
  2. static/error/404.html
  3. templates/error/4xx.html
  4. static/error/4xx.html

使用动态显示异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<table border="1px">
<tr>
<td>path</td>
<td th:text="${path}"></td>
</tr>
<tr>
<td>error</td>
<td th:text="${error}"></td>
</tr>
<tr>
<td>message</td>
<td th:text="${message}"></td>
</tr>
<tr>
<td>timestamp</td>
<td th:text="${timestamp}"></td>
</tr>
<tr>
<td>status</td>
<td th:text="${status}"></td>
</tr>
</table>

自定义异常

1
2
3
4
5
6
7
8
9
10
11
@Component
public class MyErrorAtributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> map = super.getErrorAttributes(webRequest, options);
if ((Integer) map.get("status") == 404){
map.put("message","页面不存在");
}
return map;
}
}

跨域问题

第一种方式可以使用@CrossOrigin注解来让某一个类或某一个方法接收某个域的请求

1
@CrossOrigin(value = "http://localhost:8081/",maxAge = 1800)

第二种方式可以使用全局配置来一次性解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
// @Override
// public void addCorsMappings(CorsRegistry registry) {
// registry.addMapping("/**")
// .allowedHeaders("*")
// .allowedMethods("*")
// .allowedOrigins("http://localhost:8081/")
// .maxAge(1800);
// }
@Bean
CorsFilter corsFilter(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration cfg = new CorsConfiguration();
cfg.addAllowedOrigin("http://localhost:8081");
cfg.addAllowedMethod("*");
source.registerCorsConfiguration("/**",cfg);
return new CorsFilter(source);
}
}

拦截器

  • preHandle
    • 返回false,请求将不再继续执行
  • postHandle
    • Controller执行之后被调用
  • afterCompletion
    • preHandle返回true才会执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle");
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
System.out.println("postHandle");
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
System.out.println("afterCompletion");
}
}
1
2
3
4
5
6
7
8
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**")
.excludePathPatterns("/hello");
}
}
1
2
3
4
5
6
7
8
9
10
11
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "hello1";
}
@GetMapping("hello2")
public String hello2(){
return "hello2";
}
}

系统启动任务

CommandLineRunner

1
2
3
4
5
6
7
8
@Component
@Order(100)
public class MyCommandLineRunner01 implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("args1 = " + Arrays.toString(args));
}
}
1
2
3
4
5
6
7
8
@Component
@Order(99)
public class MyCommandLineRunner02 implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("args2 = " + Arrays.toString(args));
}
}

ApplicationRunner

两个用法差异不大,主要体现在对参数的处理上 ,ApplicationRunner的参数类型更加丰富一些

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
@Order(98)
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
List<String> nonOptionArgs = args.getNonOptionArgs();
System.out.println("nonOptionArgs = " + nonOptionArgs);
Set<String> optionNames = args.getOptionNames();
for (String optionName : optionNames) {
System.out.println(optionName + "-->" + args.getOptionValues(optionName));
}
String[] sourceArgs = args.getSourceArgs();
System.out.println("sourceArgs = " + Arrays.toString(sourceArgs));
}
}

整合Web组件

Servlet

1
2
3
4
5
6
7
8
9
10
11
12
@WebServlet("/hello")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("MyServlet");
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}

Filter

1
2
3
4
5
6
7
8
@WebFilter("/*")
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("MyFilter");
filterChain.doFilter(servletRequest,servletResponse);
}
}

Listener

1
2
3
4
5
6
7
8
9
10
11
12
@WebListener
public class MyListener extends RequestContextListener {
@Override
public void requestInitialized(ServletRequestEvent requestEvent) {
System.out.println("requestInitialized");
}

@Override
public void requestDestroyed(ServletRequestEvent requestEvent) {
System.out.println("requestDestroyed");
}
}

扫描

1
2
3
4
5
6
7
@SpringBootApplication
@ServletComponentScan("top.zhengru.webcomponent") //记得加
public class WebcomponentApplication {
public static void main(String[] args) {
SpringApplication.run(WebcomponentApplication.class, args);
}
}

注册过滤器

除了用WebFilterComponent注解以外,还可以用Configuration

并且这个方法能够解决无法同时指定拦截路径和优先级的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class FilterConfiguration {
@Bean
FilterRegistrationBean<MyFilter04> filter04FilterRegistrationBean04(){
FilterRegistrationBean<MyFilter04> bean = new FilterRegistrationBean<>();
bean.setOrder(90);
bean.setFilter(new MyFilter04());
bean.setUrlPatterns(Arrays.asList("/*"));
return bean;
}
@Bean
FilterRegistrationBean<MyFilter05> filter05FilterRegistrationBean05(){
FilterRegistrationBean<MyFilter05> bean = new FilterRegistrationBean<>();
bean.setOrder(89);
bean.setFilter(new MyFilter05());
bean.setUrlPatterns(Arrays.asList("/*"));
return bean;
}
}

参数类型转换

如果是key/value的形式传入的参数,会使用自定义类型转换器处理(比如下面的MyDateConverter)

如果是传入json则可以使用@RequestBody注解,不需要类型转换器,通过HttpMessageConverter转换为对象

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class UserController {
@PostMapping("/user")
public void addUser(User user){
System.out.println(user);
}

@PostMapping("/user2")
public void addUser2(@RequestBody User user){
System.out.println(user);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class MyDateConverter implements Converter<String,Date> {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
@Override
public Date convert(String source) {
try {
return sdf.parse(source);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}

项目首页和角标

优先找静态然后再找动态

1
2
3
4
5
6
7
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/index").setViewName("index");
}
}

角标就在那五个地方放一个favicon.ico即可,不需要其他的配置

AOP

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
@Component
@Aspect
public class LogAspect {
@Pointcut("execution(* top.zhengru.aop.Service.*.*(..))")
public void pc1(){
}
@Before("pc1()")
public void before(JoinPoint jp){
String name = jp.getSignature().getName();
System.out.println(name + "方法开始执行了");
}

@After("pc1()")
public void after(JoinPoint jp){
String name = jp.getSignature().getName();
System.out.println(name + "方法结束了");
}

@AfterReturning(value = "pc1()",returning = "s")
public void afterReturning(JoinPoint jp,String s){
String name = jp.getSignature().getName();
System.out.println(name + "方法返回值是 " + s);
}

@AfterThrowing(value = "pc1()",throwing = "e")
public void afterThrowing(JoinPoint jp,Exception e){
String name = jp.getSignature().getName();
System.out.println(name + "方法抛出了异常 " + e);
}

@Around("pc1()")
public Object around(ProceedingJoinPoint pjp){
try {
Object proceed = pjp.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
return null;
}
}

JdbcTemplate

GeneratedKeyHolder来获取主键

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
@Service
public class UserService {
@Autowired
JdbcTemplate jdbcTemplate;

public int addUser(User user){
int result = jdbcTemplate.update("insert into user (username,address) values (?,?)",user.getUsername(),user.getAddress());
return result;
}

public int addUser2(User user){
GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
int res = jdbcTemplate.update(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
PreparedStatement ps = con.prepareStatement("insert into user (username,address) values (?,?)", Statement.RETURN_GENERATED_KEYS);
ps.setString(1,user.getUsername());
ps.setString(2,user.getAddress());
return ps;
}
},keyHolder);
user.setId(keyHolder.getKey().longValue());
return res;
}

public List<User> getAllUser(){
List<User> list = jdbcTemplate.query("select * from user", new RowMapper<User>() {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
String username = rs.getString("username");
String address = rs.getString("address");
long id = rs.getLong("id");
User user = new User();
user.setId(id);
user.setUsername(username);
user.setAddress(address);
return user;
}
});
return list;
}
}

配置多数据源

image-20230408131738856

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.one")
DataSource dsOne() {
return DruidDataSourceBuilder.create().build();
}

@Bean
@ConfigurationProperties(prefix = "spring.datasource.two")
DataSource dsTwo() {
return DruidDataSourceBuilder.create().build();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class JdbcTemplateConfig {
@Bean
JdbcTemplate jdbcTemplateOne(@Qualifier("dsOne") DataSource dsOne) {
return new JdbcTemplate(dsOne);
}

@Bean
JdbcTemplate jdbcTemplateTwo(@Qualifier("dsTwo") DataSource dsTwo) {
return new JdbcTemplate(dsTwo);
}
}

MyBatis

可以在MybatisApplication前面加上@MapperScan注解全局配置扫描Mapper

其他的地方都差不多

1
2
@Autowired
UserMapper userMapper;

XML的话可以在application.properties中配置mapper文件的目录

1
2
#指定Mybatis的Mapper文件
mybatis.mapper-locations=classpath:mappers/*xml

配置多数据源

有点麻烦,下面是松哥的pdf截图

image-20230408155731369

image-20230408155748537

image-20230408155757488

image-20230408155813674

Redis

redis还没学就先简单看了一下

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
@SpringBootTest
class RedisApplicationTests {
@Autowired
RedisTemplate redisTemplate;

@Autowired
StringRedisTemplate stringRedisTemplate;

@Test
void contextLoads() {
User user = new User();
user.setUsername("dzr");
user.setAddress("zhengru.top");
ValueOperations ops = redisTemplate.opsForValue();
ops.set("u",user);
User u = (User) ops.get("u");
System.out.println(u);
}

@Test
void test1(){
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("dzr","zhengru.top");
String dzr = ops.get("dzr");
System.out.println("dzr = " + dzr);
}
}

Session共享

分别在80808081端口开启服务,发现8081可以共享8080session,因为Spring Session就是使用 Spring 中的代理过滤器,将所有的 Session 操作拦截下来,自动的将数据 同步到 Redis 中,或者自动的从 Redis 中读取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
public class HelloController {
@Value("${server.port}")
Integer port;

@GetMapping("/set")
public String set(HttpSession session){
session.setAttribute("dzr","zhengru.top");
return String.valueOf(port);
}

@GetMapping("/get")
public String get(HttpSession session){
String dzr = (String) session.getAttribute("dzr");
return dzr + ":" + port;
}
}

Redis处理接口幂等性

通过拦截器,检测有没有@AutoIdempotent注解,如果有则checkTokenredis中没有token就返回token不存在,相应的,如果先前已经生成了token,就返回hello

这里放出核心的部分

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
@Service
public class RedisService {
@Autowired
StringRedisTemplate stringRedisTemplate;

public boolean setEx(String key,String value,Long expireTime){
boolean result = false;
try {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set(key,value);
stringRedisTemplate.expire(key,expireTime, TimeUnit.SECONDS);
result = true;
} catch (Exception e){
e.printStackTrace();
}
return result;
}

public boolean exists(String key){
return stringRedisTemplate.hasKey(key);
}

public boolean remove(String key){
if (exists(key)){
return stringRedisTemplate.delete(key);
}
return false;
}
}
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
public class TokenService {
@Autowired
RedisService redisService;

public String createToken(){
String uuid = UUID.randomUUID().toString();
redisService.setEx(uuid,uuid,10000L);
return uuid;
}

public boolean checkToken(HttpServletRequest request) throws IdempotentException {
String token = request.getHeader("token");
if (StringUtils.isEmpty(token)){
token = request.getParameter("token");
if (StringUtils.isEmpty(token)){
throw new IdempotentException("Token不存在");
}
}
if (!redisService.exists(token)){
throw new IdempotentException("重复操作");
}
boolean remove = redisService.remove(token);
if (!remove) {
throw new IdempotentException("重复操作");
}
return true;

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class IdempotentInterceptor implements HandlerInterceptor {
@Autowired
TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)){
return true;
}
Method method = ((HandlerMethod) handler).getMethod();
AutoIdempotent idempotent = method.getAnnotation(AutoIdempotent.class);
if (idempotent != null) {
try {
return tokenService.checkToken(request);
} catch (IdempotentException e){
throw e;
}
}
return true;
}
}

image-20230410105621556

RESTful

在SpringBoot中构建RESTful非常容易

用法

查询

get

http://localhost:8080/users/1

添加

post/json添加数据

http://localhost:8080/users

修改

put/json

http://localhost:8080/users/8

分页查询

http://localhost:8080/users?page=0&size=3&sort=id,desc

实现

1
2
3
4
5
6
7
8
9
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String address;
//...
}
1
2
public interface UserDao extends JpaRepository<User,Long> {
}

Spring Cache

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableCaching //开启缓存功能
public class CacheApplication {

public static void main(String[] args) {
SpringApplication.run(CacheApplication.class, args);
}

}

@CacheConfig

这个注解在类上使用,用来描述该类中所有方法使用的缓存名称,当然也可以不使用该注解,直接在具体的缓存注解上配置名称

1
2
3
4
@Service
@CacheConfig(cacheNames = "c1")
public class UserService {
}

@Cacheable

这个注解一般加在查询方法上,表示将一个方法的返回值缓存起来,默认情况下,缓存的key就是方法的参数,缓存的value就是方法的返回值

1
2
3
4
5
@Cacheable(key = "#id")
public User getUserById(Integer id,String username) {
System.out.println("getUserById");
return getUserFromDBById(id);
}

@CachePut

这个注解一般加在更新方法上,当数据库中的数据更新后,缓存中的数据也要跟着更新,使用该注解,可以将方法的返回值自动更新到已经存在的key上

1
2
3
4
@CachePut(key = "#user.id")
public User updateUserById(User user) {
return user;
}

@CacheEvict

这个注解一般加在删除方法上,当数据库中的数据删除后,相关的缓存数据也要自动清除,该注解在使用的时候也可以配置按照某种条件删除(condition属性)或者或者配置清除所有缓存(allEntries属性)

1
2
3
4
@CacheEvict()
public void deleteUserById(Integer id) {
//在这里执行删除操作, 删除是去数据库中删除
}

SpringSecurity

这里的安全内容准备单独写一篇,所以先跳过了

WebSocket

下面是使用WebSocket实现的一个聊天室(多人)

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat").withSockJS();
}

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic");
}
}
1
2
3
4
5
6
7
8
9
@Controller
public class GreetingController {
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Message greeting(Message message){
return message;
}

}
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="/webjars/jquery/3.5.1/dist/jquery.min.js"></script>
<script src="/webjars/sockjs-client/1.1.2/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/2.3.3/stomp.min.js"></script>
</head>
<body>
<div>
<label for="username">请输入用户名:</label>
<input type="text" id="username" placeholder="用户名">
</div>
<div>
<input type="button" value="连接" id="connect">
<input type="button" value="断开连接" id="disconnect" disabled="disabled">
</div>
<div id="chat">

</div>
<div>
<label for="content">请输入聊天内容:</label>
<input type="text" id="content" placeholder="聊天内容">
</div>
<button id="send">发送</button>
<script>
var stompClient;
$(function (){
$("#connect").click(function (){
connect();
$("#send").click(function (){
stompClient.send("/hello",{},JSON.stringify({'name':$("#username").val(),'content':$("#content").val()}))
})
})
})
function connect(){
if (!$("#username").val()){
return;
}
var soketjs = new SockJS("/chat");
stompClient = Stomp.over(soketjs);
stompClient.connect({},function (frame){
setConnect(true)
stompClient.subscribe("/topic/greetings",function (greeting){
var msgContent = JSON.parse(greeting.body);
$("#chat").append("<div>" + msgContent.name + ":" + msgContent.content + "</div>")
})
})
}
function setConnect(connected){
$("#connect").prop("disabled",connected);
$("#disconnect").prop("disabled",!connected)
}
</script>
</body>
</html>

ActiveMQ

1
2
3
4
spring.activemq.broker-url=tcp://127.0.0.1:61616
spring.activemq.packages.trust-all=true
spring.activemq.user=admin
spring.activemq.password=admin
1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootApplication
public class JmsApplication {

public static void main(String[] args) {
SpringApplication.run(JmsApplication.class, args);
}

@Bean
Queue queue(){
return new ActiveMQQueue("dzr-queue");
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class JmsComponent {
@Autowired
JmsMessagingTemplate jmsMessagingTemplate;
@Autowired
Queue queue;

public void send(Message message){
jmsMessagingTemplate.convertAndSend(this.queue,message);
}

@JmsListener(destination = "dzr-queue")
public void receive(Message msg){
System.out.println("msg = " + msg);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootTest
class JmsApplicationTests {
@Autowired
JmsComponent jmsComponent;

@Test
void contextLoads() {
Message message = new Message();
message.setContent("hello dzr");
message.setDate(new Date());
jmsComponent.send(message);
}

}

RabbitMQ

Direct模式

这里注释的部分可以省略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class DirectConfig {
@Bean
Queue directQueue(){
return new Queue("dzr-queue");
}

// @Bean
// DirectExchange directExchange(){
// return new DirectExchange("dzr-direct",true,false);
// }
//
// @Bean
// Binding directBinding(){
// return BindingBuilder.bind(directQueue()).to(directExchange()).with("direct");
// }
}
1
2
3
4
5
6
7
@Component
public class DirectReceiver {
@RabbitListener(queues = "dzr-queue")
public void handler(String msg){
System.out.println("msg = " + msg);
}
}
1
2
3
4
@Test
void contextLoads() {
rabbitTemplate.convertAndSend("dzr-queue","hello,dzr!");
}

Fanout模式

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
public class FanoutConfig {
@Bean
Queue queueOne(){
return new Queue("queue-one");
}

@Bean
Queue queueTwo(){
return new Queue("queue-two");
}

@Bean
FanoutExchange fanoutExchange(){
return new FanoutExchange("dzr",true,false);
}

@Bean
Binding bindingOne(){
return BindingBuilder.bind(queueOne()).to(fanoutExchange());
}

@Bean
Binding bindingTwo(){
return BindingBuilder.bind(queueTwo()).to(fanoutExchange());
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class FaoutReceiver {
@RabbitListener(queues = "queue-one")
public void handler1(String msg){
System.out.println("msg = " + msg);
}

@RabbitListener(queues = "queue-two")
public void handler2(String msg){
System.out.println("msg = " + msg);
}

}
1
2
3
4
@Test
void contextLoads() {
rabbitTemplate.convertAndSend("dzr-fanout",null,"hello dzr!");
}

Topic模式

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
@Configuration
public class TopicConfig {
@Bean
Queue xiaomi(){
return new Queue("xiaomi");
}

@Bean
Queue huawei(){
return new Queue("huawei");
}
@Bean
Queue phone(){
return new Queue("phone");
}
@Bean
TopicExchange topicExchange(){
return new TopicExchange("dzr-topic",true,false);
}

@Bean
Binding xiaomiBinding(){
return BindingBuilder.bind(xiaomi()).to(topicExchange()).with("xiaomi.#");
}

@Bean
Binding huaweiBinding(){
return BindingBuilder.bind(huawei()).to(topicExchange()).with("huawei.#");
}

@Bean
Binding phoneBinding(){
return BindingBuilder.bind(phone()).to(topicExchange()).with("#.phone.#");
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class TopicReceiver {
@RabbitListener(queues = "phone")
public void handler1(String msg){
System.out.println("msg:phone = " + msg);
}

@RabbitListener(queues = "xiaomi")
public void handler2(String msg){
System.out.println("msg:xiaomi = " + msg);
}

@RabbitListener(queues = "huawei")
public void handler3(String msg){
System.out.println("msg:huawei = " + msg);
}
}
1
2
3
4
5
6
@Test
void contextLoads() {
rabbitTemplate.convertAndSend("dzr-topic","xiaomi.news","小米新闻");
rabbitTemplate.convertAndSend("dzr-topic","xiaomi.phone","小米手机");
rabbitTemplate.convertAndSend("dzr-topic","huawei.phone","华为手机");
}

Header模式

用的比较少,节约篇幅就不放代码了

Mail

下面的username就是发件人邮箱,passwordPOP3/SMTP协议的授权码(不是邮箱密码!)

1
2
3
4
5
6
7
spring.mail.host=smtp.163.com
spring.mail.port=25
spring.mail.username=***
spring.mail.password=***
spring.mail.default-encoding=UTF-8
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
spring.mail.properties.mail.debug=true

contextLoads是普通邮件,test1是附件邮件,test2是图片资源邮件,test3是发送freemaker模板

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package top.zhengru.mail;

import freemarker.core.Configurable;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import top.zhengru.mail.model.User;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Date;

@SpringBootTest
class MailApplicationTests {

@Autowired
JavaMailSender javaMailSender;

@Test
void contextLoads() {
SimpleMailMessage simMsg = new SimpleMailMessage();
simMsg.setFrom("***");
simMsg.setTo("***");
simMsg.setSentDate(new Date());
simMsg.setSubject("邮件主题-测试邮件");
simMsg.setText("邮件内容-测试邮件");
javaMailSender.send(simMsg);
}

@Test
void test1() throws MessagingException {
File file = new File("C:/Users/董政儒/Desktop/头像.jpg");
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setFrom("***");
helper.setTo("***");
helper.setSentDate(new Date());
helper.setSubject("邮件主题-测试邮件");
helper.setText("邮件内容-测试邮件");
helper.addAttachment(file.getName(),file);
javaMailSender.send(mimeMessage);
}

@Test
void test2() throws MessagingException {
File file = new File("C:/Users/董政儒/Desktop/头像.jpg");
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setFrom("***");
helper.setTo("***");
helper.setSentDate(new Date());
helper.setSubject("邮件主题-测试邮件");
helper.setText("<div>hello,这是一封带图片资源的邮件...</div><div><img src='cid:p01' /></div>",true);
helper.addInline("p01",file);
javaMailSender.send(mimeMessage);
}

@Test
void test3() throws MessagingException, IOException, TemplateException {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setFrom("***");
helper.setTo("***");
helper.setSentDate(new Date());
helper.setSubject("邮件主题-测试邮件");
Configuration cfg = new Configuration(Configuration.getVersion());
cfg.setClassLoaderForTemplateLoading(MailApplicationTests.class.getClassLoader(),"mail");
Template template = cfg.getTemplate("mail.ftl");
User user = new User();
user.setUsername("***");
user.setCompany("***");
user.setPosition("***");
user.setSalary(8000.0);
StringWriter out = new StringWriter();
template.process(user,out);
helper.setText(out.toString(), true);
javaMailSender.send(mimeMessage);
}
}

定时任务

@Scheduled

记得在@SpringBootApplication下加@EnableScheduled

  • fixedDelay
    • 控制方法执行的间隔时间,是以上一次方法执行完开始算起,如上一次方法执行阻塞住了,那么直到上一次执行完,并间隔给定的时间后,执行下一次
  • fixedRate
    • 是按照一定的速率执行,是从上一次方法执行开始的时间算起,如果上一次方法阻塞住了,下一次也是不会执行,但是在阻塞这段时间内累计应该执行的次数,当不再阻塞时,一下子把这些全部执行掉,而后再按照固定速率继续执行
  • initialDelay
    • 如:@Scheduled(initialDelay = 10000,fixedRate = 15000,这个定时器就是在上一个的基础上加了一个initialDelay = 10000 意思就是在容器启动后,延迟10秒后再执行一次定时器,以后每15秒再执行一次该定时器
  • cron
    • 表达式可以定制化执行任务,但是执行的方式是与fixedDelay相近的,也是会按照上一次方法结束时间开始算起
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
public class MySchedule {
@Scheduled(fixedDelay = 1000)
public void fixedDelay(){
System.out.println("fixedDelay:" + new Date());
}

@Scheduled(fixedRate = 1000)
public void fixedRate(){
System.out.println("fixedRate:" + new Date());
}

@Scheduled(initialDelay = 1000,fixedRate = 1000)
public void initDelay(){
System.out.println("initDelay:" + new Date());
}

@Scheduled(cron = "0/5 * * * * *")
public void cron(){
System.out.println("cron:" + new Date());
}
}

@Quartz

1
2
3
4
5
6
@Component
public class MyJob01 {
public void sayHello(){
System.out.println("MyJob01:" + new Date());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class MyJob02 extends QuartzJobBean {
private String name;

public void setName(String name) {
this.name = name;
}

@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
System.out.println("MyJob02:" + name + ":" + new Date());
}
}
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
public class QuartzConfig {
@Bean
MethodInvokingJobDetailFactoryBean jobDetail01(){
MethodInvokingJobDetailFactoryBean bean = new MethodInvokingJobDetailFactoryBean();
bean.setTargetBeanName("myJob01");
bean.setTargetMethod("sayHello");
return bean;
}

@Bean
JobDetailFactoryBean jobDetail02(){
JobDetailFactoryBean bean = new JobDetailFactoryBean();
bean.setJobClass(MyJob02.class);
JobDataMap map = new JobDataMap();
map.put("name","dzr");
bean.setJobDataMap(map);
return bean;
}

@Bean
SimpleTriggerFactoryBean simpleTriggerFactoryBean(){
SimpleTriggerFactoryBean bean = new SimpleTriggerFactoryBean();
bean.setJobDetail(jobDetail01().getObject());
bean.setRepeatCount(3);
bean.setStartDelay(1000);
bean.setRepeatInterval(1000);
return bean;
}

@Bean
CronTriggerFactoryBean cronTriggerFactoryBean(){
CronTriggerFactoryBean bean = new CronTriggerFactoryBean();
bean.setJobDetail(jobDetail02().getObject());
bean.setCronExpression("0/5 * * * * ?");
return bean;
}

@Bean
SchedulerFactoryBean schedulerFactoryBean(){
SchedulerFactoryBean bean = new SchedulerFactoryBean();
bean.setTriggers(simpleTriggerFactoryBean().getObject(),cronTriggerFactoryBean().getObject());
return bean;
}
}

Swagger

第一次跑的时候报错了,查了一下Swagger3.0不兼容SpringBoot2.6.x及以上的版本,于是就把SpringBoot版本降到了2.5.6

建议还是使用Swagger2.x?

  • @ApiIgnore
    • 忽略这个接口
  • @ApiResponses
    • 响应码描述
  • @ApiOperation
    • 接口名称及描述
  • @ApiImplicitParams
    • 参数类型、描述
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
@RestController
public class UserController {
@GetMapping("/hello")
@ApiIgnore
public String hello(){
return "hello";
}

@ApiResponses({
@ApiResponse(responseCode = "200",description = "请求成功"),
@ApiResponse(responseCode = "500",description = "请求失败")
})
@GetMapping("/user/{id}")
@ApiOperation(value = "查询用户",notes = "根据id查询用户")
@ApiImplicitParams({
@ApiImplicitParam(paramType = "body",name = "id",value = "用户 id",required = true)
})
public String getUserById(@PathVariable Integer id){
return "user:" + id;
}

@PostMapping("/user")
@Parameter(name = "user")
public String addUser(@RequestBody User user){
return user.toString();
}
}
1
2
3
4
5
6
7
8
9
10
@ApiModel(value = "用户实体类",description = "这个类定义了用户所有的属性")
public class User {
@ApiModelProperty(value = "用户ID")
private Long id;
@ApiModelProperty(value = "用户名")
private String username;
@ApiModelProperty(value = "用户地址")
private String address;
//...
}

数据校验

  • @Null 被注解的元素必须为 null
  • @NotNull 被注解的元素必须不为 null
  • @AssertTrue 被注解的元素必须为 true
  • @AssertFalse 被注解的元素必须为 false
  • @Min(value) 被注解的元素必须是一个数字,其值必须大于等于指定的最小值
  • @Max(value) 被注解的元素必须是一个数字,其值必须小于等于指定的最大值
  • @DecimalMin(value) 被注解的元素必须是一个数字,其值必须大于等于指定的最小值
  • @DecimalMax(value) 被注解的元素必须是一个数字,其值必须小于等于指定的最大值
  • @Size(max=, min=) 被注解的元素的大小必须在指定的范围内
  • @Digits (integer, fraction) 被注解的元素必须是一个数字,其值必须在可接受的范围内
  • @Past 被注解的元素必须是一个过去的日期
  • @Future 被注解的元素必须是一个将来的日期
  • @Pattern(regex=,flag=) 被注解的元素必须符合指定的正则表达式
  • @NotBlank(message =) 验证字符串非 null,且长度必须大于0
  • @Email 被注解的元素必须是电子邮箱地址
  • @Length(min=,max=) 被注解的字符串的大小必须在指定的范围内
  • @NotEmpty 被注解的字符串的必须非空
  • @Range(min=,max=,message=) 被注解的元素必须在合适的范围内

普通校验

1
2
3
4
5
6
7
8
public class User {
private Long id;
@Size(min = 2,max = 5,message = "{user.username.size}")
private String username;
@NotNull
private String address;
//...
}

这里的message可以在resource目录下创建一个ValidationMessages.properties文件,在里面定义提示,例如:

1
user.username.size=username长度介于2-5

读取校验结果,BindingResult里如果有信息,即校验失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
public class UserController {
@PostMapping("/user")
public void addUser(@Validated User user, BindingResult result){
if (result != null && result.hasErrors()){
List<ObjectError> allErrors = result.getAllErrors();
for (ObjectError error : allErrors) {
System.out.println("error.getDefaultMessage() = " + error.getDefaultMessage());
}
}else {
System.out.println(user.toString());
}

}
}

分组校验

创建两个接口然后就可以在校验条件注解里加入group

image-20230413113029629

1
2
3
4
@Size(min = 2,max = 5,message = "{user.username.size}",groups = {ValidationGroup1.class})
private String username;
@NotNull(groups = {ValidationGroup1.class, ValidationGroup2.class})
private String address;

接着就可以在校验注解里加入分组

1
2
3
public void addUser(@Validated(ValidationGroup1.class) User user, BindingResult result){
//...
}

应用监控

先鸽一下…

打包问题

可执行jar和可依赖jar同时存在

1
2
3
4
5
6
7
8
9
10
11
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>exec</classifier>
</configuration>
</plugin>
</plugins>
</build>

暂时就先到这,如果有补充会继续更新