JDK 新特性

springBoot3因为不支持JDK8了,所以有些常用的新特性需要掌握

JDK:JDK19

Java Record

Java14 中预览的新特性叫做Record,在Java 中,Record 是一种特殊类型的Java 类。可用来创建不可变类,语法
简短。参考JEP 395. Jackson 2.12 支持Record 类
任何时候创建Java 类,都会创建大量的样板代码,我们可能做如下

  • 每个字段的set,get 方法
  • 公共的构造方法
  • 重写hashCode, toString(), equals()方法

Java Record 避免上述的样板代码,如下特点

  • 带有全部参数的构造方法
  • public 访问器
  • toString(),hashCode(),equals()
  • 无set,get 方法。没有遵循Bean 的命名规范
  • final 类,不能继承Record,Record 为隐士的final 类。除此之外与普通类一样
  • 不可变类,通过构造创建Record
  • final 属性,不可修改
  • 不能声明实例属性,能声明static 成员

创建Record

  • 创建Student Record
1
2
public record Student(Integer id,String name,String email,Integer age) {
}
  • 创建Record对象
1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Student lisi = new Student(1001, "lisi","lisi@qq.com",20);
System.out.println("lisi = " + lisi.toString());
Student zhangsan = new Student(1002, "zhangsan","lisi@qq.com",20);
System.out.println("zhangsan = " + zhangsan.toString());
System.out.println("lisi.equals(zhangsan) = " + lisi.equals(zhangsan));
System.out.println("lisi.name() = " + lisi.name());
System.out.println("zhangsan.name() = " + zhangsan.name());
}
  • 查看输出

Record 通过构造方法创建了只读的对象,能够读取每个属性,不能设置新的属性值。Record 用于创建不可变的
对象,同时减少了样板代码
Record 对每个属性提供了public 访问器,例如lisi.name()

Instance Methods

Record 是Java 类,和普通Java 类一样定义方法

  • 创建实例方法
1
2
3
4
5
public record Student(Integer id,String name,String email,Integer age) {
public String concat(){
return String.format("姓名:%s,年龄是:%d", this.name,this.age);
}
}
  • 调用实例方法
1
2
3
4
5
public static void main(String[] args) {
Student lisi = new Student(1001, "lisi","lisi@qq.com",20);
String nameAndAge = lisi.concat();
System.out.println( nameAndAge);
}
  • 查看输出

Static Method

  • 创建静态方法
1
2
3
4
5
6
7
8
9
public record Student(Integer id,String name,String email,Integer age) {
public String concat(){
return String.format("姓名:%s,年龄是:%d", this.name,this.age);
}
/** 静态方法*/
public static String emailUpperCase(String email){
return Optional.ofNullable(email).orElse("no email").toUpperCase();
}
}
  • 测试静态方法
1
2
3
4
public static void main(String[] args) {
String emailUpperCase = Student.emailUpperCase("lisi@163.com");
System.out.println("emailUpperCase = " + emailUpperCase);
}

构造方法

可以在Record 中添加构造方法, 有三种类型的构造方法分别

  • 紧凑型构造方法没有任何参数,甚至没有括号。
  • 规范构造方法是以所有成员作为参数
  • 定制构造方法是自定义参数个数

紧凑和定制构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public record Student(Integer id,String name,String email,Integer age) {
/*紧凑构造方法*/
public Student {
System.out.println("id"+ id );
if( id < 1 ){
throw new RuntimeException("ok");
}
}
/*自定义构造方法*/
public Student(Integer id, String name) {
this(id, name, null, null);
}
}
  • 编译Student.java -> Student.class(会发现紧凑构造方法和规范构造方法合并了)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public record Student(Integer id, String name, String email, Integer age) {
/** 紧凑构造方法和规范构造方法合并了*/
public Student(Integer id, String name, String email, Integer age) {
System.out.println("id" + id);
if (id < 1) {
throw new RuntimeException("ok"); }
else {
this.id = id;
this.name = name;
this.email = email;
this.age = age;
}
}
public Student(Integer id, String name) {
this(id, name, (String)null, (Integer)null);
}
}

Record 与Lombok

Java Record 是创建不可变类且减少样板代码的好方法。Lombok 是一种减少样板代码的工具。两者有表面上的重叠部
分。可能有人会说Java Record 会代替Lombok. 两者是有不同用途的工具

  • Lombok 提供语法的便利性,通常预装一些代码模板,根据您加入到类中的注解自动执行代码模板。这样的库纯粹是
    为了便实现POJO 类。通过预编译代码。将代码的模板加入到class 中
  • Java Record 是语言级别的,一种语义特性,为了建模而用,数据聚合。简单说就是提供了通用的数据类,充当“数据
    载体”,用于在类和应用程序之间进行数据传输

实现接口

Java Record 可以与普通类一样实现接口,重写接口的方法

  • 创建新的接口,定义一个规范方法
1
2
3
4
public interface PrintInterface {
/** 输出自定义描述信息*/
void print();
}
  • 创建新的Record 实现接口,重写接口的方法,实现当前Record 有关的业务逻辑
1
2
3
4
5
6
7
public record ProductRecord(String id,String name,Integer qty) implements PrintInterface {
@Override
public void print() {
String productDesc = String.join("-", id, name, qty.toString());
System.out.println("商品信息= " + productDesc);
}
}
  • 测试print 方法
1
2
3
4
public static void main(String[] args) {
ProductRecord product = new ProductRecord("P001", "手机", 100);
product.print();
}

Local Record

Record 可以作为局部对象使用。在代码块中定义并使用Record,下面定义一个SaleRecord

  • 定义Local Record
1
2
3
4
5
6
7
8
public static void main(String[] args) {
//定义Java Record
record SaleRecord(String saleId,String productName,Double money){};
//创建Local Record
SaleRecord saleRecord = new SaleRecord("S22020301", "手机", 3000.0);
//使用SaleRecord
System.out.println("销售记录= " + saleRecord.toString());
}

嵌套Record

多个Record 可以组合定义, 一个Record 能够包含其他的Record
我们定义Record 为Customer,存储客户信息,包含了Address 和PhoneNumber 两个Record

  • 定义Record
1
2
3
public record Address(String city,String address,String zipcode) {}
public record PhoneNumber(String areaCode,String number) {}
public record Customer(String id, String name, PhoneNumber phoneNumber,Address address) {}
  • 创建Customer 对象
1
2
3
4
5
6
public static void main(String[] args) {
Address address = new Address("北京", "大兴区凉水河二街-8 号10 栋三层", "100176");
PhoneNumber phoneNumber = new PhoneNumber("010", "400-8080-105");
Customer customer = new Customer("C1001", "李项", phoneNumber, address);
System.out.println("客户= " + customer.toString());
}

instanceof 判断Record 类型

  • 声明Person Record,拥有两个属性name 和age
1
2
public record Person(String name,Integer age) {
}
  • 在一个业务方法判断当是Record 类型时,继续判断age 年龄是否满足18 岁
1
2
3
4
5
6
7
8
9
public class SomeService {
public boolean isEligible(Object obj){
// 判断obj 为Person 记录类型
if( obj instanceof Person(String name, Integer age)){
return age >= 18;
}
return false;
}
}

instanceof 还可以下面的方式

1
2
3
4
5
6
7
if( obj instanceof Person(String name, Integer age) person){
return person.age() >= 18;
}
//或者
if( obj instanceof Person p){
return p.age() >= 18;
}
  • 测试代码
1
2
3
4
5
public static void main(String[] args) {
SomeService service = new SomeService();
boolean flag = service.isEligible(new Person("李四", 20));
System.out.println("年龄符合吗?" + flag);
}

处理判断中Record 为null

Java Record 能够自动处理null

  • record 为null
1
2
3
4
5
public static void main(String[] args) {
SomeService service = new SomeService();
boolean eligible = service.isEligible(null);
System.out.println("年龄符合吗?" + eligible);
}

总结

  1. abstract 类java.lang.Record 是所有Record 的父类

  2. 有对于equals(),hashCode(),toString()方法的定义说明

  3. Record 类能够实现java.io.Serializable 序列化或反序列化

  4. Record 支持泛型,例如record Gif<T>( T t ) { }

  5. java.lang.Class 类与Record 类有关的两个方法:

    • boolean isRecord() : 判断一个类是否是Record 类型
    • RecordComponent[] getRecordComponents():Record 的数组,表示此记录类的所有记录组件
    1
    2
    3
    4
    5
    6
    7
    Customer customer = new Customer(....);
    RecordComponent[] recordComponents = customer.getClass().getRecordComponents();
    for (RecordComponent recordComponent : recordComponents) {
    System.out.println("recordComponent = " + recordComponent);
    }
    boolean record = customer.getClass().isRecord();
    System.out.println("record = " + record);

Switch

Switch 的三个方面,参考:JEP 361

  • 支持箭头表达式
  • 支持yied 返回值
  • 支持Java Record

箭头表达式,新的case 标签

Switch 新的语法,case label -> 表达式|throw 语句|block

  • 新的case 标签
1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
int week = 7;
String memo = "";
switch (week){
case 1 -> memo = "星期日,休息";
case 2,3,4,5,6-> memo="工作日";
case 7 -> memo="星期六,休息";
default -> throw new IllegalArgumentException("无效的日期:");
}
System.out.println("week = " + memo);
}

yeild 返回值

yeild 让switch 作为表达式,能够返回值

语法
变量= switch(value) { case v1: yield 结果值; case v2: yield 结果值;case v3,v4,v5.. yield 结果值}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) {
int week = 1;
//yield 是switch 的返回值, yield 跳出当前switch 块
String memo = switch (week){
case 1 ->{
System.out.println("week=1 的表达式部分");
yield "星期日,休息";
}
case 2,3,4,5,6 ->{
System.out.println("week=2,3,4,5,6 的表达式部分");
yield "工作日";
}
case 7 -> {
System.out.println("week=7 的表达式部分");
yield "星期六,休息";
}
default -> {
System.out.println("其他语句");
yield "无效日期";
}
};
System.out.println("week = " + memo);
}

Java Record

switch 表达式中使用record,结合case 标签-> 表达式,yield 实现复杂的计算

  • 准备三个Record
1
2
3
public record Line(int x,int y) {}
public record Rectangle(int width,int height) {}
public record Shape(int width,int height) {}
  • switch record
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
Line line = new Line(10,100);
Rectangle rectangle = new Rectangle(100,200);
Shape shape = new Shape(200,200);
Object obj = rectangle;
int result = switch (obj){
case Line(int x,int y) -> {
System.out.println("图形是线, X:"+x+",Y:"+y);
yield x+y;
}
case Rectangle(int w,int h) -> w * h;
case Shape(int w,int h) ->{
System.out.println("这是图形,要计算周长");
yield 2* (w + h);
}
default -> throw new IllegalStateException("无效的对象:" + obj);
};
System.out.println("result = " + result);
}

Text Block

Text Block 处理多行文本十分方便,省时省力。无需连接”+”,单引号,换行符等。Java 15 ,参考JEP 378

认识文本块

语法:使用三个双引号字符括起来的字符串

1
2
3
"""
内容
"""

文本块定义要求

  • 文本块以三个双引号字符开始,后跟一个行结束符
  • 不能将文本块放在单行上
  • 文本块的内容也不能在没有中间行结束符的情况下跟随三个开头双引号

三个双引号字符""" 与两个双引号""的字符串处理是一样的。与普通字符串一样使用。例如equals() , "==" ,连接字符串(”+“), 作为方法的参数等

文本块与普通的双引号字符串一样

Text Block 使用方式与普通字符串一样,==,equals 比较,调用String 类的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void fun1() {
String s1= """
lisi
""";
String s2 = """
lisi
""";
//比较字符串
boolean b1 = s1.equals(s2);
System.out.println("b1 = " + b1);
//使用==的比较
boolean b2 = s1 == s2;
System.out.println("b2 = " + b2);
String msg = """
hello world""";
//字符串方法substring
String sub = msg.substring(0, 5);
System.out.println("sub = " + sub);
}

空白

  • JEP 378 中包含空格处理的详细算法说明
  • Text Block 中的缩进会自动去除,左侧和右侧的
  • 要保留左侧的缩进,空格。将文本块的内容向左移动(tab 键)

tab

1
2
3
4
5
6
7
8
9
public void fun2(){
//按tab 向右移动,保留左侧空格
String html= """
<html>
<body>qianqianzyk</body>
</html>
""";
System.out.println( html);
}

indent()方法

1
2
3
4
5
6
7
8
9
10
11
public void fun3(){
String colors= """
red
green
blue
""";
System.out.println( colors);
//indent(int space)包含缩进,space 空格的数量
String indent = colors.indent(5);
System.out.println( indent);
}

文本块的方法

Text Block 的格式方法formatted()

1
2
3
4
5
6
7
8
public void fun4(){
String info= """
Name:%s
Phone:%s
Age:%d
""".formatted("张三","13800000000",20);
System.out.println("info = " + info);
}

转义字符

新的转义字符”",表示隐士换行符,这个转义字符被Text Block 转义为空格。通常用于是拆分非常长的字符串文本,串联多个较小子字符串,包装为多行生成字符串

新的转义字符,组合非常长的字符串

1
2
3
4
5
6
7
8
public void fun5(){
String str= """
Spring Boot 是一个快速开发框架\
基于\"Spring\"框架,创建Spring 应用\
内嵌Web 服务器,以jar 或war 方式运行\
""";
System.out.println("str = " + str);
}

输出

1
2
Spring Boot 是一个快速开发框架基于Spring 框架,创建Spring 应用内嵌Web 服务器,以
jar 或war 方式运行

总结

  1. 多行字符串,应该使用Text Block
  2. 当Text Block 可以提高代码的清晰度时,推荐使用。比如代码中嵌入SQL 语句
  3. 避免不必要的缩进,开头和结尾部分
  4. 使用空格或仅使用制表符文本块的缩进。混合空白将导致不规则的缩进
  5. 对于大多数多行字符串, 分隔符位于上一行的右端,并将结束分隔符位于文本块单独行上

var

在JDK 10 及更高版本中,可以使用var 标识符声明具有非空初始化式的局部变量,这可以帮助编写简洁的代码,消除冗余信息使代码更具可读性,谨慎使用

var 声明局部变量

var 特点

  • var 是一个保留字,不是关键字(可以声明var 为变量名)
  • 方法内声明的局部变量,必须有初值
  • 每次声明一个变量,不可复合声明多个变量。var s1=”Hello”, age=20; //Error
  • var 动态类型是编译器根据变量所赋的值来推断类型
  • var 代替显示类型,代码简洁,减少不必要的排版,混乱

var 优缺点

  • 代码简洁和整齐
  • 降低了程序的可读性(无强类型声明)
1
2
3
4
5
6
7
8
//通常
try (Stream<Customer> result = dbconn.executeQuery(query)) {
//...
//推荐
try (var customers = dbconn.executeQuery(query)) {
//...
}
比较Stream<Customer> result 与var customers

什么使用时候使用var

  • 简单的临时变量
  • 复杂,多步骤逻辑,嵌套的表达式等,简短的变量有助理解代码
  • 能够确定变量初始值
  • 变量类型比较长时
1
2
3
4
5
6
7
8
9
10
11
public void fun1(){
var s1="lisi";
var age = 20;
for(var i=0;i<10;i++){
System.out.println("i = " + i);
}
List<String> strings = Arrays.asList("a", "b", "c");
for (var str: strings){
System.out.println("str = " + str);
}
}

sealed

sealed 翻译为密封,密封类(Sealed Classes)的首次提出是在Java15 的JEP 360 中,并在Java 16 的JEP 397 再次预览,而在Java 17 的JEP 409 成为正式的功能

Sealed Classes 主要特点是限制继承

Sealed Classes 主要特点是限制继承,Java 中通过继承增强,扩展了类的能力,复用某些功能。当这种能力不受控
与原有类的设计相违背,导致不预见的异常逻辑

Sealed Classes 限制无限的扩张

Java 中已有sealed 的设计

  • final 关键字,修饰类不能被继承
  • private 限制私有类

sealed 作为关键字可在class 和interface 上使用,结合permits 关键字。定义限制继承的密封类

Sealed Classes

sealed class 类名permits 子类1,子类N 列表{}

  • 声明sealed Class(permits 表示允许的子类,一个或多个)
1
2
3
4
5
6
7
public sealed class Shape permits Circle, Square, Rectangle {
private Integer width;
private Integer height;
public void draw(){
System.out.println("=======Shape 图形======");
}
}
  • 声明子类
  • 子类声明有三种
    • final 终结,依然是密封的
    • sealed 子类是密封类,需要子类实现
    • non-sealed 非密封类,扩展使用,不受限
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//第一种final
public final class Circle extends Shape {
}
//第二种sealed class
public sealed class Square extends Shape permits RoundSquare {
@Override
public void draw() {
System.out.println("=======Square 图形======");
}
}
//密封类的子类的子类
public final class RoundSquare extends Square{
}
//非密封类, 可以被扩展。放弃密封
public non-sealed class Rectangle extends Shape {
}
//继承非密封类
public class Line extends Rectangle{
}

Sealed Interface

  • 声明密封接口
1
2
3
public sealed interface SomeService permits SomeServiceImpl {
void doThing();
}
  • 实现接口
1
2
3
4
5
public final class SomeServiceImpl implements SomeService {
@Override
public void doThing() {
}
}

以上类和接口要在同一包可访问范围内

SpringBoot

SpringBoot是什么

Spring Boot 是目前流行的微服务框架 倡导 约定优先于配置其设目的是用来简化新Spring 应用的初始化搭建以及开发过程。Spring Boot 提供了很多核心的功能,比如自动化配置starter(启动器) 简化Maven配置、内嵌Servlet 容器、应用监控等功能, 让我们可以快速构建企业级应用程序

特性

  1. 创建独立的Spring 应用程序
  2. 嵌入式Tomcat、Jetty、Undertow 容器(jar)
  3. 提供的starters 简化构建配置(简化依赖管理和版本控制)
  4. 尽可能自动配置spring 应用和第三方库
  5. 提供生产指标,例如指标、健壮检查和外部化配置
  6. 没有代码生成,无需XML 配置

SpringBoot 同时提供“开箱即用”,“约定优于配置”的特性
开箱即用:Spring Boot 应用无需从0 开始,使用脚手架创建项目。基础配置已经完成。集成大部分第三方库对象,无需配置就能使用。例如在Spring Boot 项目中使用MyBatis。可以直接使用XXXMapper 对象, 调用方法执行sql 语句
约定优于配置:Spring Boot 定义了常用类,包的位置和结构,默认的设置。代码不需要做调整,项目能够按照预期运行。比如启动类在根包的路径下,使用了@SpringBooApplication 注解。创建了默认的测试类。controller,service,dao 应该放在根包的子包中。application 为默认的配置文件
脚手架(spring 提供的一个web 应用,帮助开发人员,创建springboot 项目)
SpringBoot3 最小jdk17, 支持17-20

与Spring的关系

Spring Boot 创建的是Spring 应用,对于这点非常重要。也就是使用Spring 框架创建的应用程序。这里的Spring是指Spring Framework

Spring 的核心功能:IoC , AOP , 事务管理,JDBC,SpringMVC , Spring WebFlux,集成第三方框架,MyBatis,Hibernate, Kafka , 消息队列…
Spring 包含SpringMVC, SpringMVC 作为web 开发的强有力框架,是Spring 中的一个模块

首先明确一点,Spring Boot 和Spring Framework 都是创建的Spring 应用程序。Spring Boot 是一个新的框架,看做是Spring 框架的扩展,它消除了设置Spring 应用程序所需的XML 配置,为更快,更高效的创建Spring应用提供了基础平台。Spring Boot 能够快速创建基于Spring ,SpringMVC 的普通应用以及Web 项目

SpringBoot 是包含了Spring 、SpringMVC 的高级的框架,提供了自动功能,短平快。能够更快的创建Spring应用。消除了Spring 的XML 配置文件,提供了开发效率,消除Spring 应用的臃肿。避免了大量的样板代码

Spring Boot 是掌握Spring Cloud 的基础

与SpringCloud关系

Spring Cloud

微服务(Microservices Architecture)是一种架构和组织方法,微服务是指单个小型的但有业务功能的服务,每个服务都有自己的处理和轻量通讯机制,可以部署在单个或多个服务器上

将一个大型应用的功能,依据业务功能类型,抽象出相对独立的功能,称为服务。每个服务就上一个应用程序,有自己的业务功能,通过轻量级的通信机制与其他服务通信(通常是基于HTTP 的RESTful API),协调其他服务完成业务请求的处理。这样的服务是独立的,与其他服务是隔离的, 可以独立部署,运行。与其他服务解耦合

微服务看做是模块化的应用,将一个大型应用,分成多个独立的服务,通过http 或rpc 将多个部分联系起来。请求沿着一定的请求路径,完成服务处理

项目规模大,服务多。要构建大型的分布式应用,保证应用的稳定,高效,不间断的提供服务。Spring Cloud是对分布式项目提供了有力的支持

Spring Cloud 是一系列框架的有序的组合,为开发人员提供了快速构建分布式系统中常用工具(例如,配置管理、服务发现、断路器、智能路由、微代理、控制总线、一次性令牌、全局锁、领导选举、分布式会话、集群状态)。开发人员使用使用Spring Cloud 这套框架和工具的集合可以快速建立实现分布式服务。这些框架需要使用Spring Boot 作为基础开发平台

Spring Cloud 包含的这些框架和工具各负其职,例如Spring Cloud Config提供配置中心的能力,给分布式多个服务提供动态的数据配置,像数据库的url,用户名和密码等,第三方接口数据等。Spring Cloud Gateway 网关,提供服务统一入口,鉴权,路由等功能

Spring Boot3

新特性

2022 年11 月24 日。Spring Boot3 发布,里程碑的重大发布。这个版本应该是未来5 年的使用主力。Spring官网支持Spring Boot3.0.X 版本到2025 年
SpringBoot3 中的重大变化

  1. JDK 最小Java 17,能够支持17-20
  2. Spring Boot 3.0 已将所有底层依赖项从Java EE 迁移到了Jakarta EE API。原来javax 开头的包名,修改为jakarta。例如jakarta.servlet.http.HttpServlet 原来javax.servlet.http.HttpServlet
  3. 支持GraalVM 原生镜像。将Java 应用编译为本机代码,提供显著的内存和启动性能改进
  4. 对第三方库,更新了版本支持
  5. 自动配置文件的修改
  6. 提供新的声明式Http 服务,在接口方法上声明@HttpExchange 获取http 远程访问的能力。代替OpenFeign
  7. Spring HTTP 客户端提供基于Micrometer 的可观察性. 跟踪服务,记录服务运行状态等
  8. AOT(预先编译) 支持Ahead Of Time,指运行前编译
  9. Servlet6.0 规范
  10. 支持Jackson 2.14
  11. Spring MVC :默认情况下使用的PathPatternParser。删除过时的文件和FreeMarker 、JSP 支持

伴随着Spring Boot3 的发布,还有Spring Framework 6.0 的发布(2022-11-16),先于Spring Boot 发布

脚手架

Spring 脚手架

软件工程中的脚手架是用来快速搭建一个小的可用的应用程序的骨架,将开发过程中要用到的工具、环境都配置好,同时生
成必要的模板代码

脚手架辅助创建程序的工具,Spring Initializr 是创建Spring Boot 项目的脚手架。快速建立Spring Boot 项目的最好方式。他是一个web 应用,能够在浏览器中使用。IDEA 中继承了此工具,用来快速创建Spring Boot 项目以及Spring Cloud 项目

代码结构

com.example.模块名称
+—-Application.java 启动类
+—-controller 控制器包
—StudentController.java
—ScoreController.java
+—-service 业务层包
—inter 业务层接口
—impl 接口实现包
+—-repository 持久层包
+—-model 模型包
—entity 实体类包

​ —dto 数据传输包
​ —vo 视图数据包

包和主类

通常建议将主应用程序类定位在其他类之上的根包中。@SpringBootApplication 注释通常放在主类上,它隐式地为某些项定义了一个基本的“搜索包”。例如,如果正在编写一个JPA 应用程序,则使用 @SpringBootApplication 注释类的包来搜索@Entity 项。使用根包还允许组件扫描只应用于自己的项目

Spring Boot 支持基于java 的配置。尽管可以将SpringApplication 与XML 源一起使用,但通常建议主源是单个@Configuration 类。通常,定义主方法的类可以作为主@Configuration 类

spring-boot-starter-parent

pom.xml 中的<parent>指定 spring-boot-starter-parent 作为坐标,表示继承Spring Boot 提供的父项目。从 spring-boot-starter-parent 继承以获得合理的默认值和完整的依赖树,以便快速建立一个Spring Boot 项目

父项目提供以下功能

  • JDK 的基准版本,比如<java.version>17</java.version>
  • 源码使用UTF-8 格式编码
  • 公共依赖的版本
  • 自动化的资源过滤:默认把src/main/resources 目录下的文件进行资源打包
  • maven 的占位符为‘@’
  • 对多个Maven 插件做了默认配置,如maven-compile-plugin,maven-jar-plugin

快速创建Spring Boot 项目,同时能够使用父项目带来的便利性,可以采用如下两种方式

  • 在项目中,继承spring-boot-starter-parent

  • pom.xml 不继承,单独加入spring-boot-dependencies 依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <dependencyManagement>
    <dependencies>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>3.0.1</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    </dependencies>
    </dependencyManagement>

    Spring Boot jar 文件的结构

    项目 spring boot jar 普通的jar
    目录 BOOT-INF : 应用的class 和依赖jar
    META-INF: 清单
    org.springframework.boot.loader:
    spring-boot-loader 模块类
    META-INF:清单
    class 的文件夹:jar 中的所有类
    BOOT-INF class:应用的类
    lib:应用的依赖
    没有BOOT-INF
    spring-boot-loader 执行jar 的spring boot 类 没有此部分
    可执行

starter

starter 是一组依赖描述,应用中包含starter,可以获取spring 相关技术的一站式的依赖和版本。不必复制、粘粘代码。通过starter 能够快速启动并运行项目
starter 包含

  • 依赖坐标、版本
  • 传递依赖的坐标、版本
  • 配置类,配置项

外部化配置

应用程序 = 代码+ 数据(数据库,文件,url)

应用程序的配置文件:Spring Boot 允许在代码之外,提供应用程序运行的数据,以便在不同的环境中使用相同的应用程序代码。避免硬编码,提供系统的灵活性。可使用各种外部配置源,包括Java 属性文件、YAML 文件、环境变量和命令行参数

项目中经常使用properties 与yaml 文件,其次是命令行参数

配置文件基础

配置文件格式

配置文件有两种格式分别:properies 和yaml(yml)

  • properties 是Java 中的常用的一种配置文件格式,key=value。key 是唯一的,文件扩展名为properties
  • yaml(YAML Ain’t Markup Language)也看做是yml,是一种做配置文件的数据格式,基本的语法key:[空格]
    值。yml 文件文件扩展名是yaml 或yml(常用)

yml 格式特点
YAML 基本语法规则

  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进只可以使用空格,不允许使用Tab 键
  • 缩进的空格数目不重要,相同层级的元素左侧对齐即可
  • #字符表示注释,只支持单行注释。#放在注释行的第一个字符
    YAML 缩进必须使用空格,而且区分大小写,建议编写YAML 文件只用小写和空格

YAML 支持三种数据结构

  • 对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)
  • 数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)
  • 标量(scalars):单个的、不可再分的值,例如数字、字符串、true|false 等

