0%

yaml语法介绍

一、基本语法

key:(空格)value,表示一个键值对(空格不能省略)

以空格的缩进来控制层级关系(有的类似python);只要是左对齐的一列数据,都是同一个层级的,其中属性和值也是大小写敏感;

1
2
3
server:
port: 8081
path: /hello

其中port: 8081就表示一个键值对,而port、path就是同一级

二、值的写法

1.普通类型(数字、字符串、布尔)

直接使用k: v就行

1
2
3
name: "张三"
age: 20
isMarried: false

需要主要的地方:

字符串默认不用加上单引号或者双引号,也可以加上,但是单引号和双引号会有一些小区别,

“”:双引号;不会转义字符串里面的特殊字符;特殊字符会作为本身想表示的意思 name: “zhangsan \n lisi”:输出;zhangsan 换行 lisi ‘’

:单引号;会转义特殊字符,特殊字符最终只是一个普通的字符串数据 name: ‘zhangsan \n lisi’:输出;zhangsan \n lisi

2.对象、Map类型(键值对)

k: v:在下一行来写对象的属性和值的关系;注意缩进 对象还是k: v的方式。

1
2
3
friends:
lastName: zhangsan
age: 20

行内写法

1
friends: {lastName: zhangsan,age: 18}

3.数组(List,Set)

用 “- 值” 表示数组中的一个元素

1
2
3
4
pets:
cat
dog
pig

行内写法

1
pets: [cat,dog,pig]

4.占位符

当yml做springboot中配置文件的时候还可以使用一些随机数和占位符

随机数:

1
2
${random.value}、${random.int}、${random.long}
${random.int(10)}、${random.int[1024,65536]}

占位符:

1
2
3
4
5
6
7
8
9
10
11
12
13
person:
lastName: 张三${random.uuid}
age: ${random.int}
birth: 2020/5/1
boss: false
maps: {k1: v1,k2: v2}
lists:
- lisi
- zhaoliu

dog:
name: ${person.lastName:hello}小狗
age: 2

${person.lastName:hello}表示使用前面person的lastName值,如果lastName没有默认为hello,当然其实:hello也可以不写。当properties文件做springboot的配置文件时也可以使用这些

总结

yaml是以数据为中心来代替xml的,在springboot中主要用yaml来进行配置,学习一下。

sprigboot中配置文件值注入

介绍

在springboot当中有时候我们会写一些bean和配置文件进行映射,那么我们如何将配置文件当中的值注入到bean当中去呢?一般我们会使用@ConfigurationProperties进行相应的注入

讲解

1.使用@ConfigurationProperties

写一个Person和dog类

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
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
private String lastName;
private Integer age;
private Boolean boss;
private Date birth;

private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;

@Override
public String toString() {
return "Person{" +
"lastName='" + lastName + '\'' +
", age=" + age +
", boss=" + boss +
", birth=" + birth +
", maps=" + maps +
", lists=" + lists +
", dog=" + dog +
'}';
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public Boolean getBoss() {
return boss;
}

public void setBoss(Boolean boss) {
this.boss = boss;
}

public Date getBirth() {
return birth;
}

public void setBirth(Date birth) {
this.birth = birth;
}

public Map<String, Object> getMaps() {
return maps;
}

public void setMaps(Map<String, Object> maps) {
this.maps = maps;
}

public List<Object> getLists() {
return lists;
}

public void setLists(List<Object> lists) {
this.lists = lists;
}

public Dog getDog() {
return dog;
}

public void setDog(Dog dog) {
this.dog = dog;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Dog {
private String name;
private Integer age;

@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

public String getName() {
return name;
}

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

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}
}

然后在application.yml配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
person:
lastName: 张三
age: 18
birth: 2020/5/1
boss: false
maps: {k1: v1,k2: v2}
lists:
- lisi
- zhaoliu

dog:
name: 小狗
age: 2


在springboot得测试当中:

1
2
3
4
5
6
7
8
9
10
11
@SpringBootTest
class SpringBoot02ConfigApplicationTests {

@Autowired
Person person;
@Test
void contextLoads() {
System.out.println(person);
}

}

最终输出:

1
Person{lastName='张三', age=18, boss=false, birth=Fri May 01 00:00:00 CST 2020, maps={k1=v1, k2=v2}, lists=[lisi, zhaoliu], dog=Dog{name='小狗', age=2}}

可以是可以进行配置得注入的

需要注意的点:

  • 需要导入依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring‐boot‐configuration‐processor</artifactId>
    <optional>true</optional>
    </dependency>

    我在pow.xml当中在这样导入并且没有成功,但是看网上都是这样导入的,所以记一下。可能因为我目前最新版的springboot(2.26版)出现了这样的问题,我直接从maven中央仓库找到这个包,然后使用了他给的导入方法

    1
    2
    3
    4
    5
    6
    <!--        导入依赖-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <version>2.2.6.RELEASE</version>
    </dependency>

    版本需要对应一下

  • 我的主类是SpringBoot02ConfigApplication,所有对应的测试类是SpringBoot02ConfigApplicationTests,是自己生成的,不需要我们自己创建这个类

  • 如果是在application.properties文件进行配置也可以达到相同效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    person.last-name=张三
    person.age=18
    person.birth=2020/5/1
    person.boss=false
    person.maps.k1=v1
    person.maps.k2=v2
    person.lists=lisi,zhaoliu
    person.dog.name=小狗
    person.dog.age=2

    如果出现乱码只需要在settinfgs里面搜索File Encoding,然后,然后将Properties的设置改成下面一样就行

    sprigboot中配置文件值注入01

2.和@Value的区别

@Value也可以给bean文件中注入值,具体的用法类似于这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Person {
/**
* <bean class="Person">
* <property name="lastName" value="字面量/${key}从环境变量、配置文件中获取值/#
{SpEL}"></property>
* <bean/>
*/
//@Value("${person.last‐name}")
private String lastName;
//@Value("#{11*2}")
private Integer age;
//@Value("true")
private Boolean boss;

但是@Value@ConfigurationProperties相比功能相对来说弱一些,有很多区别

@ConfigurationProperties @Value
功能上 可以批量注入配置文件当中的属性 可以注入配置文件的值或者其他的值,但是需要一个一个指定
松散语法 支持 不支持
SpEL 不支持 支持
JSR303数据校验 支持 不支持
复杂类型封装 支持 不支持

解释:

  • 松散语法就是类似lastName、last-name、last_name这种命名的方法。简单说就是@ConfigurationProperties中属性命名这些方法都可以
  • JSR303数据校验是指可以通过在类上面加@Validated,来进行属性值的验证

总结

继续学习springboot当中,加油

springboot中@PropertySource、@ImportResource、@Bean

介绍

在上一篇中学习了@ConfigurationProperties,演示的时候是直接在application.yml中进行相对应的配置,但是如果映射对的话,都写在这里面也不太合适,springboot当中也就为我们提供了一些方法解决这个问题

讲解

1.@PropertySource

@PropertySource的作用是加载指定的配置文件

我们可以将person的内容抽出来写在person.properties当中,就是resources文件夹下建立一个person.properties文件,写上如下内容

1
2
3
4
5
6
7
8
9
person.last-name=张三
person.age=18
person.birth=2020/5/1
person.boss=false
person.maps.k1=v1
person.maps.k2=v2
person.lists=lisi,zhaoliu
person.dog.name=小狗
person.dog.age=2

然后在person类的开头加上@PropertySource(value = {"classpath:person.properties"})

1
2
3
4
5
6
@PropertySource(value = {"classpath:person.properties"})
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
...
}

发现可以从person.properties当中读取对对应的配置,然后进行注入(同理yml也可以)

结论:

@PropertySource的作用是加载指定的配置文件,properties、yml文件都可以进行读取,而@ConfigurationProperties默认是从全局配置文件中获取值。

2.@ImportResource

上面的内容是导入springboot相关的配置文件,如果是导入spring的配置文件将如何处理呢?

Spring Boot里面没有Spring的配置文件,我们自己编写的配置文件,也不能自动识别; 想让Spring的配置文件生效,加载进来,可以使用@ImportResource标注在一个配置类上,导入Spring的配置文件,让配置文件里面的内容生效。

建立一个service:

1
2
public class HelloService {
}

在resources目录下建立一个beans.xml配置文件,并且对helloservice进行配置

1
2
3
4
5
6
<?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="helloService" class="com.zhouning.springboot02config.service.HelloService"></bean>
</beans>

最后在主类上加上@ImportResource

1
2
3
4
5
6
7
8
9
10

@ImportResource(locations = {"classpath:beans.xml"})
@SpringBootApplication
public class SpringBoot02ConfigApplication {

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

}

测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootTest
class SpringBoot02ConfigApplicationTests {

@Autowired
ApplicationContext applicationContext;

@Test
void contextLoads() {
//判断ioc容器中是否有helloService
System.out.println(applicationContext.containsBean("helloService"));
}
}

输出:true

3.@Bean

上面那种创建bean的方式并不是spirngboot推荐的方法,springboot推荐的方法为使用全注解的方式,使用@Configuration@Bean的方式进行配置

我们建立一个MyConfig类:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class MyConfig {
/***
* 将方法的返回值添加到容器中,容器中这个组件默认的id就是方法名
* @return
*/
@Bean
public HelloService helloService(){
return new HelloService();
}
}

然后将主类上面的@ImportResource注释之后,再运行测试发现输出也是true

结论:

@Configuration指明当前类是一个配置类;就是来替代之前的Spring配置文件

@Bean给容器中添加组件,可以使用在方法上,当使用在方法上时,作用是将方法的返回值添加到容器中,容器中这个组件默认的id就是方法名,类似于上面默认的id就是helloService

总结

学习了一下@PropertySource、@ImportResource、@Bean这三个注解的用法

Profile多环境支持

介绍

Profile是Spring对不同环境提供不同配置功能的支持,可以通过激活、 指定参数等方式快速切换环境,比如有的时候我们希望spring开发时在一个环境、测试时又在一个环境,生产的时候又一个环境,这个都可以通过Profile实现

讲解

1.多Profile文件实现

我们在主配置文件编写的时候,文件名可以是是 application-{profile}.properties或者是 application-{profile}.yml,然后实现不同环境的配置,默认使用用application.properties配置。

例如我们在resources文件夹下再创建一个application-dev.properties和一个application-prod.properties表示开发环境和生产环境,而自身的application.properties就当作普通环境

Profile多环境支持01

application.properties:

1
server.port=8081

application-dev.properties:

1
server.port=8082

application-prod.properties:

1
server.port=8083

这时我们运行程序你,默认的就是8081端口,如果我们想改变环境只需要在application.properties中加一句话激活一个环境,配置就改变了

1
2
server.port=8081
spring.profiles.active=dev

2.yml多文档块方式

上面那种有时候觉得文件太多有的不方便,那么可以使用一个yml文件,使用多文档块的方式进行环境编写

例如我们在application.yml中编写如下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server:
port: 8081
spring:
profiles:
active: prod
---
server:
port: 8082
spring:
profiles: dev
---
server:
port: 8083
spring:
profiles: prod

我们这样就激活了prod环境,启动后端口为8083

3.激活指定的profile

上面的展示了profile的一些写法,下面写一些proflie的激活方法

  • 在配置文件中指定 spring.profiles.active=dev

  • 命令行激活:java -jar xxxx.jar —spring.profiles.active=dev

    这个命令行方法可以在打包之后进行环境激活

总结

学习了一下springboot中多环境激活的内容

springboot配置文件加载

介绍

springboot当中配置文件又很多,有一定的访问顺序,做个记载记录一下访问顺序

讲解

一、形成jar包之前的访问顺序

springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件,他们的访问顺序的优先级由高到低如下所示:

  1. file:/config/(项目目录下的config文件夹下)
  2. file:/(项目目录下的)
  3. classpath:/config/(类路径的config文件夹下)
  4. classpath:/(类路径下)

配置文件加载01

​ 这里优先级的体现是当在file:/config/下创建一个application.properties文件时,里面写上server.port=8084而在classpath:/下的application.properties文件里面写下server.port=8081,项目会以端口为8084启动,就是说优先级的内容会覆盖优先级低的内容。

​ 但是这并不代表低优先级的配置文件不起作用,低优先级的的内容也起作用,比如说要是低优先级里面有个server.context-path=/abc内容而高优先级没有,那么这个context的内容将起作用,这样可以高优先级和低优先级的配置文件一起使用形成互补配置。

​ 然而当项目打包成jar包之后,file:/config/(项目目录下的config文件夹下)和file:/(项目目录下的)并不会被打包进去,所以也不会起到配置作用,有时候感觉有些鸡肋

二、形成jar包之后的加载顺序

SpringBoot也可以从以下位置加载配置, 优先级从高到低,高优先级的配置覆盖低优先级的配置,所有的配置会 形成互补配置

  1. 命令行参数

    所有的配置都可以在命令行上进行指定 java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar —server.port=8087 —server.context-path=/abc

    但是这样如果配置多了有些麻烦。改改端口什么的还可以

  2. jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置文件

  3. jar包内部的application-{profile}.properties或application.yml(带spring.profile)配置文件

  4. .jar包外部的application.properties或application.yml(不带spring.profile)配置文件

  5. jar包内部的application.properties或application.yml(不带spring.profile)配置文件

    上面的2,3,4,5的理解是如果jar外有相对应的配置文件会先读jar包外的配置文件,然后再加载已经打包进jar包的配置文件

配置文件加载02

​ 这个就会先加载jar包外部的application.properties文件的配置再加载jar里面打包的配置,形成互补配置,这也就解决了打包后更改配置的问题,方便了springboot在打包后再进行配置。

总结

配置的访问顺序还是比较简单的内容,其实还有更多的配置的讲解可以参考官方文档:传送门

springboot日志使用

简介

SpringBoot能自动适配所有的日志,但是底层默认使用slf4j+logback的方式记录日志,一般来说遵从默认的日记就可以了。springboot日志的官方文档在这里传送门

讲解

1.代码使用方法

在测试类中写下日下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@SpringBootTest
class SpringBoot03LoggingApplicationTests {

Logger logger = (Logger) LoggerFactory.getLogger(getClass());
@Test
void contextLoads() {
//日志级别由低到高
logger.trace("这是trace日志");
logger.debug("这是debug日志");
//默认是info级别
logger.info("这是info日志");
logger.warn("这是warn日志");
logger.error("这是error日志");
}

}

可以在控制台看到输出

1
2
3
2020-05-07 12:10:49.845  INFO 20044 --- [           main] c.z.SpringBoot03LoggingApplicationTests  : 这是info日志
2020-05-07 12:10:49.846 WARN 20044 --- [ main] c.z.SpringBoot03LoggingApplicationTests : 这是warn日志
2020-05-07 12:10:49.846 ERROR 20044 --- [ main] c.z.SpringBoot03LoggingApplicationTests : 这是error日志

可以知道默认的级别是info级别,要是想调整日志级别可以在application.properties中进行设置,如:

1
logging.level.com.zhouning=trace

将com.zhouning这个包下的级别调整成trace

2.logging.file 和logging.path

logging.file和logging.path都g是g设置日志文件生成路径

  • logging.file

    我们在application.properties写下:

    1
    logging.file.name=springboot.log

    运行后发现当前项目下生成了一个springboot.log文件,里面写着日志的内容

    springboot日志01

    当然logging.file也可以指定具体的位置如:

    1
    logging.file=E:/springboot.log
  • loggin.path

    loggin.path是指定日志目录,如我们将当前磁盘的根路径下spring文件夹中的log文件夹作为存放日志文件的地方

    1
    2
    # 在当前磁盘的根路径下创建spring文件夹和里面的log文件夹;使用 spring.log 作为默认文件
    logging.path=/spring/log

当两者都指定时,会以logging.file为主,但是一般情况下我们使用logging.path就足够了

3.logging.pattern.console和logging.pattern.file

  • logging.pattern.console设置在控制台的日志格式

    1
    logging.pattern.console = %d{yyyy‐MM‐dd} [%thread]  %logger{50} ‐ %msg %n

    输出结果:

    springboot日志02

  • logging.pattern.file设置文件中日志输出格式

    1
    logging.pattern.file = %d{yyyy‐MM‐dd} === [%thread] === %logger{50} ==== %msg %n

    输出结果:

    springboot日志03

简单解释:

1
2
3
4
5
6
7
日志输出格式:
%d表示日期时间,
%thread表示线程名,
%-5level:级别从左显示5个字符宽度
%logger{50} 表示logger名字最长50个字符,否则按照句点分割。
%msg:日志消息,
%n是换行符

4.指定日志配置

日志的配置也可以写成一个配置文件,然后让springboot进行识别在配置,官方文档上配置上配置文件是这样描述的:

Logging System Customization
Logback logback-spring.xml, logback-spring.groovy, logback.xml, or logback.groovy
Log4j2 log4j2-spring.xml or log4j2.xml
JDK (Java Util Logging) logging.properties

简单的说我们使用logback作为日志框架,我们编写logback.xml、logback-spring.xml等就可以对Logback进行配置。

但是,一般来说建议使用logback-spring.xml作为配置文件,因为logback-spring.xml可以使用spring boot的的高级Profile功能,就是说在对应的profile环境下不同的日志配置被激活,profile的简单讲解可以看这个

使用的方法可以这样

1
2
3
4
5
6
7
8
9
<springProfile name="dev">
<pattern>%d{yyyy‐MM‐dd HH:mm:ss.SSS} ‐‐‐‐> [%thread] ‐‐‐> %‐5level
%logger{50} ‐ %msg%n</pattern>
</springProfile>
<springProfile name="!dev">
<pattern>%d{yyyy‐MM‐dd HH:mm:ss.SSS} ==== [%thread] ==== %‐5level
%logger{50} ‐ %msg%n</pattern>
</springProfile>

在dev环境下一种输出方式,不在dev环境下另外一种输出方式

总结

写了一下springboot日志相关的内容,其实日志还有许多东西,比如切换日志框架等,但是我觉得目前这个不是很重要所有还是写一下比较实用的东西。

thymeleaf语法简单记录

简介

学习springboot的时候学习了一下thymeleaf的语法,所以拿来记录一下啊,主要参考了官方的文档

学习语法规则

thymeleaf在使用的时候我们首先需要在开头导入名称空间

1
<html lang="en" xmlns:th="http://www.thymeleaf.org">

下面这张表是在thymeleaf官方文档里面所找到的,thymeleaf的属性优先级,也是类似于所有属性的大纲,所有写在开头

Order Feature Attributes
1 Fragment inclusion th:insert th:replace(片段包含)
2 Fragment iteration th:each(遍历)
3 Conditional evaluation th:if th:unless th:switch th:case(判断)
4 Local variable definition th:object th:with(声明变量)
5 General attribute modification th:attr th:attrprepend th:attrappend(属性修改)
6 Specific attribute modification th:value th:href th:src ...(修改指定属性)
7 Text (tag body modification) th:text th:utext(修改标签体的内容,其中utext为不转义)
8 Fragment specification th:fragment(声明片段)
9 Fragment removal th:remove

1.表达式

thymeleaf的表达式主要有5个,如下所示

  • Variable Expressions: ${...}

    变量表达式应该是使用的最广的表达式,主要使用是三个方法

    • 获取对象的属性、调用方法

      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
      /*
      * Access to properties using the point (.). Equivalent to calling property getters.通过(.)来获取属性
      */
      ${person.father.name}

      /*
      * Access to properties can also be made by using brackets ([]) and writing
      * the name of the property as a variable or between single quotes.也可以通过[]进行访问
      */
      ${person['father']['name']}

      /*
      * If the object is a map, both dot and bracket syntax will be equivalent to
      * executing a call on its get(...) method.map类型也支持
      */
      ${countriesByCode.ES}
      ${personsByName['Stephen Zucchini'].age}

      /*
      * Indexed access to arrays or collections is also performed with brackets,
      * writing the index without quotes.数组类型
      */
      ${personsArray[0].name}

      /*
      * Methods can be called, even with arguments.也可以掉用方法
      */
      ${person.createCompleteName()}
      ${person.createCompleteNameWithSeparator('-')}
    • 使用内置的基本对象

      支持内置的基本对象的一些使用,如:上下文、上下文变量但是得使用#符号开头进行引用

      1
      2
      3
      4
      5
      6
      7
      #ctx: the context object.
      #vars: the context variables.
      #locale: the context locale.
      #request: (only in Web Contexts) the HttpServletRequest object.
      #response: (only in Web Contexts) the HttpServletResponse object.
      #session: (only in Web Contexts) the HttpSession object.
      #servletContext: (only in Web Contexts) the ServletContext object.

      例子:查看国家

      1
      Established locale country: <span th:text="${#locale.country}">US</span>.

      更多情况可以看附录

    • 使用表达工具对象

      内置的工具和上面的基本对象用法一样,使用#符号开头进行引用,可以看到这些都是一些方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      #execInfo: information about the template being processed.
      #messages: methods for obtaining externalized messages inside variables expressions, in the same way as they would be obtained using #{…} syntax.
      #uris: methods for escaping parts of URLs/URIs
      #conversions: methods for executing the configured conversion service (if any).
      #dates: methods for java.util.Date objects: formatting, component extraction, etc.
      #calendars: analogous to #dates, but for java.util.Calendar objects.
      #numbers: methods for formatting numeric objects.
      #strings: methods for String objects: contains, startsWith, prepending/appending, etc.
      #objects: methods for objects in general.
      #bools: methods for boolean evaluation.
      #arrays: methods for arrays.
      #lists: methods for lists.
      #sets: methods for sets.
      #maps: methods for maps.
      #aggregates: methods for creating aggregates on arrays or collections.
      #ids: methods for dealing with id attributes that might be repeated (for example, as a result of an iteration).

      例子:进行日期格式化

      1
      2
      3
      <p>
      Today is: <span th:text="${#calendars.format(today,'dd MMMM yyyy')}">13 May 2011</span>
      </p>

      更多情况可以看附录

  • Selection Variable Expressions: *{...}

    选择表达式功能上和其实和变量表达式一样,唯一的区别是星号语法*{...}

    在选定对象后,星号语法代表的是对象,而不是整个上下文上评估表达式,也就是说,只要没有选定的对象,美元和星号的语法就完全一样。那什么是选定对象呢,看下面一个例子:

    选择对象就是使用th:object创造一个变量

    1
    2
    3
    4
    5
    <div th:object="${session.user}">
    <p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
    <p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
    <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
    </div>

    上面的用法和下面这种只用${...}是相同的

    1
    2
    3
    4
    5
    <div>
    <p>Name: <span th:text="${session.user.firstName}">Sebastian</span>.</p>
    <p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
    <p>Nationality: <span th:text="${session.user.nationality}">Saturn</span>.</p>
    </div>

    当然和上面说的一样,如果没有选定对象*{...}${...}相同,也可以这样

    1
    2
    3
    4
    5
    <div>
    <p>Name: <span th:text="*{session.user.name}">Sebastian</span>.</p>
    <p>Surname: <span th:text="*{session.user.surname}">Pepper</span>.</p>
    <p>Nationality: <span th:text="*{session.user.nationality}">Saturn</span>.</p>
    </div>

    并且选择对象后,选定的对象也可以作为#object表达式变量用于${...}

    1
    2
    3
    4
    5
    <div th:object="${session.user}">
    <p>Name: <span th:text="${#object.firstName}">Sebastian</span>.</p>
    <p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
    <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
    </div>
  • Message Expressions: #{...}

    消息表达式多用于国际化的时候使用,这里暂时留个坑用到具体的例子再填

  • Link URL Expressions: @{...}

    链接表达式就是对html当中的链接进行替换使用的,需要使用th:href进行赋值,直接看例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- Will produce 'http://localhost:8080/gtvg/order/details?orderId=3' (plus rewriting) -->
    <a href="details.html"
    th:href="@{http://localhost:8080/gtvg/order/details(orderId=${o.id})}">view</a>

    <!-- Will produce '/gtvg/order/details?orderId=3' (plus rewriting) -->
    <a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a>

    <!-- Will produce '/gtvg/order/3/details' (plus rewriting) -->
    <a href="details.html" th:href="@{/order/{orderId}/details(orderId=${o.id})}">view</a>

    从上面的例子可以看出th:href主要替换<a>标签当中的href属性,并且在表达式里面可以使用另一个表达式的计算结果。另外可以使用其他语法来创建相对于服务器根目录的URL(而不是上下文根目录的URL),以便链接到同一服务器中的不同上下文。这些网址将指定为@{~/path/to/something}

  • Fragment Expressions: ~{...}

    这个后面填坑

表达式是大致为五种,表达式里面的值和运算也可以是多种多样的也有几种,如下:

  • Literals

    字面量,可以是字符串、数字、布尔类型

    • Text literals: 'one text', 'Another one!',…
    • Number literals: 0, 34, 3.0, 12.3,…
    • Boolean literals: true, false
    • Null literal: null
    • Literal tokens: one, sometext, main,…
    1
    2
    <p>The year is <span th:text="2013">1492</span>.</p>
    <p>In two years, it will be <span th:text="2013 + 2">1494</span>.</p>
  • Text operations:

    文本操作

    • String concatenation: +

      1
      <span th:text="'The name of the user is ' + ${user.name}">
    • Literal substitutions: |The name is ${name}|

      这个Literal substitutions允许很简单格式化包含变量值的字符串,而无需在文本后加上’…’+’…’

      1
      <span th:text="|Welcome to our application, ${user.name}!|">

      等同于

      1
      <span th:text="'Welcome to our application, ' + ${user.name} + '!'">

      并且只有${...}, *{...}, #{...}才能使用|...|这种用法

  • Arithmetic operations:

    数学运算

    • Binary operators: +, -, *, /, %
    • Minus sign (unary operator): -
    1
    <div th:with="isEven=(${prodStat.count} % 2 == 0)">
  • Boolean operations:

    布尔运算

    • Binary operators: and, or
    • Boolean negation (unary operator): !, not
  • Comparisons and equality:

    比较运算

    • Comparators: >, <, >=, <= (gt, lt, ge, le)
    • Equality operators: ==, != (eq, ne)
  • Conditional operators:

    条件运算

    • If-then: (if) ? (then)
    • If-then-else: (if) ? (then) : (else)
    • Default: (value) ?: (defaultvalue)
    1
    2
    3
    <tr th:class="${row.even}? 'even' : 'odd'">
    ...
    </tr>

2.Fragment inclusion和Fragment specification

Fragment inclusion和Fragment specification 实现了一个类似模板的功能。在我们的模板中,我们经常希望包含其他模板中的部分,例如页脚,页眉,菜单等部分。 为此,Thymeleaf提供了这个功能,只需要我们定义这些要包含的部分“片段”即可,定义使用th:fragment属性来完成。

简单使用

简单的使用就是这样,我们定义一个footer.htrml

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">

<body>

<div th:fragment="copy">
&copy; 2011 The Good Thymes Virtual Grocery
</div>

</body>

</html>

在其他地方要使用的时候

1
2
3
4
5
<body>
...

<div th:insert="~{footer :: copy}"></div>
</body>

th:insertth:replaceth:include)的不同

  • th:insert insert是最简单的:它将简单地插入指定的片段作为其主机标签的主体
  • th:replace replace实际上将其主机标签替换为指定的片段
  • th:includeth:include与th:insert相似,但是它不会插入片段,而是仅插入其中的内容