Language-Independent Types for YAML™ Version 1.1

application 文件

Spring Boot 同时支持properties 和yml 格式的配置文件。配置文件名称默认是application。我们可以使用application.properties , application.yml

读取配置文件的key 值,注入到Bean 的属性可用@Value,@Value 一次注入一个key 的值。将多个key 值绑定到Bean 的多个属性要用到@ConfigurationProperties 注解。在代码中访问属性除了注解,Spring 提供了外部化配置的抽象对象Environment。Environment 包含了几乎所有外部配置文件,环境变量,命令行参数等的所有key和value。需要使用Environment 的注入此对象,调用它getProperty(String key)方法即可。

Spring Boot 建议使用一种格式的配置文件,如果properties 和yml 都存在。properties 文件优先。推荐使用yml文件
application 配置文件的名称和位置都可以修改。约定名称为application,位置为resources 目录

application.properties/yml

注解@Value 读取单个值,语法${key:默认值}

1
2
@Value("${app.name}")
private String name;

Environment

Environment 是外部化的抽象,是多种数据来源的集合。从中可以读取application 配置文件,环境变量,系统属性

使用方式在Bean 中注入Environment。调用它的getProperty(key)方法

  • 创建ReadConfig 类,注入Environment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class ReadConfig {

@Autowired
private Environment environment;

public void print(){
String name = environment.getProperty("app.name");
//key 是否存在
if (environment.containsProperty("app.owner")) {
System.out.println("有app.owner 配置项");
}
//读取key 转为需要的类型,提供默认值8000
Integer port = environment.getProperty("app.port", Integer.class, 8000);
String result = String.format("读取的name:%s,端口port:%d", name,port);
System.out.println("result = " + result);
}
}
  • 单元测试
1
2
3
4
5
6
7
8
9
10
11
@SpringBootTest
class ReadConfigTest {

@Autowired
private ReadConfig readConfig;

@Test
void test01() {
readConfig.print();
}
}

组织多文件

大型集成的第三方框架,中间件比较多。每个框架的配置细节相对复杂。如果都将配置集中到一个application文件,导致文件内容多,不易于阅读。我们将每个框架独立一个配置文件,最后将多个文件集中到application。我们使用导入文件的功能

需求:项目集成redis,数据库mysql。将redis,数据库单独放到独立的配置文件

  • 在resources 创建自定义conf 目录,在conf 中创建redis.yml, db.yml
  • application.yml 导入多个配置
1
2
3
spring: 
config:
import: conf/db.yml,conf/redis.yml
  • 创建类,读取两个文件的配置项
1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class MultiConfigService {

@Value("${spring.redis.host}")
private String redisHost;

@Value("${spring.datasource.url}")
private String dbUrl;

public void printConfig(){
System.out.println("redis 的ip:"+redisHost+",数据库url;"+dbUrl);
}
}
  • 单元测试
1
2
3
4
5
6
7
8
9
10
11
@SpringBootTest
class MultiConfigServiceTest {

@Autowired
private MultiConfigService multiConfigService;

@Test
void test01() {
multiConfigService.printConfig();
}
}

多环境配置

软件开发中经常提到环境这个概念,与日常生活中的环境概念一样。环境影响居住体验。影响软件运行的也叫做环境,例如应用中访问数据库的ip,用户名和密码,Redis 的端口,配置文件的路径,windows,linux 系统,tomcat 服务器等等。围绕着程序周围的都是环境。环境影响软件的运行

Spring Profiles 表示环境,Profiles 有助于隔离应用程序配置,并使它们仅在某些环境中可用。常说开发环境,测试环境,生产环境等等。一个环境就是一组相关的配置数据, 支撑我们的应用在这些配置下运行。应用启动时指定适合的环境。开发环境下每个开发人员使用自己的数据库ip,用户,redis 端口。同一个程序现在要测试了。需要把数据库ip,redis 的改变为测试服务器上的信息。此时使用多环境能够方便解决这个问题

Spring Boot 规定环境文件的名称application-{profile}.properties(yml)。其中profile 为自定义的环境名称,推荐使用dev 表示开发,test 表示测试。prod 表示生产,feature 表示特性。总是profile 名称是自定义的。Spring Boot会加载application 以及application-{profile}两类文件,不是只单独加载application-{profile}

需求: 项目使用多环境配置,准备一个开发环境,一个测试环境

  • 在resources 创建环境配置文件

  • application-test.yml

1
2
3
4
5
6
myapp: 
memo: 测试环境
spring:
config:
activate:
on-profile: test
  • application-dev.yml
1
2
3
4
5
6
myapp: 
memo: 开发环境
spring:
config:
activate:
on-profile: dev
  • 激活环境
1
2
3
4
5
spring: 
config:
import: conf/db.yml,conf/redis.yml
profiles:
active: dev

绑定Bean

@Value 绑定单个属性,当属性较多时不方便,Spring Boot 提供了另一种属性的方法。将多个配置项绑定到 Bean 的属性,提供强类型的Bean。Bean 能够访问到配置数据

基本原则:标准的Java Bean 有无参数构造方法,包含属性的访问器。配合@ConfigurationProperties 注解一起使用。Bean 的static 属性不支持

Spring Boot 自动配置中大量使用了绑定Bean 与@ConfigurationProperties,提供对框架的定制参数。项目中要使用的数据如果是可变的,推荐在yml 或properties 中提供。项目代码具有较大的灵活性

@ConfigurationProperties 能够配置多个简单类型属性,同时支持Map,List,数组类型。对属性还能验证基本格式

简单的属性绑定
  • 准备配置文件
  • 创建Bean,定义name,owner, port 属性
1
2
3
4
5
6
7
8
@Configuration(proxyBeanMethods = false)
@ConfigurationProperties(prefix = "app")
public class AppBean {
private String name;
private String owner;
private Integer port;
// set | get 方法,toString()
}

@ConfigurationProperties 声明在类上,表示绑定属性到此类。prefix 表示前缀,是配置文件中多个key 的公共前缀。这些key 以“.”作为分隔符。例如app.name, app: name 等。prefix=”app”, 将文件中app 开始的key 都找到,调用与key 相同名称的setXXX 方法。如果有给属性赋值成功。没有的忽略

proxyBeanMethods 参数

proxyBeanMethods 参数控制是否对配置类的 @Bean 方法进行代理

  • proxyBeanMethods = true(默认值):表示启用代理,会使用 CGLIB 代理类生成的 Bean,确保每次调用 @Bean 方法返回的都是同一个实例(同一容器中的单例)
    • 在需要确保方法间调用返回同一 Bean 实例的情况下使用。例如,在多个 @Bean 方法之间有依赖关系时。通过代理机制保证单例模式,防止重复创建 Bean
  • proxyBeanMethods = false:表示不使用代理,这种方式会直接调用 @Bean 方法,而不进行方法拦截和代理。适用于不依赖方法间调用(没有循环依赖)且对性能要求较高的场景
    • 可以提高启动性能,因为不需要创建代理对象,方法间调用也不需要拦截。适合配置类比较简单、没有方法依赖的场景。例如,配置文件主要用于属性绑定(如 @ConfigurationProperties),而不涉及 @Bean 方法之间的相互调用时
嵌套Bean
  • application.yml
1
2
3
4
5
6
7
app:
name: Lession07-yml
owner: bjpowernode
port: 8002
security:
username: root
password: 123456
  • bean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Security {
private String username;
private String password;
// set | get 方法,toString()
}
@Configuration(proxyBeanMethods = false)
@ConfigurationProperties(prefix = "app")
public class NestAppBean {
private String name;
private String owner;
private Integer port;
private Security security;
// set | get 方法,toString()
}
扫描注解

@ConfigurationProperties 注解起作用,还需要@EnableConfigurationProperties@ConfigurationPropertiesScan。这个注解是专门寻找@ConfigurationProperties 注解的,将他的对象注入到Spring 容器。在启动类上使用扫描注解

1
2
3
4
5
6
7
8
9
10
11
/**
* 扫描@ConfigurationProperties 所在的包名
* 不扫描@Component
*/
@ConfigurationPropertiesScan({"top,qianqianzyk.config.pk4"})
@SpringBootApplication
public class Lession07ConfigApplication {
public static void main(String[] args) {
SpringApplication.run(Lession07ConfigApplication.class, args);
}
}
处理第三方库对象

上面的例子都是在源代码中使用@ConfigurationProperties 注解,如果某个类需要在配置文件中提供数据,但是没有源代码。此时@ConfigurationProperties 结合@Bean 一起在方法上面使用

比如现在有一个Security 类是第三方库中的类,现在要提供它的username,password 属性值

1
2
3
4
5
6
7
8
@Configuration
public class ApplicationConfig {
@ConfigurationProperties(prefix = "security")
@Bean
public Security createSecurity(){
return new Security();
}
}
集合Map,List 以及Array
  • 创建保存数据的Bean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class User {
private String name;
private String sex;
private Integer age;
//set | get ,toString
}

public class MyServer {
private String title;
private String ip;
//set | get ,toString
}

@ConfigurationProperties
public class CollectionConfig {
private List<MyServer> servers;
private Map<String,User> users;
private String [] names;
//set | get ,toString
}
  • 修改application.yml, 配置数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#集合以及数组
#List<String> names
names:
- lisi
- zhangsan
#List<MyServer> servers
servers:
- title: 华北服务器
ip: 202.12.39.1
- title: 西南服务器
ip: 106.90.23.229
- title: 南方服务器
ip: 100.21.56.23
#Map<String,User> users
users:
user1:
name: 张三
sex:
age: 22
user2:
name: 李四
sex:
age: 26

“-”表示集合一个成员,因为成员是对象,需要属性名称指定属性值。List 与数组前面加入“-”表示一个成员。Map 直接指定key 和value,无需“-”

  • 启动类增加扫描包
1
2
3
4
5
6
7
8
9
10
11
/**
* 扫描@ConfigurationProperties 所在的包名
* 不扫描@Component
*/
@ConfigurationPropertiesScan({"其他包","top.qianqianzyk.config.pk5"})
@SpringBootApplication
public class Lession07ConfigApplication {
public static void main(String[] args) {
SpringApplication.run(Lession07ConfigApplication.class, args);
}
}
  • 单元测试
1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootTest
class CollectionConfigTest {

@Autowired
private CollectionConfig collectionConfig;

@Test
void test01() {
String str = collectionConfig.toString();
System.out.println("str = " + str);
}
}
指定数据源文件

application 做配置是经常使用的,除以以外我们能够指定某个文件作为数据来源。@PropertySource 是注解,用以加载指定的properties 文件。也可以是XML 文件。@PropertySource 与@Configuration 一同使用,其他注解还有@Value,@ConfigurationProperties

需求:一个组织信息,在单独的properties 文件提供组织的名称,管理者和成员数量

  • 创建Group 类,表示组织
1
2
3
4
5
6
7
8
9
@Configuration
@ConfigurationProperties(prefix = "group")
@PropertySource(value = "classpath:/group-info.properties")
public class Group {
private String name;
private String leader;
private Integer members;
//set | get ,toString
}
  • 单元测试

创建对象三种方式

将对象注入到Spring 容器,可以通过如下方式

  • 传统的XML 配置文件
  • Java Config 技术, @Configuration 与@Bean
  • 创建对象的注解,@Controller ,@Service , @Repository ,@Component

Spring Boot 不建议使用xml 文件的方式, 自动配置已经解决了大部分xml 中的工作了。如果需要xml 提供bean的声明,@ImportResource 加载xml 注册Bean

需求:XML 配置Spring 容器。声明Bean

  • 创建Person 类,对象由容器管理
1
2
3
4
5
public class Person {
private String name;
private Integer age;
//set | get ,toString
}
  • resources 目录下创建XML 配置文件(applicationContext.xml)
1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="myPerson" class="top.qianqianzyk.config.pk7.Person">
<property name="name" value="李四" />
<property name="age" value="20" />
</bean>
</beans>
  • 启动类,从容器中获取Person 对象
1
2
3
4
5
6
7
8
9
10
@ImportResource(locations = "classpath:/applicationContext.xml")
@ConfigurationPropertiesScan({"top.qianqianzyk.config.pk4","top.qianqianzyk.config.pk5"})
@SpringBootApplication
public class Lession07ConfigApplication {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(Lession07ConfigApplication.class, args);
Person myPerson = (Person) ctx.getBean("myPerson");
System.out.println("myPerson = " + myPerson);
}
}

**@ImportResource(locations = “classpath:/applicationContext.xml”)**, 加载类路径下的applicationContext.xml 文件
location 或者value 属性都能指定文件路径

AOP

AOP(Aspect Oriented Programming):面向切面编程,保持原有代码不变,能够给原有的业务逻辑增加二维的功能。AOP 增加的功能是开发人员自己编写的,底层是动态代理实现功能的增强。对于扩展功能十分有利。Spring的事务功能就是在AOP 基础上实现的, 业务方法在执行前【开启事务】,在执行业务方法,最后【提交或回滚失败】

  • Aspect:表示切面,开发自己编写功能增强代码的地方,这些代码会通过动态代理加入到原有的业务方法中
    @Aspect 注解表示当前类是切面类。切面类是一个普通类
  • Joinpoint:表示连接点,连接切面和目标对象。或是一个方法名称,一个包名,类名。在这个特定的位置执
    行切面中的功能代码
  • 切入点(Pointcut):其实就是筛选出的连接点。一个类中的所有方法都可以是JoinPoint, 具体的那个方法要
    增加功能,这个方法就是Pointcut
  • Advice:翻译是通知,也叫做增强。表示增强的功能执行时间。Java 代码执行的单位是方法,方法是业务
    逻辑代码,在方法之前增加新的功能,还是方法之后增加功能。表示在方法前,后等的就是通知

主要包括5 个注解:@Before,@After,@AfterReturning,@AfterThrowing,@Around。注解来自aspectj 框架
@Before:在切点方法之前执行
@After:在切点方法之后执行
@AfterReturning:切点方法返回后执行
@AfterThrowing:切点方法抛异常执行
@Around:属于环绕增强,能控制切点执行前,执行后。功能最强的注解

AOP 技术主要的实现一个是Spring 框架,SpringBoot 也支持AOP;另一个是功能全面的AspectJ 框架。SpringBoot 执行AspectJ框架。使用@Before,@After,@AfterReturning,@AfterThrowing,@Around 注解的方式就来自AspectJ 框架的功能

需求:项目中的业务方法都需要在日志中输出方法调用的时间以及参数明细。业务方法多,使用AOP 最合适。新建Spring Boot 项目Lession08-aop , Maven 管理项目,无需选择依赖。包名称

  • 添加AOP依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  • 创建业务类SomeService 在aop 的service 子包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.bjpowernode.aop.service;

public interface SomeService {
void query(Integer id);
void save(String name, String code);
}

实现类:
import org.springframework.stereotype.Service;

@Service
public class SomeServiceImpl implements SomeService {

@Override
public void query(Integer id) {
System.out.println("SomeService 业务方法query 执行了");
}

@Override
public void save(String name, String code) {
System.out.println("SomeService 业务方法save 执行了");
}
}
  • 创建切面类
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
// @Component注解用于将当前类作为Bean注入到Spring容器中,便于在其他类中自动注入使用
@Component
// @Aspect注解表示该类是一个切面类,用于定义横切关注点(如日志、事务),包含切面功能代码和通知
@Aspect
public class LogAspect {

// @Before注解表示这是一个前置通知,在目标方法执行前运行
// "execution(* com.bjpowernode.aop.service..*.*(..))" 是切入点表达式,表示所有com.bjpowernode.aop.service包及其子包中所有类的所有方法
@Before("execution(* com.bjpowernode.aop.service..*.*(..))")
public void sysLog(JoinPoint jp) {
// 使用StringJoiner构造日志信息字符串,用竖线分隔,日志信息放在大括号{}中
StringJoiner log = new StringJoiner("|", "{", "}");

// 获取当前时间并格式化为"yyyy-MM-dd HH:mm:ss"格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
log.add(formatter.format(LocalDateTime.now())); // 添加当前时间到日志信息中

// 获取目标方法的参数列表,并将每个参数的字符串表示追加到日志中
Object[] args = jp.getArgs();
for (Object arg : args) {
// 如果参数为null,用"-"表示;否则调用toString方法将参数添加到日志中
log.add(arg == null ? "-" : arg.toString());
}

// 输出日志信息到控制台
System.out.println("方法执行日志:" + log.toString());
}
}

  • 单元测试
1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootTest
public class AspectTest {

@Autowired
private SomeService service;

@Test
void testLog() {
service.query(1001);
service.save("订单业务", "B01256");
}
}

输出执行结果:
方法执行日志:{2023-01-22 13:52:43|1001}
SomeService 业务方法query 执行了
方法执行日志:{2023-01-22 13:52:43|订单业务|B01256}
SomeService 业务方法save 执行了