比如说我们创建一个片段是这样

1
2
3
<footer th:fragment="copy">
&copy; 2011 The Good Thymes Virtual Grocery
</footer>

然后

1
2
3
4
5
6
7
8
9
<body>
...
<div th:insert="footer :: copy"></div>

<div th:replace="footer :: copy"></div>

<div th:include="footer :: copy"></div>

</body>

最终源码效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<body>
...
<div>
<footer>
&copy; 2011 The Good Thymes Virtual Grocery
</footer>
</div>

<footer>
&copy; 2011 The Good Thymes Virtual Grocery
</footer>

<div>
&copy; 2011 The Good Thymes Virtual Grocery
</div>

</body>

3.Fragment iteration

遍历使用th:each实现,直接看例子就可以懂了:

1
2
3
4
5
6
7
8
9
10
11
12
<table>
<tr>
<th>NAME</th>
<th>PRICE</th>
<th>IN STOCK</th>
</tr>
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
</table>

4.Conditional evaluation

有时需要模板的一部分才能仅在满足特定条件的情况下出现在结果中。 例如,假设我们要在产品表中显示一列,其中包含每个产品的评论数量,如果有评论,则指向该产品的评论详细信息页面的链接。 为了做到这一点,我们将使用th:if属性:

1
2
3
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>

当然使用th:unless替换

1
2
3
<a href="comments.html"
th:href="@{/comments(prodId=${prod.id})}"
th:unless="${#lists.isEmpty(prod.comments)}">view</a>

在thymeleaf当中还有类似于java里面swith..case的结构

1
2
3
4
5
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p>
<p th:case="#{roles.manager}">User is a manager</p>
<p th:case="*">User is some other thing</p>
</div>

这个th:case=*表示默认情况,需要注意的是当其中一个th:case被认定为true的时候,其他的就会被认定为false

5.General attribute modification和Specific attribute modification

这部分简单来说就是tymeleaf可以修改html中任意属性

举例:

1
2
3
4
5
6
<form action="subscribe.html" th:attr="action=@{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
</fieldset>
</form>

通过th:attr="value=#{subscribe.submit}"修改了value属性,除此之后也可以这样写

1
<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>

两者效果相同,就是说基本html的属性都可以使用th:attr="属性名=..."th:属性名=...进行修改

总结

目前学习到的就这么多,有后面再记一下

springboot中restfulCRUD例子

简介

在学习了springboot和web相关的内容时,学习了一下增删改查相关的讲解,所以自己也写一个增删改查的例子来记录一下,先说明为了简单本次例子中不含有数据库相关的操作,只是为了举例。