自动配置

启用autoconfigure(自动配置),框架尝试猜测和Bean 要使用的Bean,从类路径中查找xxx.jar,创建这个jar中某些需要的Bean。例如我们使用MyBatis 访问数据, 从我们项目的类路径中寻找mybatis.jar, 进一步创建SqlSessionFactory, 还需要DataSource 数据源对象,尝试连接数据。这些工作交给XXXAutoConfiguration 类,这些就是自动配置类。在spring-boot-autoconfigure-3.0.2.jar 定义了很多的XXXAutoConfiguration 类。第三方框架的starter 里面包含了自己XXXAutoConfiguration

自动配置的注解@EnableAutoConfiguration(通常由@SpringBootApplication 注解带入)所在的包,具有特殊的含义,是Spring Boot 中的默认包,默认包是扫描包的起点(根包)。@Controller ,@Service, @Repository ,@Component,@Configuration 放在根包以及子包中就会被扫描到

@Import

@EnableAutoConfiguration 源码上面的@Import(AutoConfigurationImportSelector.class)

1
2
3
4
5
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
....
}

@Import:导入类,注册为Bean。@Import 相当于xml 文件中的<import>。可以导入@Configuration 的类,实现了ImportSelector 接口的类,ImportBeanDefinitionRegistrar 接口的类

ImportSelector 接口在Spring Boot 使用的比较多

1
2
3
4
5
6
public interface ImportSelector {
//AnnotationMetadata 被@Import 注释的类注解信息
//导入配置类全限定名称
String[] selectImports(AnnotationMetadata importingClassMetadata);
...
}

AutoConfigurationImportSelector

AutoConfigurationImportSelector 间接实现了ImportSelector 接口,导入自动配置类

自动配置从jar 的指定文件读取要加载的配置类列表

位置:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

Spring Boot 之前的版本(2.7)读取spring.factories 文件。保留spring.factories 为了向后兼容

@AutoConfiguration 注解

新的注解@AutoConfiguration,用在自动配置类的上面。相当于增强的@Configuration,专注自动配置类。@AutoConfiguration 还支持通过after、afterNames、before 和benameames 属性进行自动配置排序,决定多个自动配置类执行先后顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
// @AutoConfiguration指定该类为自动配置类,自动装配的顺序在DataSourceAutoConfiguration之后,TransactionAutoConfiguration之前
@AutoConfiguration(after = DataSourceAutoConfiguration.class, before = TransactionAutoConfiguration.class)

// @ConditionalOnClass注解用于指定条件:当类路径中存在LocalContainerEntityManagerFactoryBean、EntityManager和SessionImplementor类时,才会加载当前配置类
@ConditionalOnClass({ LocalContainerEntityManagerFactoryBean.class, EntityManager.class, SessionImplementor.class })

// @EnableConfigurationProperties注解启用JpaProperties配置类,允许自动绑定相关JPA配置属性到JpaProperties类中
@EnableConfigurationProperties(JpaProperties.class)

// @Import注解导入HibernateJpaConfiguration类,将其加入到Spring容器中,使Hibernate的JPA配置生效
@Import(HibernateJpaConfiguration.class)
public class HibernateJpaAutoConfiguration {
}

访问数据库

Spring Boot 框架为SQL 数据库提供了广泛的支持,既有用JdbcTemplate 直接访问JDBC,同时支持“object relational mapping”技术(如Hibernate,MyBatis)。Spring Data 独立的项目提供对多种关系型和非关系型数据库的访问支持。比如MySQL, Oracle , MongoDB , Redis, R2DBC,Apache Solr,Elasticsearch…

Spring Boot 也支持嵌入式数据库比如H2, HSQL, and Derby。这些数据库只需要提供jar 包就能在内存中维护
数据

DataSource

通常项目中使用MySQL,Oracle,PostgreSQL 等大型关系数据库。Java 中的jdbc 技术支持了多种关系型数据库的访问。在代码中访问数据库,我们需要知道数据库程序所在的ip,端口,访问数据库的用户名和密码以及数据库的类型信息。以上信息用来初始化数据源,数据源也就是DataSource。数据源表示数据的来源,从某个ip 上的数据库能够获取数据。javax.sql.DataSource 接口表示数据源,提供了标准的方法获取与数据库绑定的连接对象(Connection)

javax.sql.Connection 是连接对象,在Connection 上能够从程序代码发送查询命令,更新数据的语句给数据库;同时从Connection 获取命令的执行结果。Connection 很重要,像一个电话线把应用程序和数据库连接起来

DataSource 在application 配置文件中以spring.datasource.*作为配置项。类似下面的代码:

1
2
3
spring.datasource.url=jdbc:mysql://localhost/mydb
spring.datasource.username=dbuser
spring.datasource.password=dbpass

DataSourceProperties.java 是数据源的配置类

1
2
3
4
5
// @ConfigurationProperties注解用于绑定配置文件中的属性到该类的字段上
// "prefix = 'spring.datasource'"表示会绑定配置文件中以"spring.datasource"开头的属性
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
}

Spring Boot 能够从spring.datasource.url 推断所使用的数据驱动类,如果需要特殊指定请设置spring.datasource.driver-class-name 为驱动类的全限定名称

Spring Boot 支持多种数据库连接池,优先使用HikariCP,其次是Tomcat pooling,再次是Commons DBCP2,如果以上都没有,最后会使用Oracle UCP 连接池。当项目中starter 依赖了spring-boot-starter-jdbc 或者spring-boot-starter-data-jpa 默认添加HikariCP 连接池依赖,也就是默认使用HikariCP 连接池

轻量的JdbcTemplate

使用JdbcTemplate 我们提供自定义SQL, Spring 执行这些SQL 得到记录结果集。JdbcTemplate 和 NamedParameterJdbcTemplate 类是自动配置的,可以@Autowire 注入到自己的Bean 中。开箱即用

JdbcTemplate 执行完整的SQL 语句,我们将SQL 语句拼接好,交给JdbcTemplate 执行,JdbcTemplate 底层就是使用JDBC 执行SQL 语句。是JDBC 的封装类而已

NamedParameterJdbcTemplate 可以在SQL 语句部分使用“:命名参数”作为占位符, 对参数命名,可读性更好NamedParameterJdbcTemplate 包装了JdbcTemplate 对象,“:命名参数”解析后,交给JdbcTemplate 执行SQL
语句

JdbcTemplateAutoConfiguration 自动配置了JdbcTemplate 对象,交给JdbcTemplateConfiguration 创建了JdbcTemplate 对象。并对JdbcTemplate 做了简单的初始设置(QueryTimeout,maxRows 等)

准备环境

访问数据库先准备数据库的script。SpringBoot 能够自动执行DDL,DML 脚本。两个脚本文件名称默认是schema.sql 和data.sql。脚本文件在类路径中自动加载
自动执行脚本还涉及到spring.sql.init.mode 配置项:

  • always:总是执行数据库初始化脚本
  • never:禁用数据库初始化

Spring Boot 处理特定的数据库类型,为特定的数据库定制script 文件。首先设置spring.sql.init.platform=hsqldb、h2、oracle、mysql、postgresql 等等,其次准备schema-${platform}. sql 、data-${platform}. sql 脚本文件

准备数据库和表脚本

数据库名称Blog , 表目前使用一个article(文章表),初始两条数据

  • schema.sql