讲解

假设我们有一个学生类student需要进行展示,并且对其进行增删改查。

首先我们写一下对应的uri:

普通CRUD(uri来区分操作) RestfulCRUD
查询 getStudent student—get
添加 addStudent student—post
修改 updateStudent student/{id}—put
删除 deleteStudent student/{id}—delete

大致的过程:

实验功能 请求URI 请求方式
查询所有学生 students get
查询某个学生或者修改某个学生 student/{id} get
到添加界面 student get
添加学生 student post
修改学生 student put
删除学生 student/{id} delete

先展示一下写完后的目录情况

crud例子01

一、编写Student、StudentDao

student类为了简单就只有姓名和学号

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
package com.zhouning.entities;

/**
* 学生
*
* @author zhouning
*/
public class Student {
/**
* 姓名
*/
private String name;

/**
* 学号,学号作为标识,不能相同
*/
private Integer sno;

public Student() {}

public Student(String name, Integer sno) {
this.name = name;
this.sno = sno;
}

public String getName() {
return name;
}

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

public Integer getSno() {
return sno;
}

public void setSno(Integer sno) {
this.sno = sno;
}


@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", sno=" + sno +
'}';
}
}

StudentDao类就有数据里面的增删改查,因为没有和数据库发生交互,所以也是比较简单的

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
@Repository
public class StudentDao {
private static Map<Integer, Student> students = null;

static {
students = new HashMap<>();
students.put(101, new Student("张三",101));
students.put(102, new Student("李四",102));
students.put(103, new Student("王五",103));
students.put(104, new Student("赵六",104));
}

private static Integer initSno = 1006;
public void save(Student student){
if (student.getSno()==null){
student.setSno(initSno++);
}
students.put(student.getSno(), student);
}

public Collection<Student> getAll(){
return students.values();
}

public Student get(Integer sno){
return students.get(sno);
}

public void delete(Integer sno){
students.remove(sno);
}

}

二、展示界面list.html

list.html实现比较简单主要功能是将学生以及有一些按按钮或者超链接进行访问都展示出来,使用了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
32
33
34
35
36
37
38
39
40
41
42
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>所有学生</title>
</head>
<body>
<table border="1">
<!-- 头部-->
<thead>
<tr>
<th>学号</th>
<th>姓名</th>
<th>操作</th>
</tr>
</thead>

<!-- 身体-->
<tbody>
<tr th:each="student:${students}">
<td th:text="${student.getSno()}">row 1, cell 1</td>
<td th:text="${student.getName()}">row 1, cell 2</td>
<td>
<a th:href="@{/student/}+${student.getSno()}" >编辑</a>

<form action="test" th:action="@{/student/}+${student.getSno()}" method="post">
<input type="hidden" name="_method" value="delete">
<input type="submit" value="删除"></input>
</form>

</td>
</tr>
</tbody>
</table>

<h2><a href="/student" >添加员工</a></h2>

</body>



</html>

比较简单使用get请求得到所有的学生

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 查找所有人
* @param model
* @return
*/
@GetMapping("/students")
public String list(Model model){
Collection<Student> students = studentDao.getAll();
System.out.println(students);
//放在请求域中
model.addAttribute("students", students);
return "student/list";
}

访问的结果是这样:

crud例子02

可以看到有编辑、删除、添加员工几个操作

三、添加员工操作

可以看到lsit.html里面有一个<h2><a href="/student" >添加员工</a></h2>,就是说/student转到添加员工页面。所以我们编写一个添加add.html进行员工添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>编辑页面</title>
</head>
<body>
<form method="post" action="/student">
学号: <input type="number" name="sno" ><br>
姓名: <input type="text" name="name" ><br>
<input type="submit" value="提交">
</form>
</body>
</html>

对应的映射内容

1
2
3
4
5
6
7
8
/**
* 到添加页面
* @return
*/
@GetMapping("/student")
public String toAddPage(){
return "student/add";
}

效果:

crud例子03

当我们填好信息按下时,可以发送了一个post请求,将我们添加的东西发送过去,对应的映射处理

1
2
3
4
5
6
7
8
9
10
11
 /**
* 添加学生
* @param student
* @return
*/
@PostMapping("/student")
public String addStudent(Student student){
System.out.println(student);
studentDao.save(student);
return "redirect:/students";
}

这里进行重定向,到学生展示页面,效果:

crud例子04

四、修改信息

我们可以看到list.html当中有这样的语句<a th:href="@{/student/}+${student.getSno()}" >编辑</a>,跳转到修改信息的页面。所以编写了一个update.html页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>编辑页面</title>
</head>
<body>
<form method="post" action="/student">
<input type="hidden" name="_method" value="put"/>
学号: <input type="number" name="sno" th:value="${student.getSno()}"><br>
姓名: <input type="text" name="name" th:value="${student.getName()}"><br>
<input type="submit" value="提交">
</form>
</body>
</html>

对应的映射

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 查找或者修改一个人
* @param sno
* @param model
* @return
*/
@GetMapping("/student/{sno}")
public String toUpdataPage(@PathVariable(value = "sno")Integer sno,Model model){
Student student = studentDao.get(sno);
model.addAttribute("student", student);
return "student/update";
}

并且我们会将学生的信息传过去,方便进行回显,比如我们点击zhouning那行的编辑,效果

crud例子05

这里我们按下提交按钮可以发送post请求,但是springboot会解析成put请求,因为<input type="hidden" name="_method" value="put"/>的原因,学过springmvc的应该知道,在springboot里面底层帮我们自动解析了。

对应的映射处理

1
2
3
4
5
6
7
8
9
10
11
/**
* 修改学生
* @param student
* @return
*/
@PutMapping("/student")
public String updateStudent(Student student){
System.out.println(student);
studentDao.save(student);
return "redirect:/students";
}

我们将zhouning的学号改成zn,不能修改学号因为学号唯一标识,并且代码里面可以看到修改学号就会产生加一个学生的效果,这里是举例,所以不深究,最终效果:

crud例子06

五、删除学生

我们看到list.html里面删除是这样的

1
2
3
4
<form action="test" th:action="@{/student/}+${student.getSno()}" method="post">
<input type="hidden" name="_method" value="delete">
<input type="submit" value="删除"></input>
</form>

经过上面的修改可以知道这是为了发送post请求解析成delete请求

对应的映射处理

1
2
3
4
5
6
@DeleteMapping("/student/{sno}")
public String deleteStudent(@PathVariable(value = "sno")Integer sno){
System.out.println(sno);
studentDao.delete(sno);
return "redirect:/students";
}

六、其他

在springboot2.xxx里面,将post解析成put和delete并不会自动配置,需要我们在application.properties里面写上这个

1
spring.mvc.hiddenmethod.filter.enabled=true

另外我还配置了一个configer,方便一开始就进入list.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.zhouning.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
* @author zhouning
*/
@Configuration
public class MyConfigurer implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("redirect:students");
registry.addViewController("/index").setViewName("redirect:students");
registry.addViewController("/main.html").setViewName("redirect:students");
}
}

总结

学习了springboot的crud自己写一下还是蛮不错的,源码在这里:源码传送门

springboot拦截器小例子

简介

拦截器顾名思义就是拦截一些请求达到我们想要的目的,在这里我写了一个简单的拦截器小例子记录一下。

讲解

假设我们有这样一个需求,需要系统在登录之后才能使用其他功能,如果没有登录就访问其他请求就强制返回登录页面。对于这个请求我们可以通过拦截器进行实现,拦截系统访问,判断是否已经登录,如果没有登录,则返回到登录界面。

1.登录功能编写

既然有登录那我们需要先编写登录

登录界面login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<form method="post" action="/login">
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
用户名: <input type="text" name="username" ><br>
密码: <input type="text" name="password" ><br>
<input type="submit" value="登录">
</form>
</body
</html>

登录对应的请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@PostMapping("/login")
public String login(@RequestParam(value = "username") String username,
@RequestParam(value = "password") String password,
Map<String,Object> map, HttpSession session){
if (!StringUtils.isEmpty(username)&&"123456".equals(password)){
//放在session当中,用于判断是否登录
sessionda.setAttribute("loginUser", username);
//登录成功,防止表单重复提交,进行重定向
return "redirect:/main.html";
}else {
map.put("msg", "用户名或者密码错误");
return "login";
}
}

@GetMapping("/hello")
@ResponseBody
public String hello(){
return "hello world";
}

这里我们假设密码为123456就通过。这时的效果是这样

但是此时如果我们没有登录成功一样可以访问后台的其他功能,比如:我们访问写好的:http://localhost:8080/hello,可以得到

这不是我们想要的结果

2.添加拦截器

这个时候我们可以添加一个拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LoginHandlerIntercep implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Object user = request.getSession().getAttribute("loginUser");
if (user==null){
//未登录,返回登录页面
request.setAttribute("msg", "请先登录");
request.getRequestDispatcher("/").forward(request, response);
return false;
}else {
//进行了登录
return true;
}
}
}

这个拦截器从Session当中获得我们登录的对象,进行判断,如果没有对象为null则返回登录界面。其中Session的对象是我们登录的时候放进去的,可以看到上面登录代码里面是有体现的。