1
2
3
4
5
6
7
8
9
CREATE TABLE `article` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_id` int(11) NOT NULL COMMENT '作者ID',
`title` varchar(100) NOT NULL COMMENT '文章标题',
`summary` varchar(200) DEFAULT NULL COMMENT '文章概要',
`read_count` int(11) unsigned zerofill NOT NULL COMMENT '阅读读数',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '最后修改时间',
PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
  • data.sql
1
2
3
4
INSERT INTO `article` VALUES ('1','2101','SpringBoot 核心注解',
'核心注解的主要作用','00000008976','2023-01-16 12:11:12','2023-01-16 12:11:19');
INSERT INTO `article` VALUES ('2','356752','JVM 调优',
'HotSpot 虚拟机详解','00000000026','2023-01-16 12:15:27','2023-01-16 12:15:30');
创建Spring Boot 工程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
JdbcTemplate 访问MySQL

项目中依赖了spring-jdbc 6.0.3,JdbcTemplate 对象会自动创建好。把JdbcTemplate 对象注入给你的Bean,再调用JdbcTemplate 的方法执行查询,更新,删除的SQL

JdbcTemplate 上手快,功能非常强大。提供了丰富、实用的方法,归纳起来主要有以下几种类型的方法:

  • execute 方法:可以用于执行任何SQL 语句,常用来执行DDL 语句
  • update、batchUpdate 方法:用于执行新增、修改与删除等语句
  • query 和queryForXXX 方法:用于执行查询相关的语句
  • call 方法:用于执行数据库存储过程和函数相关的语句

将schema.sql , data.sql 拷贝到resources 目录

修改application.properties

1
2
3
4
5
6
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/blog?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
#总是执行数据库脚本,以后设置为never
spring.sql.init.mode=always

创建实体类ArticlePO

1
2
3
4
5
6
7
8
9
10
11
12
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ArticlePO {
private Integer id;
private Integer userId;
private String title;
private String summary;
private Integer readCount;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}

单元测试,注入JdbcTemplate 对象

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
@SpringBootTest
public class TestJdbcTemplate {

@Resource
private JdbcTemplate jdbcTemplate;

// 测试聚合函数
@Test
void testCount() {
String sql="select count(*) as ct from article";
Long count = jdbcTemplate.queryForObject(sql, Long.class);
System.out.println("文章总数= " + count);
}

// 测试 “?”占位符
@Test
void testQuery() {
// ?作为占位符
String sql = "select * from article where id= ? ";
//BeanPropertyRowMapper 将查询结果集,列名与属性名称匹配, 名称完全匹配或驼峰
ArticlePO article = jdbcTemplate.queryForObject(sql,
new BeanPropertyRowMapper<>(ArticlePO.class), 1 );
System.out.println("查询到的文章= " + article);
}

// 测试自定义RowMapper
@Test
void testQueryRowMapper() {
//只能查询出一个记录,查询不出记录抛出异常
String sql = "select * from article where id= " + 1;
ArticlePO article = jdbcTemplate.queryForObject(sql, (rs, rownum) -> {
var id = rs.getInt("id");
var userId = rs.getInt("user_id");
var title = rs.getString("title");
var summary = rs.getString("summary");
var readCount = rs.getInt("read_count");
var createTime = new Timestamp(rs.getTimestamp("create_time").getTime())
.toLocalDateTime();
var updateTime = new Timestamp(rs.getTimestamp("update_time").getTime())
.toLocalDateTime();
return new ArticlePO(id, userId, title, summary, readCount,
createTime, updateTime);
});
System.out.println("查询的文章= " + article);
}

// 测试List集合
@Test
void testList() {
String sql="select * from article order by id ";
List<Map<String, Object>> listMap = jdbcTemplate.queryForList(sql);
listMap.forEach( el->{
el.forEach( (field,value)->{
System.out.println("字段名称:"+field+",列值:"+value);
});
System.out.println("===================================");
});
}

// 测试更新记录
@Test
void testUpdate() {
String sql="update article set title = ? where id= ? ";
//参数是从左往右第一个,第二个...
int updated = jdbcTemplate.update(sql, "Java 核心技术思想", 2);
System.out.println("更新记录:"+updated);
}
}
NamedParameterJdbcTemplate

NamedParameterJdbcTemplate 能够接受命名的参数,通过具名的参数提供代码的可读性,JdbcTemplate 使用的是参数索引的方式
在使用模板的位置注入NamedParameterJdbcTemplate 对象,编写SQL 语句,在SQL 中WHERE 部分“:命名参数”。调用NamedParameterJdbcTemplate 的诸如query,queryForObject, execute,update 等时,将参数封装到Map 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SpringBootTest
public class TestNamedParameterJdbcTemplate {

@Resource
private JdbcTemplate jdbcTemplate;

@Test
void testNameQuery() {
// :参数名
String sql="select count(*) as ct from article where user_id=:uid and
read_count > :num";
//key 是命名参数
Map<String,Object> param = new HashMap<>();
param.put("uid", 2101);
param.put("num", 0);
Long count = nameJdbcTemplate.queryForObject(sql, param, Long.class);
System.out.println("用户被阅读的文章数量= " + count);
}
}
多表查询

多表查询关注是查询结果如何映射为Java Object。常用两种方案:一种是将查询结果转为Map。列名是key,列值是value,这种方式比较通用,适合查询任何表。第二种是根据查询结果中包含的列,创建相对的实体类。属性和查询结果的列对应。将查询结果自定义RowMapper、ResultSetExtractor 映射为实体类对象

现在创建新的表article_detail,存储文章内容,与article 表是一对一关系

1
2
3
4
5
6
CREATE TABLE `article_detail` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '注解',
`article_id` int(11) NOT NULL COMMENT '文章ID',
`content` text NOT NULL COMMENT '文章内容',
PRIMARY KEY (`id`))
ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

需求:查询某个文章的全部属性,包括文章内容
创建新的实体类ArticleMainPO, 将ArticlePO 作为成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ArticleMainPO {
private Integer id;
private Integer userId;
private String title;
private String summary;
private Integer readCount;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private ArticleDetailPO articleDetail;
}

查询一对一文章

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
@Test
void testArticleContent() {
String sql= """
select m.*,d.id as detail_id, d.article_id,d.content
from article m join article_detail d
on m.id = d.article_id
where m.id=:id
""";
Map<String,Object> param = new HashMap<>();
param.put("id", 1);
List<ArticleMainPO> list = nameJdbcTemplate.query(sql, param, (rs, rowNum) -> {
var id = rs.getInt("id");
var userId = rs.getInt("user_id");
var title = rs.getString("title");
var summary = rs.getString("summary");
var readCount = rs.getInt("read_count");
var createTime = new Timestamp(rs.getTimestamp("create_time").getTime())
.toLocalDateTime();
var updateTime = new Timestamp(rs.getTimestamp("update_time").getTime())
.toLocalDateTime();
//文章详情
var detailId = rs.getInt("detail_id");
var content = rs.getString("content");
var articleId = rs.getInt("article_id");
ArticleDetailPO detail = new ArticleDetailPO();
detail.setId(detailId);
detail.setArticleId(articleId);
detail.setContent(content);
return new ArticleMainPO(id, userId, title, summary, readCount,
createTime, updateTime, detail);
});
list.forEach(m -> {
System.out.println("m.getId() = " + m.getId());
System.out.println("m.getArticleDetail() = " + m.getArticleDetail());
});
}
总结

JdbcTemplate 的优点简单,灵活,上手快,访问多种数据库。对数据的处理控制能力比较强,RowMapper, ResultSetExtractor 能够提供按需要灵活定制记录集与实体类的关系

缺点:对SQL 要求高,适合对SQL 比较了解,自定义查询结果比较多,调优需求的
JdbcTemplate 对象的调整参数,比较少。可设置spring.jdbc.template.开头的配置项目,比如设置超时为10 秒,spring.jdbc.template.query-timeout=10

MyBatis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
单表
  • 创建实体类
1
2
3
4
5
6
7
8
9
10
11
//PO:Persistent Object
@Data
public class ArticlePO {
private Integer id;
private Integer userId;
private String title;
private String summary;
private Integer readCount;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
  • 创建Mapper 接口
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
public interface ArticleMapper {
String
field_list="id,user_id,title,summary,read_count,create_time,update_time";
@Insert("""
insert into
article(user_id,title,summary,read_count,create_time,update_time) \
values(#{userId},#{title},#{summary},#{readCount},#{createTime},#{updateTime})
""")
int insertArticle(ArticlePO article);

@Update("""
update article set read_count=#{num} where id=#{id}
""")
int updateReadCount(Integer id,Integer num);

@Delete("""
delete from article where id=#{id}
""")
int deleteById(Integer id);

@Select("select " + field_list + " from article where id=#{articleId}")
@Results({
@Result(id = true,column = "id",property = "id"),
@Result(column = "user_id",property = "userId"),
@Result(column = "read_count",property = "readCount"),
@Result(column = "create_time",property = "createTime"),
@Result(column = "update_time",property = "updateTime"),
})
ArticlePO selectById(@Param("articleId") Integer id);
}

@Results 部分为结果映射(XML 中的<ResultMap>), 或者用MyBatis 的驼峰命名也能实现默认的映射关系
application.properties
mybatis.configuration.map-underscore-to-camel-case=true

  • 启动类加入扫描注解
1
2
3
4
5
6
7
@MapperScan({"com.bjpowernode.orm.repository"})
@SpringBootApplication
public class Lession10MyBatisApplication {
public static void main(String[] args) {
SpringApplication.run(Lession10MyBatisApplication.class, args);
}
}

@MapperScan 是扫描注解,参数是Mapper 接口所在的包名。参数是数组,可以指定多个包位置

  • 配置数据源
1
2
3
4
5
6
7
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/blog?serverTimezone=Asia/Shangh
ai&useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
#驼峰,下划线命名
mybatis.configuration.map-underscore-to-camel-case=true
  • 单元测试
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
@SpringBootTest
class Lession10MyBatisApplicationTests {
@Autowired
private ArticleMapper articleMapper;
@Test
void testInsert() {
ArticlePO article = new ArticlePO();
article.setTitle("什么时候用微服务");
article.setSummary("微服务优缺点");
article.setUserId(219);
article.setReadCount(560);
article.setCreateTime(LocalDateTime.now());
article.setUpdateTime(LocalDateTime.now());
articleMapper.insertArticle(article);
}
@Test
void testUpdate() {
int rows = articleMapper.updateReadCount(1, 230);
System.out.println("修改的记录数量:" + rows);
}
@Test
void testDelete(){
int rows = articleMapper.deleteById(11);
System.out.println("删除记录的数量" + rows);
}
@Test
void testSelect(){
ArticlePO articlePO = articleMapper.selectById(3);
System.out.println("查询到的文章:" + articlePO);
}
}
ResultMap

查询操作得到包含多个列的集合,将列值转为对象属性使用结果映射的功能,注解@Results,@ResultMap能够帮助我们完成此功能。

  • @Results 用于定义结果映射,每个列和Java 对象属性的一一对应
  • @ResultMap 指定使用哪个结果映射,两种方式可以使用@Results,另一种XML 文件

创建新的Mapper 对象

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
public interface ArticleDao {
//定义mapper, id 表示唯一名称
@Select("")
@Results(id = "BaseMapper", value = {
@Result(id = true, column = "id", property = "id"),
@Result(column = "user_id", property = "userId"),
@Result(column = "read_count", property = "readCount"),
@Result(column = "create_time", property = "createTime"),
@Result(column = "update_time", property = "updateTime"),
})
ArticlePO articleMapper();

@Select("""
select id,user_id,title,summary,read_count,create_time,update_time
from article where user_id=${userId}
""")
@ResultMap("BaseMapper")
List<ArticlePO> selectList(Integer userId);

@Select("""
select id,user_id,title,summary,read_count,create_time,update_time
from article where id=#{articleId}
""")
@ResultMap("BaseMapper")
ArticlePO selectById(@Param("articleId") Integer id);
}

另一种方法在xml 中定义<resultMap>标签,在@ResultMap 注解引用。这种方式首先创建xml。在resources 目录下创建自定义的mapper 目录

新建ArticleMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bjpowernode.orm.repository.ArticleDao">
<resultMap id="ArticleMapper" type="com.bjpowernode.orm.po.ArticlePO">
<id column="id" property="id"/>
<result column="user_id" property="userId" />
<result column="read_count" property="readCount" />
<result column="create_time" property="createTime" />
<result column="update_time" property="updateTime" />
</resultMap>
</mapper>

修改application.properties 配置mapper 文件的路径
mybatis.mapper-locations:自定义mapper xml 文件保存路径

1
2
3
4
5
6
7
8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/blog?serverTimezone=Asia/Shangh
ai&useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
#驼峰命名
#mybatis.configuration.map-underscore-to-camel-case=true
mybatis.mapper-locations=classpath:/mappers/**/*.xml

修改ArticleDao 的查询方法上面的@ResultMap

1
2
3
4
5
6
7
@Select("""
select id,user_id,title,summary,read_count,create_time,update_time
from article where id=#{articleId}
""")
//@ResultMap("BaseMapper")
@ResultMap("ArticleMapper")
ArticlePO selectById(@Param("articleId") Integer id);
SQL 提供者

我们能在方法上面直接编写SQL 语句。使用Text Block 编写长的语句。方法上编写SQL 显的不够简洁。MyBatis 提供了SQL 提供者的功能。将SQL 以方法的形式定义在单独的类中。Mapper 接口通过引用SQL 提供者中的方法名称,表示要执行的SQL
SQL 提供者有四类@SelectProvider,@InsertProvider,@UpdateProvider,@DeleteProvider
SQL 提供者首先创建提供者类,自定义的。类中声明静态方法,方法体是SQL 语句并返回SQL。例如

1
2
3
public static String selectById() {
return "SELECT * FROM users WHERE id = #{id}";
}

其次Mapper 接口的方法上面,应用@SelectProvider(type = 提供者类.class, method = “方法名称”)

  • 创建SQL 提供者
1
2
3
4
5
6
7
8
9
10
11
12
13
public class SqlProvider {
public static String selectArticle(){
return """
select id,user_id,title,summary,read_count,create_time,update_time
from article where id=#{articleId}
""";
}
public static String updateTime(){
return """
update article set update_time=#{newTime} where id=#{id}
""";
}
}
  • 创建mapper 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface ArticleRepository {
@Select("")
@Results(id = "BaseMapper", value = {
@Result(id = true, column = "id", property = "id"),
@Result(column = "user_id", property = "userId"),
@Result(column = "read_count", property = "readCount"),
@Result(column = "create_time", property = "createTime"),
@Result(column = "update_time", property = "updateTime"),
})
ArticlePO articleMapper();
//查询
@ResultMap("BaseMapper")
@SelectProvider(type = SqlProvider.class,method = "selectArticle")
ArticlePO selectById(Integer id);
//更新
@UpdateProvider(type = SqlProvider.class,method = "updateTime")
int updateTime(Integer id, LocalDateTime newTime);
}
@One 一对一查询

MyBatis 支持一对一,一对多,多对多查询。XML 文件和注解都能实现关系的操作。我们使用注解表示article和article_detail 的一对一关系。MyBatis 维护这个关系, 开发人员自己也可以维护这种关系

  • @One: 一对一
  • @Many: 一对多

创建两个表的实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
public class Article {
private Integer id;
private Integer userId;
private String title;
private String summary;
private Integer readCount;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private ArticleDetail articleDetail;
}
@Data
public class ArticleDetail {
private Integer id;
private Integer articleId;
private String content;
}

创建Mapper 查询接口

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
public interface ArticleOneToOneMapper {
@Select("""
select id,article_id,content from article_detail
where article_id = #{articleId}
""")
@Results({
@Result(id = true, column = "id", property = "id"),
@Result(column = "article_id", property = "articleId"),
@Result(column = "content", property = "content")
})
ArticleDetail queryContent(Integer articleId);

@Select("""
select id,
user_id,
title,
summary,
read_count,
create_time,
update_time
from article
where id = #{id}
""")
@Results({
@Result(id = true, column = "id", property = "id"),
@Result(column = "user_id", property = "userId"),
@Result(column = "read_count", property = "readCount"),
@Result(column = "create_time", property = "createTime"),
@Result(column = "update_time", property = "updateTime"),
@Result(column = "id", property = "articleDetail",
one = @One(select =
"com.bjpowernode.orm.repository.ArticleOneToOneMapper.queryContent",
fetchType = FetchType.LAZY))
})
Article queryAllArticle(Integer id);
}
@Many 一对多查询
  • 创建CommentPO 实体
1
2
3
4
5
6
@Data
public class CommentPO {
private Integer id;
private Integer articleId;
private String content;
}
  • 创建新的文章聚合实体
1
2
3
4
5
6
7
8
9
10
11
@Data
public class ArticleEntity {
private Integer id;
private Integer userId;
private String title;
private String summary;
private Integer readCount;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private List<CommentPO> comments; //评论的集合
}
  • 新建Mapper 接口
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
public interface ArticleOneToManyMapper {
@Select("""
select id,article_id,content from comment
where article_id = #{articleId}
""")
@Results(id="CommentMapper",value = {
@Result(id = true, column = "id", property = "id"),
@Result(column = "article_id", property = "articleId"),
@Result(column = "content", property = "content")
})
List<CommentPO> queryComments(Integer articleId);

@Select("""
select id, user_id,title,summary,
read_count,create_time,update_time
from article
where id = #{id}
""")
@Results(id="ArticleBaseMapper",value={
@Result(id = true, column = "id", property = "id"),
@Result(column = "user_id", property = "userId"),
@Result(column = "read_count", property = "readCount"),
@Result(column = "create_time", property = "createTime"),
@Result(column = "update_time", property = "updateTime"),
@Result(column = "id", property = "comments",
many = @Many(select =
"com.bjpowernode.orm.repository.ArticleOneToManyMapper.queryComments", fetchType =
FetchType.LAZY))
})
ArticleEntity queryArticleAndComments(Integer id);
}
常用配置参数
1
2
3
4
5
6
7
8
9
10
#驼峰命名
mybatis.configuration.map-underscore-to-camel-case=true
#mapper xml 文件位置
mybatis.mapper-locations=classpath:/mappers/**/*.xml
#启用缓存
mybatis.configuration.cache-enabled=true
#延迟加载
mybatis.configuration.lazy-loading-enabled=true
#mybatis 主配置文件,按需使用
mybatis.config-location=classpath:/sql-config.xml

sql-config.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<typeAliases>
<package name="com.bjpowernode.po"/>
</typeAliases>
</configuration>
MybatisAutoConfiguration

MyBatis 框架的在Spring Boot 的自动配置类MybatisAutoConfiguration.class

imports 文件中定义了org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration 自动配置类

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties({MybatisProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class,
MybatisLanguageDriverAutoConfiguration.class})
public class MybatisAutoConfiguration implements InitializingBean {
private static final Logger logger =
LoggerFactory.getLogger(MybatisAutoConfiguration.class);
private final MybatisProperties properties;
.....
}

关注:
MybatisProperties.class
DataSourceAutoConfiguration.class , DataSourceProperties.class

SqlSessionFactory.class
SqlSessionTemplate.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
{
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
....
}
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
return executorType != null ? new SqlSessionTemplate(sqlSessionFactory,
executorType) : new SqlSessionTemplate(sqlSessionFactory);
}

SqlSessionTemplate 是线程安全的,MyBatis 为了与Spring 继承。提供的由Spring 管理的Bean。这个SqlSesionTemplate 实现了SqlSession 接口, 能够由Spring 事务管理器使用。提供Spring 的事务处理。同时管理SqlSession 的创建,销毁

适合的连接池

HikariCP 连接池

连接池配置

MySQL 连接池配置建议

1
2
3
4
5
6
7
8
prepStmtCacheSize
这将设置MySQL 驱动程序将缓存每个连接的预准备语句数。默认值为保守的25。我们建议将其设置为250-500 之间。
prepStmtCacheSqlLimit
这是驱动程序将缓存的准备好的SQL 语句的最大长度。MySQL 默认值为256。根据我们的经验,特别是对于像Hibernate 这
样的ORM 框架,这个默认值远低于生成的语句长度的阈值。我们推荐的设置为2048。
cachePrepStmts
如果缓存实际上被禁用,则上述参数都没有任何影响,因为默认情况下是禁用的。必须将此参数设置为。true
useServerPrepStmts:较新版本的MySQL 支持服务器端准备语句,这可以提供实质性的性能提升。将此属性设置为。true

application.yml

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
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/blog?serverTimezone=Asia/Shanghai
username: root
password: 123456
hikari:
auto-commit: true
# # connections = ((cpu 核心数* 2) + 磁盘数量) 近似值。默认10
maximum-pool-size: 10
#最小连接数,默认10,不建议设置。默认与maximum-pool-size 一样大小。推荐使用固定大小的连接池
minimum-idle: 10
#获取连接时,检测语句
connection-test-query: select 1
###
# 连接超时,默认30 秒。
# 控制客户端在获取池中Connection 的等待时间,
# 如果没有连接可用的情况下超过该时间,则抛出SQLException 异常,
###
connection-timeout: 20000
#其他属性
data-source-properties:
cachePrepStmts: true
dataSource.cachePrepStmtst: true
dataSource.prepStmtCacheSize: 250
dataSource.prepStmtCacheSqlLimit: 2048
dataSource.useServerPrepStmts: true

声明式事务

事务分为全局事务与本地事务,本地事务是特定于资源的,例如与JDBC 连接关联的事务。本地事务可能更容易使用,但有一个显著的缺点:它们不能跨多个事务资源工作。比如在方法中处理连接多个数据库的事务,本地事务是无效的

Spring 解决了全局和本地事务的缺点。它允许应用程序开发人员在任何环境中使用一致的编程模型。只需编写一次代码,就可以从不同环境中的不同事务管理策略中获益。Spring 框架同时提供声明式和编程式事务管理。推荐声明式事务管理

Spring 事务抽象的关键是事务策略的概念,org.springframework.transaction.PlatformTransactionManager 接口
定义了事务的策略
事务控制的属性:

  • Propagation : 传播行为。代码可以继续在现有事务中运行(常见情况),也可以暂停现有事务并创建新事务
  • Isolation: 隔离级别。此事务与其他事务的工作隔离的程度。例如,这个事务能看到其他事务未提交的写吗
  • Timeout:超时时间:该事务在超时和被底层事务基础结构自动回滚之前运行的时间
  • Read-only 只读状态:当代码读取但不修改数据时,可以使用只读事务。在某些情况下,例如使用Hibernate
    时,只读事务可能是一种有用的优化

AOP:Spring Framework 的声明式事务管理是通过Spring 面向切面编程(AOP)实现的。事务方面的代码以样板的方式使用,即使不了解AOP 概念,仍然可以有效地使用这些代码。事务使用AOP 的环绕通知(TransactionInterceptor)

声明式事务的方式:

  • XML 配置文件:全局配置
  • @Transactional 注解驱动:和代码一起提供,比较直观。和代码的耦合比较高。【Spring 团队建议您只使用
    @Transactional 注释具体类(以及具体类的方法),而不是注释接口。当然,可以将@Transactional 注解放在接
    口(或接口方法)上,但这只有在使用基于接口的代理时才能正常工作】

方法的可见性:

公共(public)方法应用@Transactional 注解。如果使用@Transactional 注释了受保护的、私有的或包可见的方法,则不会引发错误,但注释的方法不会显示配置的事务设置,事务不生效。如果需要受保护的、私有的方法具有事务考虑使用AspectJ。而不是基于代理的机制

环境准备
  • 创建实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
public class ArticlePO {
private Integer id;
private Integer userId;
private String title;
private String summary;
private Integer readCount;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
//PO:Persistent Object
@Data
public class ArticleDetailPO {
private Integer id;
private Integer articleId;
private String content;
}
  • 创建Mapper 接口,创建两个方法,添加文章属性,文章内容\
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface ArticleMapper {
@Insert("""
insert into
article(user_id,title,summary,read_count,create_time,update_time) \
values(#{userId},#{title},#{summary},#{readCount},#{createTime},#{updateTime})
""")
@Options(useGeneratedKeys = true,keyColumn = "id",keyProperty = "id")
int insertArticle(ArticlePO article);

@Insert("""
insert into article_detail(article_id,content)
values(#{articleId},#{content})
""")
int insertArticleContent(ArticleDetailPO detail);
}
  • 创建Service 接口,声明发布文章的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface ArticleService {
boolean postNewArticle(ArticlePO article,String content);
}
@Service
public class ArticleServiceImpl implements ArticleService {
@Autowired
private ArticleMapper articleMapper;
@Override
public boolean postNewArticle(ArticlePO article, String content) {
//新增文章
articleMapper.insertArticle(article);
//新增文章内容
ArticleDetailPO detail = new ArticleDetailPO();
detail.setArticleId(article.getId());
detail.setContent(content);
articleMapper.insertArticleContent(detail);
return true;
}
}
  • 启动类
1
2
3
4
5
6
7
@MapperScan(basePackages = "com.bjpowernode.trans.repository")
@SpringBootApplication
public class Lession11TransApplication {
public static void main(String[] args) {
SpringApplication.run(Lession11TransApplication.class, args);
}
}
  • 编写配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/blog?serverTimezone=Asia/Shanghai
username: root
password: 123456
hikari:
auto-commit: true
# # connections = ((cpu 核心数* 2) + 磁盘数量) 近似值。默认10
maximum-pool-size: 10
#获取连接时,检测语句
connection-test-query: select 1
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  • 单元测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootTest
class Lession11TransApplicationTests {
@Autowired
private ArticleService articleService;
@Test
void testAddArticle() {
ArticlePO article = new ArticlePO();
article.setTitle("Spring 事务管理");
article.setSummary("Spring 事务属性,事务实现");
article.setUserId(2001);
article.setReadCount(0);
article.setCreateTime(LocalDateTime.now());
article.setUpdateTime(LocalDateTime.now());
boolean add = articleService.postNewArticle(article, "Spring 统一事务管理。事务管理器管理本地事务");
System.out.println("add = " + add);
}
}
添加事务注解
  • 修改postNewArticle()方法添加@Transactional
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Transactional
@Override
public boolean postNewArticle(ArticlePO article, String content) {
//新增文章
articleMapper.insertArticle(article);
if( article.getReadCount() < 1) {
throw new RuntimeException("已读数量不能< 1 ");
}
//新增文章内容
ArticleDetailPO detail = new ArticleDetailPO();
detail.setArticleId(article.getId());
detail.setContent(content);
articleMapper.insertArticleContent(detail);
return true;
}
  • 启动类
1
2
3
4
5
6
7
8
@EnableTransactionManagement
@MapperScan(basePackages = "com.bjpowernode.trans.repository")
@SpringBootApplication
public class Lession11TransApplication {
public static void main(String[] args) {
SpringApplication.run(Lession11TransApplication.class, args);
}
}
  • 单元测试
1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testAddArticle() {
ArticlePO article = new ArticlePO();
article.setTitle("Spring 事务管理111");
article.setSummary("Spring 事务属性,事务实现111");
article.setUserId(2202);
article.setReadCount(0);
article.setCreateTime(LocalDateTime.now());
article.setUpdateTime(LocalDateTime.now());
boolean add = articleService.postNewArticle(article, "Spring 统一事务管理。事务管理器管理本地事务111");
System.out.println("add = " + add);
}

添加数据失败, 在事务中抛出运行时异常。Spring 默认回滚事务

无效事务1

Spring 事务处理是AOP 的环绕通知,只有通过代理对象调用具有事务的方法才能生效。类中有A 方法,调用带有事务的B 方法。调用A 方法事务无效。当然protected, private 方法默认是没有事务功能的

  • 接口中增加方法managerArticles
1
2
3
4
5
6
7
//接口中增加方法
boolean managerArticle(String action,ArticlePO article,String content);
//实现类方法:
@Override
public boolean managerArticle(String action, ArticlePO article, String content) {
return postNewArticle(article,content);
}
  • 单元测试,readCount 为0
1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testAddArticle2() {
ArticlePO article = new ArticlePO();
article.setTitle("Spring 事务管理222");
article.setSummary("Spring 事务属性,事务实现222");
article.setUserId(2202);
article.setReadCount(0);
article.setCreateTime(LocalDateTime.now());
article.setUpdateTime(LocalDateTime.now());
boolean add = articleService.managerArticle("add",article, "222 Spring 统一事务管理。事务管理器管理本地事务");
System.out.println("add = " + add);
}

测试发现,事务不起作用。aritcleService 是代理对象,managerArticle 方法不是事务方法。事务无效

无效事务2

方法在线程中运行的,在同一线程中方法具有事务功能, 新的线程中的代码事务无效

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
@Transactional
@Override
public boolean postNewArticle(ArticlePO article, String content) {
System.out.println("Start 父线程:" + Thread.currentThread().threadId());
Thread thread = new Thread(() -> {
System.out.println("子线程:" + Thread.currentThread().threadId());
//新增文章
articleMapper.insertArticle(article);
if (article.getReadCount() < 1) {
throw new RuntimeException("===已读数量不能< 1 ");
}
//新增文章内容
ArticleDetailPO detail = new ArticleDetailPO();
detail.setArticleId(article.getId());
detail.setContent(content);
articleMapper.insertArticleContent(detail);
});
//线程启动
thread.start();
try{
//等他thread 执行完成,在继续后面的代码
thread.join();
}catch (Exception e){
e.printStackTrace();
}
System.out.println("End 父线程:" + Thread.currentThread().threadId());
return true;
}
事务回滚规则
  • RuntimeException 的实例或子类时回滚事务
  • Error 会导致回滚
  • 已检查异常不会回滚。默认提交事务

@Transactional 注解的属性控制回滚

  • rollbackFor:指定需要回滚事务的异常类型。可以包含多个异常类型,标记的异常类型及其子类都将触发回滚 @Transactional(rollbackFor = {IOException.class, SQLException.class})
  • noRollbackFor:指定不需要回滚的异常类型。包含的异常类型及其子类发生时,不会触发回滚,而是提交事务
  • rollbackForClassName:指定异常的类名字符串来控制回滚。适用于通过类名称控制回滚的情况 @Transactional(rollbackForClassName = "com.example.MyCheckedException")
  • noRollbackForClassName:同上

Web 服务

基于浏览器的B/S 结构应用十分流行。Spring Boot 非常适合Web 应用开发。可以使用嵌入式Tomcat、Jetty、Undertow 或Netty 创建一个自包含的HTTP 服务器。一个Spring Boot 的Web 应用能够自己独立运行,不依赖需要安装的Tomcat,Jetty 等

Spring Boot 可以创建两种类型的Web 应用

  • 基于Servlet 体系的Spring Web MVC 应用
  • 使用spring-boot-starter-webflux 模块来构建响应式,非阻塞的Web 应用程序

Spring MVC 是“model view controller”的框架。专注web 应用开发

简单示例

html 页面视图
  • 依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 视图技术Thymeleaf 模板引擎-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
  • 创建Controller
1
2
3
4
5
6
7
8
9
10
@Controller
public class QuickController {
@RequestMapping("/exam/quick")
public String quick(Model model){
//业务处理结果数据,放入到Model 模型
model.addAttribute("title", "Web 开发");
model.addAttribute("time", LocalDateTime.now());
return "quick";
}
}
  • 创建视图
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>视图</title>
</head>
<body>
<!--格式化,排列数据,视图在浏览器显示给用户-->
<h3>显示请求处理结果</h3>
<p th:text="${title}"></p>
<p th:text="${time}"></p>
</body>
</html>
JSON 视图
  • 创建Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Data
public class User {
private String name;
private Integer age;
}
@Controller
public class JSONViewController {
//HttpServletResponse
@RequestMapping("/exam/json")
public void exam1(HttpServletResponse response) throws IOException {
String data="{\"name\":\"lisi\",\"age\":20}";
response.getWriter().println(data);
}
//@ResponseBody
@RequestMapping("/exam/json2")
@ResponseBody public User exam2() {
User user = new User();
user.setName("张三");
user.setAge(22);
return user;
}
}

构建前-后端分离项目经常采用这种方式

给项目加favicon

首先找一个在线工具创建favicon.ico。比如https://quanxin.org/favicon , 用文字,图片生成我们需要的内容。生成的logo 文件名称是favicon.ico

  • 将生成的favicon.ico 拷贝项目的resources/ 或resources/static/ 目录
  • 在你的视图文件,加入对favicon.ico 的引用(在视图的<head>部分加入<link rel="icon" href="../favicon.ico" type="image/x-icon"/>

注意:

  1. 关闭缓存,浏览器清理缓存
  2. 如果项目有过滤器,拦截器需要放行对favicon.ico 的访问

Spring MVC

Spring MVC 是非常著名的Web 应用框架,现在的大多数Web 项目都采用Spring MVC。它与Spring 有着紧密的关系。是Spring 框架中的模块,专注Web 应用,能够使用Spring 提供的强大功能,IoC , Aop 等等

Spring MVC 框架是底层是基于Servlet 技术。遵循Servlet 规范,Web 组件Servlet,Filter,Listener 在SpringMVC中都能使用。同时Spring MVC 也是基于MVC 架构模式的,职责分离,每个组件只负责自己的功能,组件解耦

Spring Boot 的自动配置、按约定编程极大简化,提高了Web 应用的开发效率

控制器Controller

控制器一种由Spring 管理的Bean 对象,赋予角色是“控制器”。作用是处理请求,接收浏览器发送过来的参数,将数据和视图应答给浏览器或者客户端app 等

控制器是一个普通的Bean,使用@Controller 或者@RestController 注释。@Controller 被声明为@Component

匹配请求路径到控制器方法

SpringMVC 支持多种策略,匹配请求路径到控制器方法。AntPathMatcher 、PathPatternParser

1
spring.mvc.pathmatch.matching-strategy=path_pattern_parser

从SpringBoot3 推荐使用PathPatternParser 策略。比之前AntPathMatcher 提示6-8 倍吞吐

PathPatternParser 中有关uri 的定义

通配符:

  • ? : 一个字符
  • *: 0 或多个字符。在一个路径段中匹配字符
  • **:匹配0 个或多个路径段,相当于是所有

正则表达式: 支持正则表达式

举例

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
@GetMapping("/file/t?st.html")
// ?匹配只有一个字符
// GET http://localhost:8080/file/test.html
@GetMapping("/file/t?st.html")
public String path1(HttpServletRequest request){
return "path 请求="+request.getRequestURI();
}

@GetMapping ("/images/*.gif")
// *:匹配一个路径段中的0 或多个字符
// GET http://localhost:8080/images/user.gif
// GET http://localhost:8080/images/cat.gif
// GET http://localhost:8080/images/.gif
@GetMapping ("/images/*.gif")
public String path2(HttpServletRequest request){
return "* 请求="+request.getRequestURI();
}

@GetMapping ("/pic/**")
// ** 匹配0 或多段路径, 匹配/pic 开始的所有请求
// GET http://localhost:8080/pic/p1.gif
// GET http://localhost:8080/pic/20222/p1.gif
// GET http://localhost:8080/pic/user
// GET http://localhost:8080/pic/
@GetMapping ("/pic/**")
public String path3(HttpServletRequest request){
return "/pic/**请求="+request.getRequestURI();
}

// RESTFul
@GetMapping("/order/{*id}")
// {*id} 匹配/order 开始的所有请求, id 表示order 后面直到路径末尾的所有内容。
// id 自定义路径变量名称。与@PathVariable 一样使用
// GET http://localhost:8080/order/1001
// GET http://localhost:8080/order/1001/2023-02-16
@GetMapping("/order/{*id}")
public String path4(@PathVariable("id") String orderId, HttpServletRequest request){
return "/order/{*id}请求="+request.getRequestURI() + ",id="+orderId;
}
// 注意:@GetMapping("/order/{*id}/{*date}")无效的, {*..}后面不能在有匹配规则了

@GetMapping("/pages/{fname:\\w+}.log")
// :\\w+ 正则匹配, xxx.log
// GET http://localhost:8080/pages/req.log
// GET http://localhost:8080/pages/111.log
@GetMapping("/pages/{fname:\\w+}.log")
public String path5(@PathVariable("fname") String filename, HttpServletRequest
request){
return "/pages/{fname:\\w}.log 请求="+request.getRequestURI() + ",
filename="+filename;
}
@RequestMapping

@RequestMapping:用于将web 请求映射到控制器类的方法。此方法处理请求。可用在类上或方法上。在类和方法同时组合使用

重要的属性:

  • value:别名path 表示请求的uri, 在类和方法方法同时使用value,方法上的继承类上的value 值
  • method:请求方式,支持GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE
    值为:RequestMethod[] method() , RequestMethod 是enum 类型

快捷注解:

  • @GetMapping: 表示get 请求方式的@RequestMapping
  • @PostMapping:表示post 请求方式的@RequestMapping
  • @PutMapping:表示put 请求方式的@RequestMapping
  • @DeleteMapping: 表示delete 请求方式的@RequestMapping

对于请求方式get,post,put,delete 等能够HttpMethod 表示,Spring Boot3 之前他是enum,Spring Boot3是class

控制器方法参数类型与可用返回值类型

参数类型Method Arguments :: Spring Framework

类型 作用
WebRequest, NativeWebRequest 访问请求参数,作用同ServletRequest,
jakarta.servlet.ServletRequest,
jakarta.servlet.ServletResponse
Servlet API 中的请求,应答
jakarta.servlet.http.HttpSession Servlet API 的会话
jakarta.servlet.http.PushBuilder Servlet 4.0 规范中推送对象
HttpMethod 请求方式
java.io.InputStream, java.io.Reader IO 中输入,读取request body
java.io.OutputStream, java.io.Writer IO 中输入,访问response body
@PathVariable,@MatrixVariable,@RequestParam,
@RequestHeader,@CookieValue,@RequestBody,
@RequestPart,@ModelAttribute
uri 模板中的变量,访问矩阵,访问单个请求参数,访问
请求header,访问cookie,读取请求body, 文件上传,
访问model 中属性
Errors, BindingResult 错误和绑定结果
java.util.Map,
org.springframework.ui.Model,
org.springframework.ui.ModelMap
存储数据Map,Model,ModelMap
其他参数 String name, Integer , 自定义对象

返回值Return Values :: Spring Framework

返回值类型 作用
@ResponseBody 将response body 属性序列化输出
HttpEntity, ResponseEntity 包含http 状态码和数据的实体
String 实体名称或字符串数据
自定义对象 如果有json 库,序列化为json 字符串
ErrorResponse 错误对象
ProblemDetail RFC7807,规范的错误应答对象
void 无返回值
ModelAndView 数据和视图
java.util.Map,
org.springframework.ui.Model
作为模型的数据
接收请求参数

用户在浏览器中点击按钮时,会发送一个请求给服务器,其中包含让服务器程序需要做什么的参数。这些参数发送给控制器方法。控制器方法的形参列表接受请求参数

接受参数方式:

  • 请求参数与形参一一对应,适用简单类型。形参可以有合适的数据类型,比如String,Integer ,int 等
  • 对象类型,控制器方法形参是对象,请求的多个参数名与属性名相对应
  • @RequestParam 注解,将查询参数,form 表单数据解析到方法参数,解析multipart 文件上传
  • @RequestBody,接受前端传递的json 格式参数
  • HttpServletRequest 使用request 对象接受参数, request.getParameter(“…”)
  • @RequestHeader ,从请求header 中获取某项值

解析参数需要的值,SpringMVC 中专门有个接口来干这个事情,这个接口就是:HandlerMethodArgumentResolver,
中文称呼:处理器方法参数解析器,说白了就是解析请求得到Controller 方法的参数的值

接收json

  • 创建控制器类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Data
public class User {
private String name;
private Integer age;
}
@RestController
public class ParamController {
@PostMapping("/param/json")
public String getJsonData(@RequestBody User user){
System.out.println("接收的json:"+user.toString());
return "json 转为User 对象"+user.toString();
}


// 接收json array
@PostMapping("/param/jsonarray")
public String getJsonDataArray(@RequestBody List<User> users){
System.out.println("接收的json array:"+users.toString());
return "json 转为List<User>对象"+users.toString();
}
}

Reader-InputStream

1
2
3
4
5
6
7
8
9
10
11
12
13
@PostMapping("/param/json2")
public String getJsonData2(Reader in) {
StringBuffer content = new StringBuffer("");
try(BufferedReader bin = new BufferedReader(in)){
String line = null;
while( (line=bin.readLine()) != null){
content.append(line);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return "读取请求体数据"+ content.toString();
}

数组参数接收多个值

数组作为形参,接受多个参数值,请求格式参数名=值1&参数名=值2…

1
2
3
4
5
6
7
8
@GetMapping("/param/vals")
public String getMultiVal(Integer [] id){
List<Integer> idList = Arrays.stream(id).toList();
return idList.toString();
}
// 测试请求:
// GET http://localhost:8080/param/vals?id=11&id=12&id=13
// GET http://localhost:8080/param/vals?id=11,12,13,14,15
验证参数

服务器端程序,Controller 在方法接受了参数,这些参数是由用户提供的,使用之前必须校验参数是我们需要的吗,值是否在允许的范围内,是否符合业务的要求。比如年龄不能是负数,姓名不能是空字符串,email 必须有@符号,phone 国内的11 位才可以

验证参数

  • 编写代码,手工验证,主要是if 语句,switch 等等。

  • Java Bean Validation : JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation, 是一个运行时的数据

    验证规范,为JavaBean 验证定义了相应的元数据模型和API

Java Bean Validation

Spring Boot 使用Java Bean Validation 验证域模型属性值是否符合预期,如果验证失败,立即返回错误信息

Java Bean Validation 将验证规则从controller,service 集中到Bean 对象。一个地方控制所有的验证

Bean 的属性上,加入JSR-303 的注解,实现验证规则的定义。JSR-3-3 是规范,hibernate-validator 是实现

JSR-303: https://beanvalidation.org/
hibernate-validator:https://hibernate.org/validator/
https://docs.jboss.org/hibernate/validator/4.2/reference/en-US/html/

JSR-303 注解

注解 作用
@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(value) 被注释的元素必须符合指定的正则表达式
@Email 被注释的元素必须是电子邮箱地址
@NotEmpty 被注释的字符串的必须非空

Hibernate 提供的部分注解

注解 作用
@URL 被注释的字符为URL
@Length 被注释的字符串的大小必须在指定的范围内
@Range 被注释的元素必须在合适的范围内

简单示例

  • 添加Bean Validator Starter
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  • 创建文章数据类,添加约束注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Data
public class ArticleVO {
//文章主键
private Integer id;
@NotNull(message = "必须有作者")
private Integer userId;
//同一个属性可以指定多个注解
@NotBlank(message = "文章必须有标题")
//@Size 中null 认为是有效值.所以需要@NotBlank
@Size(min = 3,max = 30,message = "标题必须3 个字以上")
private String title;
@NotBlank(message = "文章必须副标题")
@Size(min = 8,max = 60,message = "副标题必须30 个字以上")
private String summary;
@DecimalMin(value = "0",message = "已读最小是0")
private Integer readCount;
@Email(message = "邮箱格式不正确")
private String email;
}
  • Controller 使用Bean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class ArticleController {
@PostMapping("/article/add")
public Map<String,Object> addArticle(@Validated @RequestBody ArticleVO articleVo,
BindingResult br){
Map<String,Object> map = new HashMap<>();
if( br.hasErrors() ){
br.getFieldErrors().forEach( field->{
map.put(field.getField(), field.getDefaultMessage());
});
}
return map;
}
}

@Validated: Spring 中的注解,支持JSR 303 规范,还能对group 验证。可以类,方法,参数上使用BindingResult 绑定对象,错误Validator 的绑定

分组校验

上面的AriticleVO 用来新增文章, 新的文章主键id 是系统生成的。现在要修改文章,比如修改某个文章的title,summary, readCount,email 等。此时id 必须有值,修改这个id 的文章。新增和修改操作对id 有不同的
要求约束要求。通过group 来区分是否验证

group 是Class 作为表示, java 中包加类一定是唯一的, 这个标识没有其他实际意义

  • 添加group 标志
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
@Data
public class ArticleVO {
//新增组
public static interface AddArticleGroup { };
//编辑修改组
public static interface EditArticleGroup { };
//文章主键
@NotNull(message = "文章ID 不能为空", groups = { EditArticleGroup.class } )
@Min(value = 1, message = "文章ID 从1 开始",
groups = { EditArticleGroup.class } )
private Integer id;
@NotNull(message = "必须有作者",
groups = {AddArticleGroup.class, EditArticleGroup.class})
private Integer userId;
//同一个属性可以指定多个注解
@NotBlank(message = "文章必须有标题",
groups = {AddArticleGroup.class, EditArticleGroup.class})
//@Size 中null 认为是有效值.所以需要@NotBlank
@Size(min = 3, max = 30, message = "标题必须3 个字以上",
groups = {AddArticleGroup.class,EditArticleGroup.class})
private String title;
@NotBlank(message = "文章必须副标题",
groups = {AddArticleGroup.class, EditArticleGroup.class})
@Size(min = 8, max = 60, message = "副标题必须30 个字以上",
groups = {AddArticleGroup.class,EditArticleGroup.class})
private String summary;
@DecimalMin(value = "0", message = "已读最小是0",
groups = {AddArticleGroup.class,EditArticleGroup.class})
private Integer readCount;
//可为null,有值必须符合邮箱要求
@Email(message = "邮箱格式不正确",
groups = {AddArticleGroup.class, EditArticleGroup.class})
private String email;
}
  • 修改Controller,不同方法增加group 标志
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
@RestController
public class ArticleController {
//新增文章
@PostMapping("/article/add")
public Map<String,Object> addArticle(@Validated(ArticleVO.AddArticleGroup.class)
@RequestBody ArticleVO articleVo,
BindingResult br){
Map<String,Object> map = new HashMap<>();
if( br.hasErrors() ){
br.getFieldErrors().forEach( field->{
map.put(field.getField(), field.getDefaultMessage());
});
}
return map;
}
//修改文章
@PostMapping("/article/edit")
public Map<String,Object>
editArticle(@Validated(ArticleVO.EditArticleGroup.class) @RequestBody ArticleVO
articleVo,
BindingResult br){
Map<String,Object> map = new HashMap<>();
if( br.hasErrors() ){
br.getFieldErrors().forEach( field->{
map.put(field.getField(), field.getDefaultMessage());
});
}
return map;
}
}

ValidationAutoConfiguration

spring-boot-starter-validation 引入了jakarta.validation:jakarta.validation-api:3.0.2 约束接口,org.hibernate.validator:hibernate-validator:8.0.0.Final 约束注解的功能实现

ValidationAutoConfiguration 自动配置类,创建了LocalValidatorFactoryBean 对象,当有class 路径中有hibernate.validator。能够创建hiberate 的约束验证实现对象

@ConditionalOnResource(resources = “classpath:META-INF/services/jakarta.validation.spi.ValidationProvider”)

模型Model

在许多实际项目需求中,后台要从控制层直接返回前端所需的数据,这时Model 大家族就派上用场了

Model 模型的意思,Spring MVC 中的“M”,用来传输数据。从控制层直接返回数据给前端,配置jsp,模板技术能够展现M 中存储的数据

Model 简单理解就是给前端浏览器的数据,放在Model 中,ModelAndView 里的任意值,还有json 格式的字符串等都是Model

视图View

Spring MVC 中的View(视图)用于展示数据的,视图技术的使用是可插拔的。无论决定使用thymleaf、jsp 还是其他技术,classpath 有jar 就能使用视图了。开发者主要就是配置更改.Spring Boot3 不推荐FreeMarker、jsp 这些了。页面的视图技术Thymeleaf , Groovy Templates
org.springframework.web.servlet.View 视图的接口,实现此接口的都是视图类,视图类作为Bean 被Spring 管理。当然这些不需要开发者编写代码

  • ThymeleafView:使用thymeleaf 视图技术时的,视图类
  • InternalResourceView:表示jsp 的视图类

控制器方法返回值和视图有是关系的

  • String:如果项目中有thymeleaf , 这个String 表示xxx.html 视图文件(/resources 目录)
  • ModelAndView: View 中就是表示视图

@ResponeBody , @RestController 适合前后端分离项目

  • String : 表示一个字符串数据
  • Object:如果有Jackson 库,将Objet 转为json 字符串

常用的返回值

  • String
  • 自定义Object
  • ResponseEntity
页面视图
  • 创建Controller ,控制器方法返回ModelAndView
1
2
3
4
5
6
7
8
9
10
11
@Controller
public class ReturnController {
@GetMapping("/hello")
public ModelAndView hello(Model model) {
ModelAndView mv = new ModelAndView();
mv.addObject("name","李四");
mv.addObject("age",20);
mv.setViewName("hello");
return mv;
}
}
  • 创建视图: 在resources/templates 创建hello.html
1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>视图文件</h3>
姓名:<div th:text="${name}"></div> <br/>
年龄:<div th:text="${age}"></div> <br/>
</body>
</html>
  • application.properties 默认thymeleaf 的设置
1
2
3
4
#前缀视图文件存放位置
spring.thymeleaf.prefix=classpath:/templates/
#后缀视图文件扩展名
spring.thymeleaf.suffix=.html
JSON视图
  • 增加控制器方法
1
2
3
4
5
6
7
@GetMapping("/show/json")
@ResponseBody public User getUser(){
User user = new User();
user.setName("李四");
user.setAge(20);
return user;
}
复杂JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
public class Role {
//角色ID
private Integer id;
//角色名称
private String roleName;
//角色说明
private String memo;
}
@Data
public class User {
private String name;
private Integer age;
private Role role;
}
1
2
3
4
5
6
7
8
9
10
11
12
@GetMapping("/show/json2")
@ResponseBody public User getUserRole(){
User user = new User();
user.setName("李四");
user.setAge(20);
Role role = new Role();
role.setId(5892);
role.setRoleName("操作员");
role.setMemo("基本操作,读取数据,不能修改");
user.setRole(role);
return user;
}
ResponseEntity

ResponseEntity 包含HttpStatus Code 和应答数据的结合体

  • ResponseEntity 做控制器方法返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@GetMapping("/show/json3")
ResponseEntity<User> getUserInfo(){
User user = new User();
user.setName("李四");
user.setAge(20);
Role role = new Role();
role.setId(5892);
role.setRoleName("操作员");
role.setMemo("基本操作,读取数据,不能修改");
user.setRole(role);
ResponseEntity<User> response = new ResponseEntity<>(user, HttpStatus.OK);
return response;
}

// 其他创建ResponseEntity 的方式
// ResponseEntity<User> response = new ResponseEntity<>(user, HttpStatus.OK);
// 状态码:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status/204
// 200 状态码
ResponseEntity<User> response = ResponseEntity.ok(user);
// HTTP 204 No Content 成功状态响应码,表示该请求已经成功了
ResponseEntity<User> response = ResponseEntity.noContent().build();
Map 作为返回值
1
2
3
4
5
6
7
8
@GetMapping("/map/json")
@ResponseBody public Map getMap(){
Map<String,Object> map = new HashMap<>();
map.put("id",1001);
map.put("address","大兴区");
map.put("city","北京");
return map;
}

SpringMVC 请求流程

Spring MVC 框架是基于Servlet 技术的。以请求为驱动,围绕Servlet 设计的。Spring MVC 处理用户请求与访问一个Servlet 是类似的,请求发送给Servlet,执行doService 方法,最后响应结果给浏览器完成一次请求处理

DispatcherServlet

DispatcherServlet 是核心对象,称为中央调度器(前端控制器Front Controller)。负责接收所有对Controller的请求,调用开发者的Controller 处理业务逻辑,将Controller 方法的返回值经过视图处理响应给浏览器

DispatcherServlet 作为SpringMVC 中的C,职责:

  1. 是一个门面,接收请求,控制请求的处理过程。所有请求都必须有DispatcherServlet 控制。SpringMVC
    对外的入口。可以看做门面设计模式
  2. 访问其他的控制器。这些控制器处理业务逻辑
  3. 创建合适的视图,将2 中得到业务结果放到视图,响应给用户
  4. 解耦了其他组件,所有组件只与DispatcherServlet 交互。彼此之间没有关联
  5. 实现ApplictionContextAware, 每个DispatcherServlet 都拥自己的WebApplicationContext,它继承了
    ApplicationContext。WebApplicationContext 包含了Web 相关的Bean 对象,比如开发人员注释@Controller的类,视图解析器,视图对象等等。DispatcherServlet 访问容器中Bean 对象
  6. Servlet + Spring IoC 组合

Spring MVC 的完整请求流程

  1. 红色DispatherServlet 是框架创建的核心对象(可配置它的属性contextPath)

  2. 蓝色的部分框架已经提供多个对象。开发人员可自定义,替换默认的对象

  3. 绿色的部分是开发人员自己创建的对象,控制器Conroller 和视图对象

流程说明:

  1. DispatcherServlet 接收到客户端发送的请求。判断是普通请求,上传文件的请求
  2. DispatcherServlet 收到请求调用HandlerMapping 处理器映射器
  3. HandleMapping 根据请求URI 找到对应的控制器以及拦截器,组装成HandlerExecutionChain 读写。将此对象返回给DispatcherServlet,做下一步处理
  4. DispatcherServlet 调用HanderAdapter 处理器适配器。这里是适配器设计模式,进行接口转换,将对一个接口调用转换为其他方法
  5. HandlerAdapter 根据执行控制器方法,也就是开发人员写的Controller 类中的方法,并返回一个ModeAndView
  6. HandlerAdapter 返回ModeAndView 给DispatcherServlet
  7. DispatcherServlet 调用HandlerExceptionResolver 处理异常,有异常返回包含异常的ModelAndView
  8. DispatcherServlet 调用ViewResolver 视图解析器来来解析ModeAndView
  9. ViewResolver 解析ModeAndView 并返回真正的View 给DispatcherServlet
  10. DispatcherServlet 将得到的视图进行渲染,填充Model 中数据到request 域
  11. 返回给客户端响应结果

SpringMVC 自动配置

Spring MVC 自动配置会创建很多对象,重点的有:

  • ContentNegotiatingViewResolver 和BeanNameViewResolver bean
  • 支持提供静态资源,包括对WebJars 的支持
  • 自动注册Converter、GenericConverter 和Formatter bean
  • 对HttpMessageConverters 的支持
  • 自动注册MessageCodesResolver
  • 静态index.html 支持
  • 自动使用ConfigurableWebBindingInitializer bean

WebMvcAutoConfiguration 是Spring MVC 自动配置类

1
2
3
4
5
6
7
8
9
10
11
12
@AutoConfiguration(after = { DispatcherServletAutoConfiguration.class,
TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class,
WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@ImportRuntimeHints(WebResourcesRuntimeHints.class)
public class WebMvcAutoConfiguration {
//.....
}
  • DispatcherServletAutoConfiguration.class 自动配置DispatcherServlet
  • WebMvcConfigurationSupport.class 配置SpringMVC 的组件
  • ValidationAutoConfiguration.class: 配置JSR-303 验证器
  • @ConditionalOnWebApplication(type = Type.SERVLET) :应用是基于SERVET 的web 应用时有效
  • @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }):当项目有Servlet.class,
    DispatcherServlet.lcass 时起作用
DispatcherServletAutoConfiguration.class

web.xml 在SpringMVC 以xml 文件配置DispatcherServlet,现在有自动配置完成

1
2
3
4
5
6
7
8
9
10
11
12
13
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/dispatcher.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

DispatcherServletAutoConfiguration 自动配置DispatcherServlet。作用:

1 创建DispatcherServlet:@Bean 创建DispatcherServlet 对象,容器中的名称为dispatcherServlet。作为Servlet 的url-pattern 为“/”

1
2
3
4
5
6
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
....
return dispatcherServlet;
}

2 将DispatchServlet 注册成bean,放到Spring 容器,设置load-on-startup = -1 。

3 创建MultipartResolver,用于上传文件

4 他的配置类WebMvcProperties.class, 前缀spring.mvc

1
2
@ConfigurationProperties(prefix = "spring.mvc")
public class WebMvcProperties { }
WebMvcConfigurationSupport

Spring MVC 组件的配置类,Java Config 方式创建HandlerMappings 接口的多个对象,HandlerAdapters 接口多个对象, HandlerExceptionResolver 相关多个对象,PathMatchConfigurer, ContentNegotiationManager,
OptionalValidatorFactoryBean, HttpMessageConverters 等这些实例
HandlerMappings:
RequestMappingHandlerMapping
HandlerAdapter:
RequestMappingHandlerAdapter
HandlerExceptionResolver:
DefaultHandlerExceptionResolver,ExceptionHandlerExceptionResolver(处理@ExceptionHandler 注解)

通过以上自动配置, SpringMVC 处理需要的DispatcherServlet 对象,HandlerMappings,HandlerAdapters,
HandlerExceptionResolver,以及无视图的HttpMessageConverters 对象

ServletWebServerFactoryAutoConfiguration

ServletWebServerFactoryAutoConfiguration 配置嵌入式Web 服务器

  • EmbeddedTomcat
  • EmbeddedJetty
  • EmbeddedUndertow

Spring Boot 检测classpath 上存在的类,从而判断当前应用使用的是Tomcat/Jetty/Undertow 中的哪一个Servlet Web
服务器,从而决定定义相应的工厂组件。也就是Web 服务器

配置类:ServerProperties.class ,配置web server 服务器

1
2
3
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
}

application 文件配置服务器,现在使用tomcat 服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#服务器端口号
server.port=8001
#上下文访问路径
server.servlet.context-path=/api
#request,response 字符编码
server.servlet.encoding.charset=utf-8
#强制request,response 设置charset 字符编码
server.servlet.encoding.force=true
#日志路径
server.tomcat.accesslog.directory=D:/logs
#启用访问日志
server.tomcat.accesslog.enabled=true
#日志文件名前缀
server.tomcat.accesslog.prefix=access_log
#日志文件日期时间
server.tomcat.accesslog.file-date-format=.yyyy-MM-dd
#日志文件名称后缀
server.tomcat.accesslog.suffix=.log
#post 请求内容最大值,默认2M
server.tomcat.max-http-form-post-size=2000000
#服务器最大连接数
server.tomcat.max-connections=8192

SpringMVC 配置

1
2
3
4
5
spring.mvc.servlet.path=/course
#Servlet 的加载顺序,越小创建时间越早
spring.mvc.servlet.load-on-startup=0
#时间格式,可以在接受请求参数使用
spring.mvc.format.date-time=yyyy-MM-dd HH:mm:ss

@DateTimeFormat 格式化日期,可以方法,参数,字段上使用。
示例:控制器方法接受日期参数

1
2
3
4
5
@GetMapping("/test/date")
@ResponseBody public String paramDate(@DateTimeFormat(pattern = "yyyy-MM-dd
HH:mm:ss") LocalDateTime date){
return "日期:" + date;
}

无需设置:spring.mvc.format.date-time=yyyy-MM-dd HH:mm:ss

Servlets, Filters, and Listeners

Web 应用还会用到Servlet、Filter 或Listener。这些对象能够作为Spring Bean 注册到嵌入式的Tomcat 中。ServletRegistrationBean、FilterRegistrationBean 和ServletListenerRegistrationBean 控制Servlet,Filter,Listener

@Order 或Ordered 接口控制对象的先后顺序

Servlet 现在完全支持注解的使用方式,@WebServlet

Servlets
@WebServlet 使用Servlet
  • 创建Servlet
1
2
3
4
5
6
7
8
9
10
11
12
@WebServlet(urlPatterns = "/helloServlet",name = "HelloServlet")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.println("这是一个Spring Boot 中的Servlet");
writer.flush();
writer.close();
}
}
  • 扫描Servlet 注解
1
2
3
4
5
6
7
@ServletComponentScan(basePackages = "com.bjpowernode.web")
@SpringBootApplication
public class Lession13ServletFilterApplication {
public static void main(String[] args) {
SpringApplication.run(Lession13ServletFilterApplication.class, args);
}
}
ServletRegistrationBean

能够编码方式控制Servlet,不需要注解

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class WebAppConfig {
@Bean
public ServletRegistrationBean addServlet(){
ServletRegistrationBean registrationBean = new ServletRegistrationBean();
registrationBean.setServlet(new LoginServlet());
registrationBean.addUrlMappings("/user/login");
registrationBean.setLoadOnStartup(1);
return registrationBean;
}
}
Filter

Filter 对象使用频率比较高,比如记录日志,权限验证,敏感字符过滤等等。Web 框架中包含内置的Filter,
SpringMVC 中也包含较多的内置Filter,比如CommonsRequestLoggingFilter,CorsFilter,FormContentFilter…

@WebFilter 注解
1
2
3
4
5
6
7
8
9
10
11
@WebFilter(urlPatterns = "/*")
public class LogFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
String requestURI = ((HttpServletRequest) request).getRequestURI();
System.out.println("filter 代码执行了,uri=" +requestURI );
chain.doFilter(request,response);
}
}
  • 扫描包
1
2
3
4
5
6
7
@ServletComponentScan(basePackages = "com.bjpowernode.web")
@SpringBootApplication
public class Lession13ServletFilterApplication {
public static void main(String[] args) {
SpringApplication.run(Lession13ServletFilterApplication.class, args);
}
}
FilterRegistrationBean
1
2
3
4
5
6
7
@Bean
public FilterRegistrationBean addFilter(){
FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
filterRegistration.setFilter(new LogFilter());
filterRegistration.addUrlPatterns("/*");
return filterRegistration;
}
Filter 排序

多个Filter 对象如果要排序,有两种途径:

  1. 过滤器类名称,按字典顺序排列, AuthFilter - > LogFilter
  2. FilterRegistrationBean 登记Filter,设置order 顺序,数值越小,先执
  • 创建两个Filter,使用之前的AuthFilter, LogFilter,去掉两个Filter 上面的注解
  • 创建配置类,登记Filter
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
@Configuration
public class WebAppConfig {
@Bean
public ServletRegistrationBean addServlet(){
ServletRegistrationBean registrationBean = new ServletRegistrationBean();
registrationBean.setServlet(new LoginServlet());
registrationBean.addUrlMappings("/user/login");
registrationBean.setLoadOnStartup(1);
return registrationBean;
}
@Bean
public FilterRegistrationBean addLogFilter(){
FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
filterRegistration.setFilter(new LogFilter());
filterRegistration.addUrlPatterns("/*");
filterRegistration.setOrder(1);
return filterRegistration;
}
@Bean
public FilterRegistrationBean addAuthFilter(){
FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
filterRegistration.setFilter(new AuthFilter());
filterRegistration.addUrlPatterns("/*");
filterRegistration.setOrder(2);
return filterRegistration;
}
}

LogFilter.setOrder(1), AuthFilter.setOrder(2) ; LogFilter 先执行

使用框架中的Filter

Spring Boot 中有许多已经定义好的Filter,这些Filter 实现了一些功能,如果我们需要使用他们。可以像自己的Filter 一样,通过FilterRegistrationBean 注册Filter 对象。
现在我们想记录每个请求的日志。CommonsRequestLoggingFilter 就能完成简单的请求记录

  • 登记CommonsRequestLoggingFilter
1
2
3
4
5
6
7
8
9
10
11
12
13
@Bean
public FilterRegistrationBean addOtherFilter(){
FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
//创建Filter 对象
CommonsRequestLoggingFilter commonLog = new CommonsRequestLoggingFilter();
//包含请求uri
commonLog.setIncludeQueryString(true);
//登记Filter
filterRegistration.setFilter(commonLog);
//拦截所有地址
filterRegistration.addUrlPatterns("/*");
return filterRegistration;
}
  • 设置日志级别为debug,修改application.properties
1
logging.level.web=debug
Listener

@WebListener 用于注释监听器,监听器类必须实现下面的接口:

  • jakarta.servlet.http.HttpSessionAttributeListener
  • jakarta.servlet.http.HttpSessionListener
  • jakarta.servlet.ServletContextAttributeListener
  • jakarta.servlet.ServletContextListener
  • jakarta.servlet.ServletRequestAttributeListener
  • jakarta.servlet.ServletRequestListener
  • jakarta.servlet.http.HttpSessionIdListener

另一种方式用ServletListenerRegistrationBean 登记Listener 对象

创建监听器

1
2
3
4
5
6
7
@WebListener("Listener 的描述说明")
public class MySessionListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent se) {
HttpSessionListener.super.sessionCreated(se);
}
}

WebMvcConfigurer

WebMvcConfigurer 作为配置类是,采用JavaBean 的形式来代替传统的xml 配置文件形式进行针对框架个性化定制,就是Spring MVC XML 配置文件的JavaConfig(编码)实现方式。自定义Interceptor,ViewResolver,MessageConverter。WebMvcConfigurer 就是JavaConfig 形式的Spring MVC 的配置文件WebMvcConfigurer 是一个接口,需要自定义某个对象,实现接口并覆盖某个方法。主要方法功能介绍一下:

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
public interface WebMvcConfigurer {
//帮助配置HandlerMapping
default void configurePathMatch(PathMatchConfigurer configurer) {
}
//处理内容协商
default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
}
//异步请求
default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
}
//配置默认servlet
default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
}
//配置内容转换器
default void addFormatters(FormatterRegistry registry) {
}
//配置拦截器
default void addInterceptors(InterceptorRegistry registry) {
}
//处理静态资源
default void addResourceHandlers(ResourceHandlerRegistry registry) {
}
//配置全局跨域
default void addCorsMappings(CorsRegistry registry) {
}
//配置视图页面跳转
default void addViewControllers(ViewControllerRegistry registry) {
}
//配置视图解析器
default void configureViewResolvers(ViewResolverRegistry registry) {
}
//自定义参数解析器,处理请求参数
default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
}
//自定义控制器方法返回值处理器
default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
}
//配置HttpMessageConverters
default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
}
//配置HttpMessageConverters
default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
//配置异常处理器
default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
}
//扩展异常处理器
default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
}
//JSR303 的自定义验证器
default Validator getValidator() {
return null;
}
//消息处理对象
default MessageCodesResolver getMessageCodesResolver() {
return null;
}
}
页面跳转控制器

Spring Boot 中使用页面视图,比如Thymeleaf。要跳转显示某个页面,必须通过Controller 对象。也就是我们需要创建一个Controller,转发到一个视图才行。如果我们现在需要显示页面,可以无需这个Controller。addViewControllers() 完成从请求到视图跳转。
需求:访问/welcome 跳转到项目首页index.html(Thyemeleaf 创建的对象)

  • 创建视图,resources/templates/index.html
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>项目首页,欢迎各位小伙伴</h3>
</body>
</html>
  • 创建SpringMVC 配置类
1
2
3
4
5
6
7
8
@Configuration
public class MvcSettings implements WebMvcConfigurer {
// 跳转视图页面控制器
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/welcome").setViewName("index");
}
}
数据格式化

Formatter<T>是数据转换接口,将一种数据类型转换为另一种数据类型。与Formatter<T>功能类型的还有Converter<S,T>。Formatter<T>只能将String 类型转为其他数据数据类型。这点在Web 应用适用更广。因为几乎所有的Web 请求的所有参数都是String,我们需要把String 转为Integer ,Long,Date 等等
Spring 中内置了一下·Formatter·:

  • DateFormatter : String 和Date 之间的解析与格式化
  • InetAddressFormatter :String 和InetAddress 之间的解析与格式化
  • PercentStyleFormatter :String 和Number 之间的解析与格式化,带货币符合
  • NumberFormat :String 和Number 之间的解析与格式化

在使用@ DateTimeFormat , @NumberFormat 注解时,就是通过Formatter<T>解析String 类型到我们期望的Date 或Number 类型
Formatter<T>也是Spring 的扩展点,我们处理特殊格式的请求数据时,能够自定义合适的Formatter<T>,将请求的String 数据转为我们的某个对象,使用这个对象更方便后续编码

接口原型

1
2
public interface Formatter<T> extends Printer<T>, Parser<T> {
}

Formatter<T>是一个组合接口,没有自己的方法。内容来自Printer<T>Parser<T>
Printer<T>:将T 类型转为String,格式化输出

Parser<T>:将String 类型转为期望的T 对象

在项目开发的时候,可能面对多种类型的项目,复杂程度有简单,有难一些。特别是与硬件打交道的项目,数据的格式与一般的name: lisi, age:20 不同。数据可能是一串“1111; 2222; 333,NF; 4; 561”

需求:将“1111;2222;333,NF;4;561”接受,代码中用DeviceInfo 存储参数值

  • 创建DeviceInfo 数据类
1
2
3
4
5
6
7
8
@Data
public class DeviceInfo {
private String item1;
private String item2;
private String item3;
private String item4;
private String item5;
}
  • 自定义Formatter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class DeviceFormatter implements Formatter<DeviceInfo> {
//将String 数据,转为DeviceInfo
@Override
public DeviceInfo parse(String text, Locale locale) throws ParseException {
DeviceInfo info = null;
if (StringUtils.hasLength(text)) {
String[] items = text.split(";");
info = new DeviceInfo();
info.setItem1(items[0]);
info.setItem2(items[1]);
info.setItem3(items[2]);
info.setItem4(items[3]);
info.setItem5(items[4]);
}
return info;
}
//将DeviceInfo 转为String
@Override
public String print(DeviceInfo object, Locale locale) {
StringJoiner joiner = new StringJoiner("#");
joiner.add(object.getItem1()).add(object.getItem2());
return joiner.toString();
}
}
  • 新建Controller 接受请求设备数据
1
2
3
4
5
6
7
@RestController
public class DeviceController {
@PostMapping("/device/add")
public String AddDevice(@RequestParam("device") DeviceInfo deviceInfo){
return "接收到的设备数据:"+deviceInfo.toString();
}
}
  • 单元测试
1
2
3
POST http://localhost:8080/device/add
Content-Type: application/x-www-form-urlencoded
device=1111;2222;333,NF;4;561
拦截器

HandlerInterceptor 接口和它的实现类称为拦截器,是SpringMVC 的一种对象。拦截器是Spring MVC 框架的对象与Servlet 无关。拦截器能够预先处理发给Controller 的请求。可以决定请求是否被Controller 处理。用户请求是先由DispatcherServlet 接收后,在Controller 之前执行的拦截器对象

一个项目中有众多的拦截器:框架中预定义的拦截器, 自定义拦截器。根据拦截器的特点,类似权限验证,记录日志,过滤字符,登录token 处理都可以使用拦截器
拦截器定义步骤:

  1. 声明类实现HandlerInterceptor 接口,重写三个方法
  2. 登记拦截器
一个拦截器

需求:zhangsan 操作员用户,只能查看文章,不能修改,删除

  • 创建文章的Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
public class ArticleController {
@PostMapping("/article/add")
public String addArticle(){
return "发布新的文章";
}
@PostMapping("/article/edit")
public String editArticle(){
return "修改文章";
}
@GetMapping("/article/query")
public String query(){
return "查询文章";
}
}
  • 创建有关权限拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class AuthInterceptor implements HandlerInterceptor {
private final String COMMON_USER="zhangsan";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler)
throws Exception {
System.out.println("=====AuthInterceptor 权限拦截器=====");
//获取登录的用户
String loginUser = request.getParameter("loginUser");
//获取操作的url
String requestURI = request.getRequestURI();
if( COMMON_USER.equals(loginUser) &&
(requestURI.startsWith("/article/add")
|| requestURI.startsWith("/article/edit")
|| requestURI.startsWith("/article/remove"))) {
return false;
}
return true;
}
}
  • 登记拦截器
1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class MvcSettings implements WebMvcConfigurer {
//...
@Override
public void addInterceptors(InterceptorRegistry registry) {
AuthInterceptor authInterceptor= new AuthInterceptor();
registry.addInterceptor(authInterceptor)
.addPathPatterns("/article/**") //拦截article 开始的所有请求
.excludePathPatterns("/article/query"); //排除/article/query 请求
}
}
多个拦截器

增加一个验证登录用户的拦截器,只有zhangsan,lisi,admin 能够登录系统。其他用户不可以。两个拦截器登录的拦截器先执行,权限拦截器后执行,order()方法设置顺序,整数值越小,先执行

  • 创建登录拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LoginInterceptor implements HandlerInterceptor {
private List<String> permitUser= new ArrayList();
public LoginInterceptor() {
permitUser = Arrays.asList("zhangsan","lisi","admin");
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler)
throws Exception {
System.out.println("=====LoginInterceptor 登录拦截器=====");
//获取登录的用户
String loginUser = request.getParameter("loginUser");
if( StringUtils.hasText(loginUser) && permitUser.contains(loginUser) ) {
return true;
}
return false;
}
}
  • 登记拦截器,设置顺序order
1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void addInterceptors(InterceptorRegistry registry) {
AuthInterceptor authInterceptor= new AuthInterceptor();
registry.addInterceptor(authInterceptor)
.order(2)
.addPathPatterns("/article/**") //拦截article 开始的所有请求
.excludePathPatterns("/article/query"); //排除/article/query 请求
LoginInterceptor loginInterceptor = new LoginInterceptor();
registry.addInterceptor(loginInterceptor)
.order(1)
.addPathPatterns("/**") //拦截所有请求
.excludePathPatterns("/article/query"); //排除/article/query 请求
}

文件上传

上传文件首先想到的就是Apache Commons FileUpload,这个库使用非常广泛。Spring Boot3 版本中已经不能使用了。代替他的是Spring Boot 中自己的文件上传实现

Spring Boot 上传文件现在变得非常简单。提供了封装好的处理上传文件的接口MultipartResolver,用于解析上传文件的请求,他的内部实现类StandardServletMultipartResolver。之前常用的CommonsMultipartResolver 不可用了。CommonsMultipartResolver 是使用Apache Commons FileUpload 库时的处理类

StandardServletMultipartResolver 内部封装了读取POST 其中体的请求数据,也就是文件内容。我们现在只需要在Controller 的方法加入形参@RequestParam MultipartFile。MultipartFile 表示上传的文件,提供了方便的方法保存文件到磁盘

MultipartFile API

方法 作用
getName() 参数名称(upfile)
getOriginalFilename() 上传文件原始名称
isEmpty() 上传文件是否为空
getSize() 上传的文件字节大小
getInputStream() 文件的InputStream,可用于读取部件的内容
transferTo(File dest) 保存上传文件到目标dest
MultipartResolver
  • 服务器创建目录存放上传后的文件
  • 创建index.html 作为上传后的显示页面resources/static/index.html
1
2
3
4
5
<html lang="en">
<body>
<h3>项目首页,上传文件成功</h3>
</body>
</html>
  • 创建上传文件页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>上传文件</h3>
<form action="files" enctype="multipart/form-data" method="post">
选择文件:<input type="file" name="upfile" > <br/>
<input type="submit" value="上传文件">
</form>
</body>
</html>

<!--
enctype="multipart/form-data"
method="post"
<input type="file" name="upfile" > 表示一个上传文件,upfile 自定义上传文件参数名称
-->
  • 创建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
25
26
@Controller
public class UploadFileController {
@PostMapping("/upload")
public String upload(@RequestParam("upfile") MultipartFile multipartFile){
Map<String,Object> info = new HashMap<>();
try {
if( !multipartFile.isEmpty()){
info.put("上传文件参数名",multipartFile.getName());
info.put("内容类型",multipartFile.getContentType());
var ext = "unknown";
var filename = multipartFile.getOriginalFilename();
if(filename.indexOf(".") > 0){
ext = filename.substring(filename.indexOf(".") + 1);
}
var newFileName = UUID.randomUUID().toString() + ext;
var path = "E:/upload/" + newFileName;
info.put("上传后文件名称", newFileName );
multipartFile.transferTo(new File(path));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
//防止刷新,重复上传
return "redirect:/index.html";
}
}

Spring Boot 默认单个文件最大支持1M,一次请求最大10M。改变默认值,需要application 修改配置项

1
2
3
spring.servlet.multipart.max-file-size=800B
spring.servlet.multipart.max-request-size=5MB
spring.servlet.multipart.file-size-threshold=0KB

file-size-threshold 超过指定大小,直接写文件到磁盘,不在内存处理

  • 配置错误页面resources/static/error/5xx.html
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>上传文件错误</h3>
</body>
</html>
Servlet 规范

Servlet3.0 规范中,定义了jakarta.servlet.http.Part 接口处理multipart/form-data POST 请求中接收到表单数据。有了Part 对象,其write()方法将上传文件保存到服务器本地磁盘目录

在HttpServletRequest 接口中引入的新方法:

  • getParts():返回Part 对象的集合
  • getPart(字符串名称):检索具有给定名称的单个Part 对象

Spring Boot 3 使用的Servlet 规范是基于5 的,所以上传文件使用的就是Part 接口。StandardServletMultipartResolver 对Part 接口进行的封装,实现基于Servlet 规范的文件上传

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
@Controller
public class UploadAction {
@PostMapping("/files")
public String upload(HttpServletRequest request){
try {
for (Part part : request.getParts()) {
String fileName = extractFileName(part);
part.write(fileName);
}
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ServletException e) {
throw new RuntimeException(e);
}
return "redirect:/index.html";
}
private String extractFileName(Part part) {
String contentDisp = part.getHeader("content-disposition");
String[] items = contentDisp.split(";");
for (String s : items) {
if (s.trim().startsWith("filename")) {
return s.substring(s.indexOf("=") + 2, s.length()-1);
}
}
return "";
}
}

上传文件包含header 头content-disposition,类似下面的内容, 可获取文件原始名称。form-data; name=”dataFile”; filename=”header.png”

application 文件,可配置服务器存储文件位置,例如:

1
spring.servlet.multipart.location=E://files/
多文件上传

多文件上传,在接收文件参数部分有所改变MultiPartFile [] files 。循环遍历数组解析每个上传的文件

全局异常处理

在Controller 处理请求过程中发生了异常,DispatcherServlet 将异常处理委托给异常处理器(处理异常的类)。实现HandlerExceptionResolver 接口的都是异常处理类

项目的异常一般集中处理,定义全局异常处理器。在结合框架提供的注解,诸如:@ExceptionHandler,@ControllerAdvice ,@RestControllerAdvice 一起完成异常的处理

@ControllerAdvice 与@RestControllerAdvice 区别在于:@RestControllerAdvice 加了@RepsonseBody

全局异常处理器
  • 创建收入数字的页面在static 目录下创建input.html , static 目录下的资源浏览器可以直接访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="divide" method="get">
&nbsp;&nbsp;&nbsp;数:<input name="n1" /> <br/>
被除数:<input name="n2" /> <br/>
<input type="submit" value="计算">
</form>
</body>
</html>
  • 创建控制器,计算两个整数相除
1
2
3
4
5
6
7
8
@RestController
public class NumberController {
@GetMapping("/divide")
public String some(Integer n1,Integer n2){
int result = n1 / n2;
return "n1/n2=" + result;
}
}

如果输入的是10,0,则会显示默认错误页面

  • 创建自定义异常处理器
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
@ControllerAdvice
public class GlobalExceptionHandler {
//用视图页面作为展示
@ExceptionHandler({ArithmeticException.class})
public String handleArithmeticException(ArithmeticException e, Model model){
String error = e.getMessage();
model.addAttribute("error",error);
return "exp";
}
//不带视图,直接返回数据
/*
@ExceptionHandler({ArithmeticException.class})
@ResponseBody public Map<String,Object>
handleArithmeticExceptionReturnData(ArithmeticException e){
String error = e.getMessage();
Map<String,Object> map = new HashMap<>();
map.put("错误原因", e.getMessage());
map.put("解决方法", "输入的被除数要>0");
return map;
}*/
//其他异常
@ExceptionHandler({Exception.class})
@ResponseBody public Map<String,Object> handleRootException(Exception e){
String error = e.getMessage();
Map<String,Object> map = new HashMap<>();
map.put("错误原因", e.getMessage());
map.put("解决方法", "请稍候重试");
return map;
}
}
  • 创建给用提示的页面在resources/templates/ 创建exp.html
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
错误原因:<h3 th:text="${error}"></h3>
</body>
</html>

建议在参数签名中尽可能具体异常类,以减少异常类型和原因异常之间不匹配的问题,考虑创建多个@ExceptionHandler 方法的,每个方法通过其签名匹配单个特定的异常类型。最后增加一个根异常,考虑没有匹配的其他情况

BeanValidator 异常处理

使用JSR-303 验证参数时,我们是在Controller 方法,声明BindingResult 对象获取校验结果。Controller 的方法很多,每个方法都加入BindingResult 处理检验参数比较繁琐。校验参数失败抛出异常给框架,异常处理器能够捕获到MethodArgumentNotValidException,它是BindException 的子类

BindException 异常实现了BindingResult 接口,异常类能够得到BindingResult 对象,进一步获取JSR303 校验的异常信息

需求:全局处理JSR-303 校验异常

  • 添加JSR-303 依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  • 创建Bean 对象,属性加入JSR-303 注解
1
2
3
4
5
6
7
8
9
10
11
@Data
public class OrderVO {
@NotBlank(message = "订单名称不能为空")
private String name;
@NotNull(message = "商品数量必须有值")
@Range(min = 1,max = 99,message = "一个订单商品数量在{min}-{max}")
private Integer amount;
@NotNull(message = "用户不能为空")
@Min(value = 1,message = "从1 开始")
private Integer userId;
}
  • Controlller 接收请求
1
2
3
4
5
6
7
@RestController
public class OrderController {
@PostMapping("/order/new")
public String createOrder(@Validated @RequestBody OrderVO orderVO){
return orderVO.toString();
}
}
  • 创建异常处理器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestControllerAdvice
public class GlobalExceptionHandler2 {
//校验参数异常
@ExceptionHandler({BindException.class})
public Map<String,Object> handleJSR303Exception(BindException e){
Map<String,Object> map = new HashMap<>();
BindingResult result = e.getBindingResult();
if (result.hasErrors()) {
List<FieldError> errors = result.getFieldErrors();
errors.forEach(field -> {
map.put("错误["+field.getField()+"]原因",field.getDefaultMessage());
});
}
return map;
}
}
ProblemDetail [SpringBoot 3]

一直依赖Spring Boot 默认的异常反馈内容比较单一,包含Http Status Code, 时间,异常信息。但具体异常原因没有体现。这次Spring Boot3 对错误信息增强了

RFC 7807

RFC 7807: Problem Details for HTTP APIs

RESTFul 服务中通常需要在响应体中包含错误详情,Spring 框架支持”Problem Details“。定义了Http 应答错误的处理细节,增强了响应错误的内容。包含标准和非标准的字段。同时支持json 和xml 两种格式
基于Http 协议的请求,可通过Http Status Code 分析响应结果,200 为成功, 4XX 为客户端错误,500 是服务器程序代码异常。status code 过于简单,不能进一步说明具体的错误原因和解决途径。比如http status code 403,但并不能说明”是什么导致了403“,以及如何解决问题。Http 状态代码可帮助我们区分错误和成功状态,但没法区分得太细致。RFC 7807 中对这些做了规范的定义

”Problem Details“ 的JSON 应答格式

1
2
3
4
5
6
{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/transactions/abc"
}

”Problem Details“ 包含内容:

标准字段 描述 必须
type 标识错误类型的URI。在浏览器中加载这个URI 应该转向这个错误的文档。此字段可用于识别错误类。完善的系统可用type 构建异常处理模块,
默认为about:blank
可认为是
title 问题类型的简短、易读的摘要
detail 错误信息详细描述,对title 的进一步阐述
instance 标识该特定故障实例的URI。它可以作为发生的这个错误的ID
status 错误使用的HTTP 状态代码。它必须与实际状态匹配

除了以上字段,用户可以扩展字段。采用key:value 格式。增强对问题的描述

MediaType

RFC 7807 规范增加了两种媒体类型: application/problem+jsonapplication/problem+xml。返回错误的HTTP 响应应在其Content-Type响应标头中包含适当的内容类型,并且客户端可以检查该标头以确认格式

Spring 支持Problem Detail

Spring 支持ProblemDetail

  • ProblemDetail 类: 封装标准字段和扩展字段的简单对象
  • ErrorResponse :错误应答类,完整的RFC 7807 错误响应的表示,包括status、headers 和RFC 7807 格式的ProblemDetail 正文- -
  • ErrorResponseException :ErrorResponse 接口一个实现,可以作为一个方便的基类。扩展自定义的错误处理类
  • ResponseEntityExceptionHandler:它处理所有Spring MVC 异常,与@ControllerAdvice 一起使用

以上类型作为异常处理器方法的返回值,框架将返回值格式化RFC 7807 的字段
ErrorResponse:接口,ErrorResponseException 是他的实现类,包含应答错误的status ,header, ProblemDetail .SpringMVC 中异常处理方法(带有@ExceptionHandler)返回ProblemDetail ,ErrorResponse 都会作为RFC 7807的规范处理

自定义异常处理器ProblemDetail

需求:我们示例查询某个isbn 的图书。在application.yml 中配置图书的初始数据。用户访问一个api 地址,查询某个isbn 的图书, 查询不到抛出自定义异常BootNotFoundException。自定义异常处理器捕获异常

ProblemDetail 作为应答结果。支持RFC 7807

  • Maven 依赖
1
2
3
4
5
6
7
8
9
10
<!--web 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok 依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
  • 新建图书的Record(普通的POJO 类都是可以的)
1
2
public record Book(String isbn,String name,String author) {
}
  • 创建存储多本图书的容器类
1
2
3
4
5
6
@Setter
@Getter
@ConfigurationProperties(prefix = "product")
public class BookContainer {
private List<Book> books;
}
  • application.yml 配置图书基础数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
product:
books:
- isbn: B001
name: java
author: lisi
- isbn: B002
name: tomcat
author: zhangsan
- isbn: B003
name: jvm
author: zhouxing
server:
servlet:
context-path: /api
  • 新建自定义异常类
1
2
3
4
5
6
7
8
public class BookNotFoundException extends RuntimeException{
public BookNotFoundException() {
super();
}
public BookNotFoundException(String message) {
super(message);
}
}
  • 新建控制器类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
public class BookController {
@Resource
private BookContainer bookContainer;
@GetMapping("/book")
Book getBook(String isbn) throws Exception {
Optional<Book> book = bookContainer.getBooks().stream()
.filter(el -> el.isbn().equals(isbn))
.findFirst();
if( book.isEmpty() ){
throw new BookNotFoundException("isbn:"+ isbn + "->没有此图书");
}
return book.get();
}
}
  • 新建异常处理器
1
2
3
4
5
6
7
8
9
10
11
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = { BookNotFoundException.class })
public ProblemDetail handleBookNotFoundException(BookNotFoundException ex){
ProblemDetail problemDetail =
ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND,ex.getMessage());
problemDetail.setType(URI.create("/api/probs/not-found"));
problemDetail.setTitle("图书异常");
return problemDetail;
}
}
扩展ProblemDetail

修改异常处理方法,增加ProblemDetail 自定义字段,自定义字段以Map<String,Object>存储,调用setProperty(name,value)将自定义字段添加到ProblemDetail 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
@ExceptionHandler(value = { BookNotFoundException.class })
public ProblemDetail handleBookNotFoundException(BookNotFoundException ex){
ProblemDetail problemDetail =
ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND,ex.getMessage());
problemDetail.setType(URI.create("/api/probs/not-found"));
problemDetail.setTitle("图书异常");
//增加自定义字段
//时间戳
problemDetail.setProperty("timestamp", Instant.now());
//客服邮箱
problemDetail.setProperty("客服邮箱", "sevice@bjpowernode.com");
return problemDetail;
}
ErrorResponse

Spring Boot 识别ErrorResponse 类型作为异常的应答结果。可以直接使用ErrorResponse 作为异常处理方法的返回值,ErrorResponseException 是ErrorResponse 的基本实现类

注释掉GlobalExceptionHandler#handleBookNotFoundException 方法,增加下面的方法

1
2
3
4
5
@ExceptionHandler(value = { BookNotFoundException.class})
public ErrorResponse handleException(BookNotFoundException ex){
ErrorResponse error = new ErrorResponseException(HttpStatus.NOT_FOUND,ex);
return error;
}
扩展ErrorResponseException

自定义异常可以扩展ErrorResponseException, SpringMVC 将处理异常并以符合RFC 7807 的格式返回错误响应。ResponseEntityExceptionHandler 能够处理大部分SpringMVC 的异常的其方法handleException()提供了对ErrorResponseException 异常处理:

1
2
3
4
5
6
7
8
@ExceptionHandler({
...
ErrorResponseException.class,
...
})
@Nullable
public final ResponseEntity<Object> handleException(Exception ex, WebRequest
request)

由此可以创建自定义异常类,继承ErrorResponseException,剩下的交给SpringMVC 内部自己处理就好。省去了自己的异常处理器,@ExceptionHandler

  • 创建新的异常类继承ErrorResponseException
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class IsbnNotFoundException extends ErrorResponseException {
public IsbnNotFoundException(HttpStatus status, String detail) {
super(status,createProblemDetail(status,detail),null);
}
private static ProblemDetail createProblemDetail(HttpStatus status,String detail)
{
ProblemDetail problemDetail = ProblemDetail.forStatus(status);
problemDetail.setType(URI.create("/api/probs/not-found"));
problemDetail.setTitle("图书异常");
problemDetail.setDetail(detail);
//增加自定义字段
problemDetail.setProperty("严重程度", "低");
problemDetail.setProperty("客服邮箱", "sevice@bjpowernode.com");
return problemDetail;
}
}
  • 抛出IsbnNotFoundException
1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/book")
Book getBook(String isbn) throws Exception {
Optional<Book> book = bookContainer.getBooks().stream()
.filter(el -> el.isbn().equals(isbn))
.findFirst();
if( book.isEmpty() ){
//throw new BookNotFoundException("isbn:"+ isbn + "->没有此图书");
throw new IsbnNotFoundException(HttpStatus.NOT_FOUND,"isbn:"+ isbn + "->没有此图书");
}
return book.get();
}
  • 启动RFC 7807 支持修改application.yml,增加配置
1
2
3
4
spring:
mvc:
problemdetails:
enabled: true

远程访问@HttpExchange[SpringBoot 3]

远程访问是开发的常用技术,一个应用能够访问其他应用的功能。Spring Boot 提供了多种远程访问的技术。基于HTTP 协议的远程访问是支付最广泛的。Spring Boot3 提供了新的HTTP 的访问能力,通过接口简化HTTP远程访问,类似Feign 功能。Spring 包装了底层HTTP 客户的访问细节

SpringBoot 中定义接口提供HTTP 服务。生成的代理对象实现此接口,代理对象实现HTTP 的远程访问。需要理解:

  • @HttpExchange
  • WebClient

WebClient 特性:
我们想要调用其他系统提供的HTTP 服务,通常可以使用Spring 提供的RestTemplate 来访问,RestTemplate 是Spring 3 中引入的同步阻塞式HTTP 客户端,因此存在一定性能瓶颈。Spring 官方在Spring 5中引入了WebClient 作为非阻塞式HTTP 客户端

  • 非阻塞,异步请求
  • 它的响应式编程的基于Reactor
  • 高并发,硬件资源少
  • 支持Java 8 lambdas 函数式编程

什么是异步非阻塞
理解:异步和同步,非阻塞和阻塞
上面都是针对对象不一样
异步和同步针对调度者,调用者发送请求,如果等待对方回应之后才去做其他事情,就是同步,如果发送请求之后不等着对方回应就去做其他事情就是异步
阻塞和非阻塞针对被调度者,被调度者收到请求后,做完请求任务之后才给出反馈就是阻塞,收到请求之后马上给出反馈然后去做事情,就是非阻塞

准备工作
  1. 安装GsonFormat 插件,方便json 和Bean 的转换
  2. 介绍一个免费的、24h 在线的Rest Http 服务,JSONPlaceholder - Free Fake REST API
声明式HTTP 远程服务

需求:访问https://jsonplaceholder.typicode.com/ 提供的todos 服务。基于RESTful 风格,添加新的todo,修改todo,修改todo 中的title,查询某个todo。声明接口提供对象https://jsonplaceholder.typicode.com/todos 服务的访问

  • Maven 依赖pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
  • 声明Todo 数据类
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
/**
* 根据https://jsonplaceholder.typicode.com/todos/1 的结构创建的
*/
public class Todo {
private int userId;
private int id;
private String title;
private boolean completed;
//省略set , get 方法
public boolean getCompleted() {
return completed;
}
public void setCompleted(boolean completed) {
this.completed = completed;
}
@Override
public String toString() {
return "Todo{" +
"userId=" + userId +
", id=" + id +
", title='" + title + '\'' +
", completed=" + completed +
'}';
}
}
  • 声明服务接口
1
2
3
4
5
6
7
8
9
10
11
12
13
public interface TodoService {
@GetExchange("/todos/{id}")
Todo getTodoById(@PathVariable Integer id);
@PostExchange(value = "/todos",accept = MediaType.APPLICATION_JSON_VALUE)
Todo createTodo(@RequestBody Todo newTodo);
@PutExchange("/todos/{id}")
ResponseEntity<Todo> modifyTodo(@PathVariable Integer id,@RequestBody Todo
todo);
@PatchExchange("/todos/{id}")
HttpHeaders pathRequest(@PathVariable Integer id, @RequestParam String title);
@DeleteExchange("/todos/{id}")
void removeTodo(@PathVariable Integer id);
}
  • 创建HTTP 服务代理对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration(proxyBeanMethods = false) // 表示该类是一个配置类,并关闭代理,以优化性能
public class HttpConfiguration {

@Bean // 声明一个 `TodoService` 的 Bean,将其加入 Spring 容器
public TodoService requestService() {

// 创建一个 WebClient 实例,用于发送 HTTP 请求
WebClient webClient = WebClient.builder()
.baseUrl("https://jsonplaceholder.typicode.com/") // 设置基础 URL
.build();

// 使用 WebClientAdapter 构建 HttpServiceProxyFactory,生成代理
HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(
WebClientAdapter.forClient(webClient)) // 将 WebClient 适配为代理工厂
.build();

// 返回 TodoService 的代理实现类,使调用其方法时实际发出 HTTP 请求
return proxyFactory.createClient(TodoService.class);
}
}
  • 单元测试
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
@SpringBootTest
class HttpApplicationTests {
@Resource
private TodoService requestService;
@Test
void testQuery() {
Todo todo = requestService.getTodoById(1);
System.out.println("todo = " + todo);
}
@Test
void testCreateTodo() {
Todo todo = new Todo();
todo.setId(1001);
todo.setCompleted(true);
todo.setTitle("录制视频");
todo.setUserId(5001);
Todo save = requestService.createTodo(todo);
System.out.println(save);
}
@Test
void testModifyTitle() {
//org.springframework.http.HttpHeaders
HttpHeaders entries = requestService.pathRequest(5, "homework");
entries.forEach( (name,vals)->{
System.out.println(name);
vals.forEach(System.out::println);
System.out.println("=========================");
});
}
@Test
void testModifyTodo() {
Todo todo = new Todo();
todo.setCompleted(true);
todo.setTitle("录制视频!!!");
todo.setUserId(5002);
ResponseEntity<Todo> result = requestService.modifyTodo(2,todo);
HttpStatusCode statusCode = result.getStatusCode();
HttpHeaders headers = result.getHeaders();
Todo modifyTodo = result.getBody();
System.out.println("statusCode = " + statusCode);
System.out.println("headers = " + headers);
System.out.println("modifyTodo = " + modifyTodo);
}
@Test
void testRemove() {
requestService.removeTodo(2);
}
}
Http 服务接口的方法定义

@HttpExchange 注解用于声明接口作为HTTP 远程服务。在方法、类级别使用。通过注解属性以及方法的参数设置HTTP 请求的细节
快捷注解简化不同的请求方式

  • GetExchange
  • PostExchange
  • PutExchange
  • PatchExchange
  • DeleteExchange

@GetExchange 就是@HttpExchange 表示的GET 请求方式

1
2
3
4
5
6
7
8
9
@HttpExchange(method = "GET")
public @interface GetExchange {
@AliasFor(annotation = HttpExchange.class)
String value() default "";
@AliasFor(annotation = HttpExchange.class)
String url() default "";
@AliasFor(annotation = HttpExchange.class)
String[] accept() default {};
}

作为HTTP 服务接口中的方法允许使用的参数列表

参数 说明
URL 设置请求的url,覆盖注解的url 属性
HttpMethod 请求方式,覆盖注解的method 属性
@RequestHeader 添加到请求中header。参数类型可以为Map<String,?>, MultiValueMap<String,?>,单个值或者Collection<?>
@PathVariable url 中的占位符,参数可为单个值或Map<String,?>
@RequestBody 请求体,参数是对象
@RequestParam 请求参数,单个值或Map<String,?>, MultiValueMap<String,?>,Collection<?>
@RequestPart 发送文件时使用
@CookieValue 向请求中添加cookie

接口中方法返回值

返回值类型 说明
void 执行请求,无需解析应答
HttpHeaders 存储response 应答的header 信息
对象 解析应答结果,转为声明的类型对象
ResponseEntity<Void>ResponseEntity<T> 解析应答内容,得到ResponseEntity,从ResponseEntity 可以获取http 应答码,header,body 内容

反应式的相关的返回值包含Mono<Void>,Mono<HttpHeaders>,Mono<T>,Flux<T>
Mono<ResponseEntity<Void>>,Mono<ResponseEntity<T>>,Mono<ResponseEntity<Flux<T>>

组合使用注解

@HttpExchange , @GetExchange 等可以组合使用
这次使用Albums 远程服务接口,查询Albums 信息

  • 创建Albums 数据类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Albums {
private int userId;
private int id;
private String title;
//省略set ,get
@Override
public String toString() {
return "Albums{" +
"userId=" + userId +
", id=" + id +
", title='" + title + '\'' +
'}';
}
}
  • 创建AlbumsService 接口
  • 接口声明方法,提供HTTP 远程服务。在类级别应用@HttpExchange 接口,在方法级别使用@HttpExchange ,
    @GetExchange 等
1
2
3
4
5
6
7
8
@HttpExchange(url = "https://jsonplaceholder.typicode.com/")
public interface AlbumsService {
@GetExchange("/albums/{aid}")
Albums getById(@PathVariable Integer aid);
@HttpExchange(url = "/albums/{aid}",method = "GET",
contentType = MediaType.APPLICATION_JSON_VALUE)
Albums getByIdV2(@PathVariable Integer aid);
}
  • 声明代理
1
2
3
4
5
6
7
8
9
10
11
@Configuration(proxyBeanMethods = true)
public class HttpServiceConfiguration {
@Bean
public AlbumsService albumsService(){
WebClient webClient = WebClient.create();
HttpServiceProxyFactory proxyFactory =
HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient))
.build();
return proxyFactory.createClient(AlbumsService.class);
}
}
  • 单元测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest
public class TestHttpExchange {
@Resource
AlbumsService albumsService;
@Test
void getQuery() {
Albums albums = albumsService.getById(1);
System.out.println("albums = " + albums);
}
@Test
void getQueryV2() {
Albums albums = albumsService.getByIdV2(2);
System.out.println("albums = " + albums);
}
}
Java Record

测试Java Record 作为返回类型,由框架的HTTP 代理转换应该内容为Record 对象

  • 创建Albums 的Java Record
1
2
3
4
public record AlbumsRecord(int userId,
int id,
String title) {
}
  • AlbumsService 接口增加新的远程访问方法,方法返回类型为Record
1
2
3
4
5
6
7
@GetExchange("/albums/{aid}")
AlbumsRecord getByIdRecord(@PathVariable Integer aid);
@Test
void getQueryV3() {
AlbumsRecord albums = albumsService.getByIdRecord(1);
System.out.println("albums = " + albums);
}
定制HTTP 请求服务

设置HTTP 远程的超时时间, 异常处理
在创建接口代理对象前,先设置WebClient 的有关配置

  • 设置超时,异常处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Bean
public AlbumsService albumsService(){
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30000) //连接超时
.doOnConnected(conn -> {
conn.addHandlerLast(new ReadTimeoutHandler(10)); //读超时
conn.addHandlerLast(new WriteTimeoutHandler(10)); //写超时
});
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
//定制4XX,5XX 的回调函数
.defaultStatusHandler(HttpStatusCode::isError,clientResponse -> {
System.out.println("******WebClient 请求异常********");
return
Mono.error(new RuntimeException(
"请求异常"+ clientResponse.statusCode().value()));
}).build();
HttpServiceProxyFactory proxyFactory
= HttpServiceProxyFactory.builder(
WebClientAdapter.forClient(webClient)).build();
return proxyFactory.createClient(AlbumsService.class);
}

视图技术Thymeleaf

Thymeleaf 是一个表现层的模板引擎, 一般被使用在Web 环境中,它可以处理HTML, XML、JS 等文档,简单来说,它可以将JSP 作为Java Web 应用的表现层,有能力展示与处理数据。Thymeleaf 可以让表现层的界面节点与程序逻辑被共享,这样的设计, 可以让界面设计人员、业务人员与技术人员都参与到项目开发中
这样,同一个模板文件,既可以使用浏览器直接打开,也可以放到服务器中用来显示数据,并且样式之间基本上不会存在差异,因此界面设计人员与程序设计人员可以使用同一个模板文件,来查看静态与动态数据的效果
Thymeleaf 作为视图展示模型数据,用于和用户交互操作。JSP 的代替技术。比较适合做管理系统

表达式

表达式用于在页面展示数据的,有多种表达式语法,最常用的是变量表达式、链接表达式

表达式 作用 例子
$ {…} 变量表达式,可用于获取后台传过来的值 <p th:text="${userName}">中国</p>
@ {…} 链接⽹址表达式 th:href="@{/css/home.css}"
  • 创建首页在static/main.html
1
2
3
4
5
6
7
8
<html lang="en">
</head>
<body>
<div style="margin-left: 200px">
1.<a href="exp">表达式</a> <br/>
</div>
</body>
</html>
  • 创建Controller,提供数据给页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
public class ThymeleafController {
@GetMapping("/exp")
public String exp(Model model){
model.addAttribute("name","");
model.addAttribute("address","");
return "exp";
}
@GetMapping("/link")
public String link(Integer id, String name,Model model){
model.addAttribute("id",id);
model.addAttribute("myname",name);
return "link";
}
}
  • templates/目下创建Thymeleaf 的文件
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
<!--exp.html-->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>表达式</h3>
<div th:text="${name}"></div>
<div th:text="${address}"></div>
<br/>
<a th:href="@{http://www.baidu.com}">连接到百度</a> <br/>
<a th:href="@{/link}">连接表达式无参数</a> <br/>
<a th:href="@{/link(id=111,name=lisi)}">连接表达式传递参数</a> <br/>
</body>
</html>

<!--link.html-->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>表达式</h3>
id:<div th:text="${id}"></div>
姓名:<div th:text="${myname}"></div>
</body>
</html>
if-for
  • main.html
1
<a href="if-for">if 和for</a>
  • 创建UserVO
1
2
3
4
5
6
7
@Data
@AllArgsConstructor
public class UserVO {
private Integer id;
private String name;
private Integer age;
}
  • 创建控制器方法
1
2
3
4
5
6
7
8
9
10
11
@GetMapping("if-for")
public String ifFor(Model model){
UserVO userVO = new UserVO(10, "李四",20);
model.addAttribute("user", userVO);
List<UserVO> users = Arrays.asList(
new UserVO(11, "张三",21),
new UserVO(12, "周行",22)
);
model.addAttribute("users",users);
return "base";
}
  • 创建base.html 模板页面
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
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>if,for 表达式</h3>
<div th:if="${user.age > 18}">
成年了
</div>
<br/>
<table border="1">
<thead>
<th>id</th>
<th>姓名</th>
<th>年龄</th>
</thead>
<tbody>
<tr th:each="u:${users}">
<td th:text="${u.id}"></td>
<td th:text="${u.name}"></td>
<td th:text="${u.age}"></td>
</tr>
</tbody>
</table>
</body>
</html>

文章管理模块

新的Spring Boot 项目BlogAdmin。Maven 构建工具,包名称top.qianqianzyk.blog
JDK19,依赖:

  • Spring Web
  • Lombok
  • Thymeleaf
  • MyBatis Framework
  • MySQL Driver

依赖还需要Bean Validation

需求:文章管理工作,发布新文章,编辑文章,查看文章内容等

配置文件

  • app-base.yml
1
2
3
4
5
article:
#最低文章阅读数量
low-read: 10
#首页显示最多的文章数量
top-read: 20
  • db.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/blog?serverTimezone=Asia/Shanghai
username: root
password: 123456
hikari:
auto-commit: true
maximum-pool-size: 10
minimum-idle: 10
#获取连接时,检测语句
connection-test-query: select 1
connection-timeout: 20000
#其他属性
data-source-properties:
cachePrepStmts: true
dataSource.cachePrepStmtst: true
dataSource.prepStmtCacheSize: 250
dataSource.prepStmtCacheSqlLimit: 2048
dataSource.useServerPrepStmts: true
视图文件

favicon.ico 放在static/ 根目录下

创建模板页面

  • articleList.html
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
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>文章列表</title>
<link rel="icon" href="../favicon.ico" type="image/x-icon"/>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
</head>
<body>
<div style="margin-left: 200px">
<h3>阅读最多的前10 篇文章</h3>
<table border="1px" cellspacing="0px" cellpadding="2px">
<thead>
<th>选择</th>
<th>序号</th>
<th>标题</th>
<th>副标题</th>
<th>已读数量</th>
<th>发布时间</th>
<th>最后修改时间</th>
<th>编辑</th>
</thead>
<tbody>
<tr th:each="article,loopStats : ${articleList}">
<td><input type="checkbox" th:value="${article.id}"></td>
<td th:text="${loopStats.index+1}"></td>
<td th:text="${article.title}"></td>
<td th:text="${article.summary}"></td>
<td th:text="${article.readCount}"></td>
<td th:text="${article.createTime}"></td>
<td th:text="${article.updateTime}"></td>
<td>
<a th:href="@{/article/get(id=${article.id})}">编辑</a>
</td>
</tr>
<tr>
<td colspan="8">
<table width="100%">
<tr>
<td><button id="add" onclick="addArticle()">发布新文章
</button></td>
<td><button id="delete" onclick="deleteArticle()">删除文章
</button></td>
<td><button id="read" onclick="readOverview()">文章概览
</button></td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
<form id="frm" th:action="@{/view/addArticle}" method="get">
</form>
<form id="delfrm" th:action="@{/article/removeArticle}" method="post">
<input type="hidden" id="idsDom" name="ids" value="" >
</form>
</div>
<script type="text/javascript">
function addArticle(){
$("#frm").submit();
}
function deleteArticle(){
var ids = [];
$("input[type='checkbox']:checked").each( (index,item)=>{
ids.push( item.value );
})
$("#idsDom").val(ids);
$("#delfrm").submit();
}
function readOverview(){
var ids = [];
$("input[type='checkbox']:checked").each( (index,item)=>{
ids.push( item.value );
})
if( ids.length != 1){
alert("选择一个文章查看");
return;
}
$.get("../article/detail/overview", { id:ids[0] }, (data,status)=>{
alert(data)
} )
}
</script>
</body>
</html>
  • addArticle.html
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="icon" href="../favicon.ico" type="image/x-icon"/>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
</head>
<body>
<div style="margin-left: 200px">
<h3>发布文章</h3>
<form id="frm" th:action="@{/article/add}" method="post">
<table border="1px" cellspacing="0px" cellpadding="5px">
<tr>
<td>标题</td>
<td><input type="text" name="title"></td>
</tr>
<tr>
<td>副标题</td>
<td><input type="text" name="summary" size="50"></td>
</tr>
<tr>
<td>文章内容</td>
<td>
<textarea name="content" rows="20" cols="60" ></textarea>
</td>
</tr>
</table>
<br />
<input type="submit" value="发布新文章" style="margin-left: 200px">
</form>
</div>
<script type="text/javascript">
</script>
</body>
</html>
  • editArticle.html
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
editArticle.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="icon" href="../favicon.ico" type="image/x-icon"/>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
</head>
<body>
<div style="margin-left: 200px">
<h3>修改文章</h3>
<form id="frm" th:action="@{/article/edit}" method="post">
<table border="1px" cellspacing="0px" cellpadding="5px">
<tr>
<td>标题</td>
<td><input type="text" th:value="${article.title}" name="title"></td>
</tr>
<tr>
<td>副标题</td>
<td><input type="text" th:value="${article.summary}" name="summary"
size="50"></td>
</tr>
<tr>
<td>文章内容</td>
<td>
<textarea name="content" th:text="${article.content}" rows="20"
cols="60">
</textarea>
</td>
</tr>
</table>
<br />
<input type="hidden" name="id" th:value="${article.id}">
<input type="submit" value="确定修改" style="margin-left: 200px">
</form>
</div>
<script type="text/javascript">
</script>
</body>
</html>
  • /error/bind.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div style="margin-left: 200px">
<div th:each="field:${errors}">
<div th:text="${field.field}"></div>
<div th:text="${field.defaultMessage}"></div>
</div>
</div>
</body>
</html>
  • /error/error.html
1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div style="margin-left: 200px">
<div th:text="${error}"></div>
</div>
</body>
</html>
Java 代码
  • model/vo/ArticleVO.java
1
2
3
4
5
6
7
8
9
10
11
@Data
public class ArticleVO {
private Integer id;
private Integer userId;
private String title;
private String summary;
private String content;
private Integer readCount;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
  • model/param/ArticleParam.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Data
public class ArticleParam {
public static interface AddArticle {};
public static interface EditArticle {};
@NotNull(message = "修改时必须有id",groups = EditArticle.class)
@Min(value = 1,message = "id 必须从{value}开始",groups = EditArticle.class)
private Integer id;
@NotBlank(message = "请输入文章标题",groups ={ AddArticle.class,
EditArticle.class })
@Size(min = 2,max = 20,message = "文章标题{min}-{max}",groups ={ AddArticle.class,
EditArticle.class })
private String title;
@NotBlank(message = "请输入文章副标题",groups ={ AddArticle.class,
EditArticle.class })
@Size(min = 10,max = 30,message = "文章副标题{min}-{max}",groups
={ AddArticle.class, EditArticle.class })
private String summary;
@NotBlank(message = "请输入文章副标题",groups ={ AddArticle.class,
EditArticle.class })
@Size(min = 50,max = 8000,message = "文章至少五十个字,文章至多八千字",groups
={ AddArticle.class, EditArticle.class })
private String content;
}
  • model/dto/ArticleDTO.java
1
2
3
4
5
6
7
8
9
10
11
@Data
public class ArticleDTO {
private Integer id;
private Integer userId;
private String title;
private String summary;
private Integer readCount;
private String content;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
  • model/po/ArticlePO.java
1
2
3
4
5
6
7
8
9
10
@Data
public class ArticlePO {
private Integer id;
private Integer userId;
private String title;
private String summary;
private Integer readCount;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
  • model/po/ArticleDetailPO.java
1
2
3
4
5
6
@Data
public class ArticleDetailPO {
private Integer id;
private Integer articleId;
private String content;
}
  • mapper/ArticleMapper.java
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
public interface ArticleMapper {
@Select("""
select id,user_id,title,summary,read_count,create_time,update_time
from article
where read_count >=#{lowRead}
order by read_count desc
limit #{topRead}
""")
@Results(id = "ArticleBaseMap", value = {
@Result(id = true, column = "id", property = "id"),
@Result(column = "user_id", property = "userId"),
@Result(column = "title", property = "title"),
@Result(column = "summary", property = "summary"),
@Result(column = "read_count", property = "readCount"),
@Result(column = "create_time", property = "createTime"),
@Result(column = "update_time", property = "updateTime"),
})
List<ArticlePO> topSortByReadCount(Integer lowRead, Integer topRead);

@Insert("""
insert into
article(user_id,title,summary,read_count,create_time,update_time) \
values(#{userId},#{title},#{summary},#{readCount},#{createTime},#{updateTime})
""")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
int insertArticle(ArticlePO articlePO);

@Insert("""
insert into article_detail(article_id, content)
values (#{articleId},#{content})
""")
int insertArticleDetail(ArticleDetailPO articleDetailPO);

@Select("""
select m.id as articleId,title,summary,content
from article m left join article_detail ad
on m.id = ad.article_id
where m.id=#{id}
""")
@Results({
@Result(id = true, column = "articleId", property = "id"),
@Result(column = "title", property = "title"),
@Result(column = "summary", property = "summary"),
@Result(column = "content", property = "content", jdbcType =JdbcType.LONGVARCHAR, javaType = String.class)
})
ArticleDTO selectArticleAndDetail(Integer id);

//更新文章title,summary
@Update("""
update article set title=#{title},summary=#{summary} where id=#{id}
""")
int updateArticle(ArticlePO articlePO);

@Update("""
update article_detail set content=#{content} where article_id=#{articleId}
""")
int updateArticleDetail(ArticleDetailPO articleDetailPO);

//<script>动态sql</script>
@Delete("""
<script>
delete from article where id in
<foreach item="id" collection="list" open="(" separator="," close=")">
#{id}
</foreach>
</script>
""")
int deleteArticle(List<Integer> ids);

@Delete("""
<script>
delete from article_detail where id in
<foreach item="id" collection="list" open="(" separator="," close=")">
#{id}
</foreach>
</script>
""")
int deleteArticleDetail(List<Integer> ids);

@Select("""
select id,article_id,content from article_detail
where article_id= #{id}
""")
ArticleDetailPO selectDetailByArticleId(Integer id);
}
  • service/ArticleService.java
1
2
3
4
5
6
7
8
public interface ArticleService {
List<ArticlePO> queryTopAritcle();
boolean addArticle(ArticleDTO article);
boolean modifyArticle(ArticleParam param);
int removeArticle(List<Integer> ids);
ArticleDTO queryByArticleId(Integer id);
String queryTop20Detail(Integer id);
}
  • service/impl/ArticleServiceImpl.java
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
@RequiredArgsConstructor
@Service
public class ArticleServiceImpl implements ArticleService {
private final ArticleMapper articleMapper;
private final ArticleSettings articleSettings;
@Override
public List<ArticlePO> queryTopAritcle() {
Integer lowRead = articleSettings.getLowRead();
Integer topRead = articleSettings.getTopRead();
return articleMapper.topSortByReadCount(lowRead, topRead);
}
@Transactional(rollbackFor = Exception.class)
@Override
public boolean addArticle(ArticleDTO article) {
ArticlePO articlePO = new ArticlePO();
articlePO.setTitle(article.getTitle());
articlePO.setSummary(article.getSummary());
//从登陆信息中获取,现在给个默认
articlePO.setUserId(new Random().nextInt(1000));
articlePO.setReadCount(new Random().nextInt(50));
articlePO.setCreateTime(LocalDateTime.now());
articlePO.setUpdateTime(LocalDateTime.now());
articleMapper.insertArticle(articlePO);
ArticleDetailPO articleDetailPO = new ArticleDetailPO();
articleDetailPO.setArticleId(articlePO.getId());
articleDetailPO.setContent(article.getContent());
articleMapper.insertArticleDetail(articleDetailPO);
return true;
}
@Transactional(rollbackFor = Exception.class)
public boolean modifyArticle(ArticleParam param){
ArticlePO articlePO = new ArticlePO();
articlePO.setId(param.getId());
articlePO.setTitle(param.getTitle());
articlePO.setSummary(param.getSummary());
int editArticle = articleMapper.updateArticle(articlePO);
ArticleDetailPO detailPO = new ArticleDetailPO();
detailPO.setArticleId(param.getId());
detailPO.setContent(param.getContent());
int editDetail = articleMapper.updateArticleDetail(detailPO);
if( editArticle > 0 && editDetail > 0 ){
return true;
}
return false;
}
@Transactional(rollbackFor = Exception.class)
@Override
public int removeArticle(List<Integer> ids) {
int master = articleMapper.deleteArticle(ids);
int detail = articleMapper.deleteArticleDetail(ids);
return master;
}
@Override
public ArticleDTO queryByArticleId(Integer id) {
return articleMapper.selectArticleAndDetail(id);
}
@Override
public String queryTop20Detail(Integer id) {
ArticleDetailPO articleDetailPO = articleMapper.selectDetailByArticleId(id);
String content = articleDetailPO.getContent();
if(StringUtils.hasText(content)){
content = content.substring(0, content.length() >=20 ? 20 : content.length());
}
return content;
}
}
  • controller/ArticleController.java
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
@RequiredArgsConstructor
@Controller
public class ArticleController {
private final ArticleService articleService;
@GetMapping( value = {"/", "/article/hot"})
public String showHotArticle(Model model){
List<ArticlePO> articlePOList = articleService.queryTopAritcle();
//转为VO
List<ArticleVO> articleVOList = BeanUtil.copyToList(articlePOList,
ArticleVO.class);
//存储数据
model.addAttribute("articleList", articleVOList);
//视图
return "/blog/articleList";
}
//添加文章
@PostMapping("/article/add")
public String addArticle(@Validated(ArticleParam.AddArticle.class) ArticleParam
param){
ArticleDTO article = new ArticleDTO();
article.setTitle(param.getTitle());
article.setSummary(param.getSummary());
article.setContent(param.getContent());
boolean add = articleService.addArticle(article);
return "redirect:/article/hot";
}
//查询文章
@GetMapping("/article/get")
public String queryById(Integer id, Model model){
ArticleDTO articleDTO = articleService.queryByArticleId(id);
ArticleVO articleVO = BeanUtil.copyProperties(articleDTO, ArticleVO.class);
model.addAttribute("article",articleVO);
return "/blog/editArticle";
}
//修改文章
@PostMapping("/article/edit")
public String modifyArticle(@Validated(ArticleParam.EditArticle.class)
ArticleParam param){
boolean edit = articleService.modifyArticle(param);
return "redirect:/article/hot";
}
//删除文章
@PostMapping("/article/removeArticle")
public String removeArticle(@RequestParam("ids") IdType idType){
System.out.println("ids="+idType);
if(idType.getIdList() == null){
throw new IdNullException("Id 为null");
}
articleService.removeArticle(idType.getIdList());
return "redirect:/article/hot";
}
//查询文章开始的20 个字
@GetMapping("/article/detail/overview")
@ResponseBody
public String queryDetail(Integer id){
String top20Content = articleService.queryTop20Detail(id);
return top20Content;
}
}
  • exception/IdNullException.java
1
2
3
4
5
6
7
8
public class IdNullException extends BlogRootException{
public IdNullException() {
super();
}
public IdNullException(String message) {
super(message);
}
}
  • exception/BlogRootException.java
1
2
3
4
5
6
7
8
public class BlogRootException extends RuntimeException{
public BlogRootException() {
super();
}
public BlogRootException(String message) {
super(message);
}
}
  • exception/GlobalHandleException.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@ControllerAdvice
public class GlobalHandleException {
@ExceptionHandler( BindException.class)
public String handlerBindException(BindException bindException, Model model){
BindingResult result = bindException.getBindingResult();
if( result.hasErrors()){
model.addAttribute("errors",result.getFieldErrors());
System.out.println("result.getFieldErrors()="+result.getFieldErrors().size());
}
return "/blog/error/bind";
}
@ExceptionHandler( Exception.class)
public String handlerDefaultException(Exception exception, Model model){
model.addAttribute("msg","请稍后重试!!!");
return "/blog/error/error";
}
}
  • formattter/IdType.java
1
2
3
4
@Data
public class IdType {
private List<Integer> idList;
}
  • formatter/IdTypeFormatter.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class IdTypeFormatter implements Formatter<IdType> {
@Override
public IdType parse(String text, Locale locale) throws ParseException {
IdType idType = new IdType();
if(StringUtils.hasText(text)){
List<Integer> ids = new ArrayList<>();
for (String id : text.split(",")) {
ids.add(Integer.parseInt(id));
}
idType.setIdList(ids);
}
return idType;
}
@Override
public String print(IdType object, Locale locale) {
return null;
}
}
  • settings/ArticleSettings.java
1
2
3
4
5
6
@Data
@ConfigurationProperties(prefix = "article")
public class ArticleSettings {
private Integer lowRead;
private Integer topRead;
}
  • settings/WebMvcSettings.java
1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class WebMvcSettings implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new IdTypeFormatter());
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/view/addArticle").setViewName("/blog/addArticle");
}
}
  • 启动类
1
2
3
4
5
6
7
8
@MapperScan(basePackages = { "top.qianqianzyk.blog.mapper" })
@EnableConfigurationProperties( {ArticleSettings.class} )
@SpringBootApplication
public class AdminApplication {
public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
}
}

了解AOT 和GraalVM

提供性能的技术

什么是AOT:
Ahead-of-Time Compilation :预编译(提前编译)它在JEP-295 中描述,并在Java 9 中作为实验性功能添加
AOT 是提升Java 程序性能的一种方法,特别是提供JVM 的启动时间。在启动虚拟机之前,将Java 类编译为本机代码。改进小型和大型Java 应用程序的启动时间
JIT (just in time):
JIT 是现在JVM 提高执行速度的技术,JVM 执行Java 字节码,并将经常执行的代码编译为本机代码。这称为实时(JIT) 编译

当JVM 发现某个方法或代码块运行特别频繁的时候,就会认为这是“热点代码”(Hot Spot Code)。然后JIT会把“热点代码”编译成本地机器相关的机器码,并进行优化,然后再把编译后的机器码缓存起来,以备下次使用
JVM 根据执行期间收集的分析信息决定JIT 编译哪些代码。JIT 编译器速度很快,但是Java 程序非常大,以至于JIT 需要很长时间才能完全预热。不经常使用的Java 方法可能根本不会被编译
特点:在程序执行时,边运行代码边编译。JIT 编译需要时间开销,空间开销,只有对执行频繁的代码才值得编译

  • AOT:静态的
  • JIT:动态的
Native Image

native image: 原生镜像(本机映像)
native image is a technology to ahead-of-time compile Java code to a standalone executable, called a native image 本机映像是一种预先将Java 代码编译为独立可执行文件的技术,称为本机映像(原生镜像)。镜像是用于执行的文件
通过镜像构建技术(工具)生成镜像文件(native image)
native image 既是技术的名词也指他的生成的可执行文件
native image 支持基于jvm 的语言,例如Java, Scala, Clojure, Kotlin
原生镜像文件内容包括应用程序类、来自其依赖项的类、运行时库类和来自JDK 的静态链接本机代码(二进制文件可以直接运行,不需要额外安装JDK),本机映像运行在GraalVM 上,具有更快的启动时间和更低的运行时内存开销
在AOT 模式下,编译器在构建项目期间执行所有编译工作,这里的主要想法是将所有的”繁重工作”–昂贵的计算–转移到构建时间。也就是把项目都要执行的所有东西都准备好,具体执行的类,文件等。最后执行这个准备好的文件,此时应用能够快速启动。减少内存,cpu 开销(无需运行时的JIT 的编译)。因为所有东西都是预先计算和预先编译好的

Native Image builder

Native Image builder(镜像构建器):是一个实用程序,用于处理应用程序的所有类及其依赖项,包括来自JDK 的类。它静态地分析这些数据,以确定在应用程序执行期间可以访问哪些类和方法。然后,它预先将可到达的代码和数据编译为特定操作系统和体系结构的本机可执行文件

GraalVM

GraalVM 是一个高性能JDK 发行版,旨在加速用Java 和其他JVM 语言编写的应用程序,同时支持JavaScript、Ruby、Python 和许多其他流行语言。GraalVM 的多语言功能可以在单个应用程序中混合多种编程语言,同时消除外语调用成本。GraalVM 是支持多语言的虚拟机
GraalVM 是OpenJDK 的替代方案,包含一个名为native image 的工具,支持预先(ahead-of-time,AOT)编译。GraalVM 执行native image 文件启动速度更快,使用的CPU 和内存更少,并且磁盘大小更小。这使得Java在云中更具竞争力
目前,AOT 的重点是允许使用GraalVM 将Spring 应用程序部署为本机映像。Spring Boot 3 中使用GraalVM方案提供Native Image 支持
GraalVM 的”native image “工具将Java 字节码作为输入,输出一个本地可执行文件。为了做到这一点,该工具对字节码进行静态分析。在分析过程中,该工具寻找你的应用程序实际使用的所有代码,并消除一切不必要的东西。native image 是封闭式的静态分析和编译,不支持class 的动态加载,程序运行所需要的多有依赖项均在静态分析阶段完成

AOT 的操作步骤

使用SpringBoot 或者Spring 创建项目, GraalVM 的native image build 生成原生镜像文件,在GraalVM 上执行镜像文件

感谢

  1. Spring Boot
  2. 动力节点授课视频