光是编写拦截器并不够,我们需要将拦截器添加到进行配置当中,我采用的方法是自己编写一个配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
public class MyConfigurer implements WebMvcConfigurer {
/**
* 视图映射
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("login");
registry.addViewController("/index").setViewName("login");
registry.addViewController("/main.html").setViewName("redirect:students");
}

/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// addPathPatterns表示对请求进行拦截,excludePathPatterns表示除了()请求之外
registry.addInterceptor(new LoginHandlerIntercep()).addPathPatterns("/**").
excludePathPatterns("/index.html","/","/login");
}
}

可以看拦截中对于“/**”进行拦截,意思是对于所有请求进行拦截,而excludePathPatterns(“/index.html”,”/“,”/login”)则是除了除了主页面和登录请求之外,因为需要访问主页面和发送登录请求才能进行登录。总和起来就是除了主页面和登录请求之外,其他请求的欧进行拦截。

如果这时候我们还访问http://localhost:8080/hello,结果是这样

总结

拦截器内容比较简单,就写这么多后面学习到了更多再进行补充

springboot错误相应定制

介绍

在springboot中如果发生访问错误,如404的话,如果是浏览器访问它会给你一个默认的定制页面比如下面这样

错误页面定制01

如果是其他的,会返回json数据(来自idea插件RestfulToolkit):

错误页面定制02

那我们可以定制自己的错误页面吗,答案是肯定的,下面就讲解如何定制自己的错误页面已经信息

方法

一、定制错误页面

  1. 使用了模板引擎情况

    使用了模板引擎thymeleaf的情况下,我们只需要在resources的templates下创建error文件夹,在里面创建以状态码开头的html文件就行,比如:404.html就会对应到404的页面。除此之外,我们可以使用4xx和5xx作为错误页面的文件名来匹配这种类型的所有错误,但是精确优先(优先寻找精确的状态 码.html)。

    举例:

    我们在error文件夹下创建404.html和4xx.html

    错误页面定制03

    404.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <!DOCTYPE html>
    <html lang="en"xmlns:th="http://www.thymeleaf.org">
    <head>
    <meta charset="UTF-8">
    <title>404</title>
    </head>
    <body>
    <h1>status:[[${status}]]</h1>
    <h2>timestamp:[[${timestamp}]]</h2>
    </body>
    </html>

    4xx.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>4XX</title>
    </head>
    <body>
    <h2>status:[[${status}]]</h2>
    </body>
    </html>

    结果:

    404:

    错误页面定制04

    400错误:

    错误页面定制05

  2. 没有使用模板引擎

    没有使用模板引擎的话,直接在resources的static下创建error文件夹,然后在里面创建以状态码开头的html文件就行,4xx和5xx在这里面同样生效

二、定制错误json数据

为了比较好定制json错误,我们创建一个UserNotExistException

1
2
3
4
5
public class UserNotExistException extends RuntimeException{
public UserNotExistException() {
super("用户不存在");
}
}

然后在controller里面加上一个映射

1
2
3
4
@GetMapping("/exception")
public String toException(){
throw new UserNotExistException();
}

这样当我们访问http://localhost:8080/exception时,就可以发生错误然后有错误页面和数据

定制错误的json数据我们步骤如下:

  1. 编写一个ExceptionHandler ,加上@ControllerAdvice注解,并且编写处理Exception的方法

    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
    @ControllerAdvice
    public class MyExceptionHandler {


    @ExceptionHandler(value = UserNotExistException.class)
    public String handleUserNotExistException(Exception e, HttpServletRequest request){
    Map<String,Object> map = new HashMap<>();
    //传入我们自己的错误状态码 4xx 5xx,否则就不会进入定制错误页面的解析流程

    System.out.println("user处理器被执行");
    //需要写错误码,不然默认为200
    request.setAttribute("javax.servlet.error.status_code",401);
    map.put("code","user.notexist");
    map.put("message","user部分发生错误");
    request.setAttribute("ext", map);
    //转发到/error
    return "forward:/error";
    }

    @ExceptionHandler(value = Exception.class)
    public String handleException(Exception e, HttpServletRequest request){
    Map<String,Object> map = new HashMap<>();
    //传入我们自己的错误状态码 4xx 5xx,否则就不会进入定制错误页面的解析流程

    System.out.println("处理器被执行");
    //需要写错误码,不然默认为200
    request.setAttribute("javax.servlet.error.status_code",500);
    map.put("code","find Exception");
    map.put("message",e.getMessage());
    request.setAttribute("ext", map);
    //转发到/error
    return "forward:/error";
    }
    }

    可以看到我们将错误设置,然后将错误信息的map放在了request里面

  2. 编写ErrorAttributes

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Component
    public class MyErrorAttributes extends DefaultErrorAttributes {
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {

    Map<String, Object> map = super.getErrorAttributes(webRequest,
    includeStackTrace);
    map.put("name","zhouning");
    Map<String,Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
    map.put("ext", ext);
    return map;
    }
    }

    然后在我们编写的MyErrorAttributes里面我们可以将上面的map取出来,然后放到父类得到的map中。

  3. 最终效果

    错误页面定制06

    可以看到我们的信息显示在了上面,并且只有handleUserNotExistException做出相应(响应更加精确的错误)。

  4. 缺点

    定制json数据的方法其实有很多,比如自己编写一个controller同样可以实现,但是这种方法简洁一些所以推荐使用这种。这个方法的缺点就是无法处理404发生错误的请求,其他的可以,目前除了重新写一个controller我没有想到怎么编写可以处理404错误,知道的人可以指点一下。

原理

上面是举出实际例子,但是光靠例子很难理解,现在主要讲解一下里面的原理。我们需要找到ErrorMvcAutoConfiguration这个类

1.BasicErrorController

ErrorMvcAutoConfiguration里面我们能够找到添加了BasicErrorController

1
2
3
4
5
6
7
8
@Bean
@ConditionalOnMissingBean(
value = {ErrorController.class},
search = SearchStrategy.CURRENT
)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes, ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(), (List)errorViewResolvers.orderedStream().collect(Collectors.toList()));
}

然后点进去可以看到具体的实现,可以发现很多东西如下所示(加一些注释上面):

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
@Controller
//可以知道映射的位置可以使用server.error.path设置,如果没有设置默认为error.path,error.path没有设置默认为“/error”,这也是前面转发到“/error”的原因
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
private final ErrorProperties errorProperties;

public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
this(errorAttributes, errorProperties, Collections.emptyList());
}

public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, errorViewResolvers);
Assert.notNull(errorProperties, "ErrorProperties must not be null");
this.errorProperties = errorProperties;
}

public String getErrorPath() {
return this.errorProperties.getPath();
}

//对要求返回html的处理
@RequestMapping(
produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
//注意这个getErrorAttributes在,下面的方法里面也被掉用了
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
//设置状态
response.setStatus(status.value());
//得到解析后的视图,记住这个resolveErrorView这个方法下面会看到
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
//如果得到的视图为空,则返回“error”这个视图
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}

//对于其他处理,主要会返回json文件
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = this.getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
//getErrorAttributes被掉用
Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
return new ResponseEntity(body, status);
}
}

//异常处理
@ExceptionHandler({HttpMediaTypeNotAcceptableException.class})
public ResponseEntity<String> mediaTypeNotAcceptable(HttpServletRequest request) {
HttpStatus status = this.getStatus(request);
return ResponseEntity.status(status).build();
}

protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) {
IncludeStacktrace include = this.getErrorProperties().getIncludeStacktrace();
if (include == IncludeStacktrace.ALWAYS) {
return true;
} else {
return include == IncludeStacktrace.ON_TRACE_PARAM ? this.getTraceParameter(request) : false;
}
}

protected ErrorProperties getErrorProperties() {
return this.errorProperties;
}
}

从上面的解析我们可以看到转发“/error”的原因,以及返回html页面和返回json数据的相应处理。

需要注意的地方:

  • getErrorAttributes在处理html和json的数据里面都被掉用
  • 先使用resolveErrorView解析视图,如果没有视图,再返回ModelAndView("error", model);这个视图

2.DefaultErrorViewResolver

我们往下翻发现这样的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration(
proxyBeanMethods = false
)
static class DefaultErrorViewResolverConfiguration {
private final ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;

DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext, ResourceProperties resourceProperties) {
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
}
//DefaultErrorViewResolver视图解析器
@Bean
@ConditionalOnBean({DispatcherServlet.class})
@ConditionalOnMissingBean({ErrorViewResolver.class})
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
}
}

我们进进入DefaultErrorViewResolver,可以在里面找到这样的方法

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
//和上面BasicErrorController方法里面掉用的resolveErrorView相呼应
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
}

return modelAndView;
}
//解析
private ModelAndView resolve(String viewName, Map<String, Object> model) {
//从error/下找对应的视图,解释了为什么404.html要放在“error/”文件夹下
String errorViewName = "error/" + viewName;
//模板引擎解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
//如果provider不为null,直接返回模板引擎解析的,如果为null在进行resolveResource
return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}

private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
//从静态资源下
String[] var3 = this.resourceProperties.getStaticLocations();
int var4 = var3.length;

for(int var5 = 0; var5 < var4; ++var5) {
String location = var3[var5];

try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model);
}
} catch (Exception var8) {
}
}
//从国静态资里面也没有,则返回null
return null;
}

这部分和上面相呼应,也就解释了为啥文件放在error/文件夹下。

需要注意的点:

  • 视图解析时有模板引擎,先使用模板引擎解析视图,如果模板引擎解析出来为null,再从静态资源里面解析,如果静态资源里面都为null,那么就返回null。这也对应了BasicErrorController中最终可能ModelAndView("error", model)

3.WhitelabelErrorViewConfiguration

上面看到了ModelAndView("error", model)这个视图但是不知道是什么样,我在ErrorMvcAutoConfiguration里面找到了这个

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
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnProperty(
prefix = "server.error.whitelabel",
name = {"enabled"},
matchIfMissing = true
)
@Conditional({ErrorMvcAutoConfiguration.ErrorTemplateMissingCondition.class})
protected static class WhitelabelErrorViewConfiguration {
//就是他error视图
private final ErrorMvcAutoConfiguration.StaticView defaultErrorView = new ErrorMvcAutoConfiguration.StaticView();

protected WhitelabelErrorViewConfiguration() {
}

//error视图在这里
@Bean(
name = {"error"}
)
@ConditionalOnMissingBean(
name = {"error"}
)
public View defaultErrorView() {
return this.defaultErrorView;
}

@Bean
@ConditionalOnMissingBean
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(2147483637);
return resolver;
}
}

然后我们找到StaticView

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
private static class StaticView implements View {
//渲染,原本html的原型
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
if (response.isCommitted()) {
String message = this.getMessage(model);
logger.error(message);
} else {
response.setContentType(TEXT_HTML_UTF8.toString());
StringBuilder builder = new StringBuilder();
Date timestamp = (Date)model.get("timestamp");
Object message = model.get("message");
Object trace = model.get("trace");
if (response.getContentType() == null) {
response.setContentType(this.getContentType());
}

builder.append("<html><body><h1>Whitelabel Error Page</h1>").append("<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>").append("<div id='created'>").append(timestamp).append("</div>").append("<div>There was an unexpected error (type=").append(this.htmlEscape(model.get("error"))).append(", status=").append(this.htmlEscape(model.get("status"))).append(").</div>");
if (message != null) {
builder.append("<div>").append(this.htmlEscape(message)).append("</div>");
}

if (trace != null) {
builder.append("<div style='white-space:pre-wrap;'>").append(this.htmlEscape(trace)).append("</div>");
}

builder.append("</body></html>");
response.getWriter().append(builder.toString());
}
}
}

我们找到了原本html的原型

4.DefaultErrorAttributes

我们在BasicErrorController中看到resolveresolveResource方法里面都掉用了getErrorAttributes这个方法得到信息,我们点进去,发现父类AbstractErrorController里面是这样的

1
2
3
4
protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
WebRequest webRequest = new ServletWebRequest(request);
return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
}

我们找到这个errorAttributes发现是这样的private final ErrorAttributes errorAttributes

然后我们在ErrorMvcAutoConfiguration里面找到DefaultErrorAttributes

1
2
3
4
5
6
7
8
9
//当这个容器中存在ErrorAttributes时,在容器中添加DefaultErrorAttributes
@Bean
@ConditionalOnMissingBean(
value = {ErrorAttributes.class},
search = SearchStrategy.CURRENT
)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());
}

点进去,发现它实现的就是ErrorAttributes

1
2
3
4
5
6
7
8
9
10
11
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
//实现
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap();
errorAttributes.put("timestamp", new Date());
this.addStatus(errorAttributes, webRequest);
this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
this.addPath(errorAttributes, webRequest);
return errorAttributes;
}
}

也就是说在BasicErrorController掉用的方法getErrorAttributes默认是DefaultErrorAttributes,而当我们实现一个ErrorAttributes并切添加进容器时,默认就不会添加DefaultErrorAttributes,而使用我们实现的这个类,这也是为什么我们自定义数据继承了DefaultErrorAttributes

总结

     当我们springboot出现异常比如404、503等,BasicErrorController会对这些错误进行反应,返回对应的html或者json,`BasicErrorController`返回html时,是通过`DefaultErrorViewResolver`进行视图解析,当解析返回null时,掉用系统自带的`StaticView`,而`BasicErrorController`的数据信息来源则是由`DefaultErrorAttributes`进行提供。

​ 以上都是一些我自己的理解,如果有错误的地方欢迎指出来,一起学习。