validation-api

validation-api

JSR303/JSR-349,hibernate validator,spring validation之间的关系

JSR303是一项标准,JSR-349是其的升级版本,添加了一些新特性,他们规定一些校验规范即校验注解,如@Null,@NotNull,@Pattern,他们位于javax.validation.constraints包下,只提供规范不提供实现。而hibernate-validator是对这个规范的实践(不要将hibernate和数据库orm框架联系在一起),他提供了相应的实现,并增加了一些其他校验注解,如@Email,@Length,@Range等等,他们位于org.hibernate.validator.constraints包下。而万能的spring为了给开发者提供便捷,对hibernate validator进行了二次封装,

使用validation-api,通过注解形式进行数据校验

依赖包含关系

校验注解包含在spring-boot-starter-web里面

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

查看spring-boot-starter-web的子依赖:

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.2.1.RELEASE</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<artifactId>tomcat-embed-el</artifactId>
<groupId>org.apache.tomcat.embed</groupId>
</exclusion>
</exclusions>
</dependency>

子依赖中包含了spring-boot-starter-validation,再查看该依赖的子依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>2.0.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.18.Final</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<artifactId>validation-api</artifactId>
<groupId>javax.validation</groupId>
</exclusion>
</exclusions>
</dependency>

可以发现,该子依赖中包含了validation-api,同时包含了它的实现hibernate-validator

validation-api基本注解

限制 说明
@Null 限制只能为null
@NotNull 限制必须不能为null
@AssertFalse 限制必须为false
@AssertTrue 限制必须为true
@DecimalMax(value) 限制必须为一个不大于指定值的数字
@DecimalMin(value) 限制必须为一个不小于指定值的数字
@Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
@Future 限制必须是一个将来的日期
@FutureOrPresent 限制必须是将来的日期或现在
@PastOrPresent 限制必须是过去的日期或现在
@Past 限制必须是一个过去的日期
@Min(value) 限制必须为一个不小于指定值的数字,@Min and @Max supports primitives and their wrappers.
@Max(value) 限制必须为一个不大于指定值的数字
@Pattern(regrexp) 限制必须符合指定的正则表达式,通过regrexp指定正则表达式
@Size(max,min) 限制字符长度必须在min到max之间,supports String, Collection, Map and arrays
@NotEmpty 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@NotBlank 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty@NotBlank只应用于字符串且在比较时会去除字符串的空格(The difference to @NotEmpty is that this constraint can only be applied on character sequences and that trailing white-spaces are ignored.)
@Email(regrexp, flags) 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式

注意

1. 对dto(@RequestBody)进行数据校验

直接在dto的类中的属性上加上校验注解。但是仅仅这样的话,该注解不会生效

需要在controller中需要校验的参数前加上@Validated注解:

e.g.

1
2
3
4
5
6
@PostMapping("/register")
public ResultEntity register(@Validated
@RequestBody CustomerRequest customerRequest){
customerService.register(customerRequest);
return ResultEntity.succeed();
}

2. 对GET请求的参数(@RequestParam)进行数据校验

@RequestParam注解的参数通常没有专门的类,需要直接在controller方法的参数处加上校验注解:

1
2
3
4
5
6
@GetMapping("/check-code")
public ResultEntity sendCheckCode(@RequestParam
@Email(message = "必须为合法邮箱地址") String email) {
customerService.sendCheckCode(email);
return ResultEntity.succeed();
}

仅仅这样,注解也不会生效。切记需要在controller类上加入@Validated注解才可以生效:

1
2
3
4
5
@Slf4j
@RestController
@Validated // 对@RequestParam的校验需要在controller上加@Validated注解
@RequestMapping("/customer")
public class CustomerController {

官方文档

https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#section-builtin-constraints

Spring Boot对静态方法进行打桩

Spring Boot对静态方法进行打桩

问题

在对Spring Boot项目进行测试的时候,会对业务逻辑service层进行测试。而service层的代码可能会使用util中的工具类,而工具方法通常来说都是static类型的方法。问题在于,当对service层的代码进行测试时,我们往往需要对静态方法打桩,返回我们需要的结果。然而,主流的测试框架Mockito并不支持对静态方法的打桩。对于此问题,我们需要寻求其他框架的解决方案

解决

1.

首先,通过mockito官方文档的描述,可以发现:

What are the limitations of Mockito

  • Cannot mock final classes
  • Cannot mock static methods
  • Cannot mock final methods - their real behavior is executed without any exception.
  • Mockito cannot warn you about mocking final methods so be vigilant.

可以看到,Mockito并不支持mock静态方法,同时也有以下的描述:

Can I mock static methods?

No. Mockito prefers object orientation and dependency injection over static, procedural code that is hard to understand & change. If you deal with scary legacy code you can use JMockit or Powermock to mock static methods.

通过这样的描述,我们发现其他框架提供了解决方案:

  • JMockit
  • Powermock

之后,由于网上的回答中,Powermock更加主流,且与Mockito的语法相近,于是考虑通过Powermock框架解决问题。

再次查阅了Powermock在github上的项目主页:,得到以下的说明:

urrently PowerMock supports JUnit and TestNG. There are three different JUnit test executors available, one for JUnit 4.4-4.12, one for JUnit 4.0-4.3. The test executor for JUnit 3 is not avaliable since PowerMock 2.0.

可以看出,Powermock当前最新版本仅支持到JUnit4.12,而无法对JUnit5提供支持。然而,spring-boot-starter-test中,整合的已是JUnit5. 在网上找寻了众多的回答,所有的例子都是通过JUnit4编写的测试脚本。

且通过Powermock测试的脚本结构如下:

1
2
3
4
5
@RunWith(PowerMockRunner.class)
@PrepareForTest( { YourClassWithEgStaticMethod.class })
public class YourTestCase {
...
}

该结构为JUnit4的写法,并不能在JUnit5中使用。我也尝试强行在Spring Boot项目中单独添加JUnit4依赖,然后单独通过JUnit4来运行通过Powermock编写的测试脚本,多次尝试之后依然无法运行。于是转向另一个框架–JMockit。

2.

尝试通过JMockit来测试静态方法。首先查看JMockit是否支持JUnit5,在官网寻找到了答案:

To run tests that use any of the JMockit APIs, use your Java IDE, Maven/Gradle build script, etc. the way you normally would. In principle, any JDK of version 1.7 or newer, on Windows, Mac OS X, or Linux, can be used. JMockit supports (and requires) the use of JUnit (version 4 or 5) or TestNG

既然JMockit支持JUnit5,且可以对静态方法进行打桩,这就是一种可行的解决方案。所以接下来我就去寻找对静态方法进行打桩的实现方案。

在一篇博客中,我发现了解决方案:

  • 首先,需要添加JMockit的依赖,通过mvnrepository添加最新版的JMockit依赖:

    1
    2
    3
    4
    5
    6
    7
    <!-- https://mvnrepository.com/artifact/org.jmockit/jmockit -->
    <dependency>
    <groupId>org.jmockit</groupId>
    <artifactId>jmockit</artifactId>
    <version>1.48</version>
    <scope>test</scope>
    </dependency>
  • 查看博客中给出的例子:

    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
    // 需要测试的类
    public class AppManager {

    public boolean managerResponse(String question) {
    return AppManager.isResponsePositive(question);
    }

    public static boolean isResponsePositive(String value) {
    if (value == null) {
    return false;
    }
    int length = value.length();
    int randomNumber = randomNumber();
    return length == randomNumber ? true : false;
    }

    private static int randomNumber() {
    return new Random().nextInt(7);
    }
    }

    // 对静态方法进行打桩
    @Test
    public void givenAppManager_whenStaticMethodCalled_thenValidateExpectedResponse() {
    new MockUp<AppManager>() {
    @Mock
    public boolean isResponsePositive(String value) {
    return false;
    }
    };

    assertFalse(appManager.managerResponse("Some string..."));
    }
  • 按照类似的写法,完成了我的测试脚本的编写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @Test
    @DisplayName("发送邮件内容正确")
    void shouldSendCorrectEmailWhenUserIsNew(){
    // 使用JMockit对静态方法进行打桩
    new MockUp<CheckCodeUtil>(){
    @mockit.Mock
    public String generateCheckCode(){
    return "123456";
    }
    };
    when(redisTemplate.opsForValue()).thenReturn(new ValueOperationsFake());
    ArgumentCaptor<String> emailCaptor = ArgumentCaptor.forClass(String.class);
    ArgumentCaptor<String> subjectCaptor = ArgumentCaptor.forClass(String.class);
    ArgumentCaptor<String> contentCaptor = ArgumentCaptor.forClass(String.class);
    customerService.sendCheckCode("10175101152@stu.ecnu.edu.cn");
    verify(mailService, times(1))
    .sendHtmlMail(emailCaptor.capture(), subjectCaptor.capture(), contentCaptor.capture());
    assertAll(
    () -> assertEquals("10175101152@stu.ecnu.edu.cn", emailCaptor.getValue()),
    () -> assertEquals("Registration from MeetHere", subjectCaptor.getValue()),
    () -> assertEquals("<h1>Welcome to MeetHere!</h1><p>Your check code is <u>" +
    "123456" + "</u></p>", contentCaptor.getValue())
    );
    }
  • 编写完毕后,尝试是否可以运行,但是依然报错:

    1
    java.lang.IllegalStateException: JMockit didn't get initialized; please check the -javaagent JVM initialization parameter was used

    针对报错信息去Google进行查找,大多数的解决方案是:

    1
    @RunWith(JMockit.class)

    这同样是JUnit4的写法。并不能解决我所遇到的问题

  • 再次回到JMockit的官方文档,查看官方文档中给出的运行方法,发现了潜在的解决方案:

    JMockit also requires the -javaagent JVM initialization parameter to be used; when using the Maven Surefire plugin for test execution, it’s specified as follows:

    所以看来想要运行JMockito编写的测试脚本,需要指定-javaagent的JVM参数才可以。

    官网给出了方案:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <plugins>
    <plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.22.2</version> <!-- or some other version -->
    <configuration>
    <argLine>
    -javaagent:${settings.localRepository}/org/jmockit/jmockit/${jmockit.version}/jmockit-${jmockit.version}.jar
    </argLine>
    </configuration>
    </plugin>
    </plugins>

    将这段xml复制到pom文件中,并将${jmockit.version}替换为项目中使用的1.48

  • 再次运行测试脚本:

    屏幕快照 2019-12-10 下午6.12.39

    BINGO!

Spring Boot-CORS

Spring Boot-CORS

什么是跨域?

定义:浏览器从一个域名的网页取请求另一个域名下的东西。通俗点说,浏览器直接从A域访问B域中的资源是不被允许的,如果想要访问,就需要进行一步操作,这操作就叫“跨域”。例如,你从百度的页面,点击一个按钮,请求了新浪的一个接口,这就进行了跨域。不单单只有域名不同就是跨域,域名、端口、协议其一不同就是不同的域,请求资源需要跨域。

为什么要跨域?

为什么需要跨域,而不直接访问其他域下的资源呢?这是浏览器的限制,专业点说叫浏览器同源策略限制。主要是为了安全考虑。现在的安全框架,一般请求的时候header中不是都存个token嘛,你要是用这个token去正常访问A域下的东西是没问题的,然后又去访问了B域,结果阴差阳错的还带着这个token,那么B域,或者说B网站是不是就可以拿着你的token去A域下做点什么呢,这就相当危险了。所以浏览器加上了所谓的浏览器同源策略限制。但是为了我们真的需要从A域下访问B的资源(正常访问),就需要用到跨域,跨越这个限制了。

SpringBoot解决跨域问题

SpringBoot可以基于Cors解决跨域问题,Cors是一种机制,告诉我们的后台,哪边(origin )来的请求可以访问服务器的数据。

全局配置

配置实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer(){
return new WebMvcConfigurer(){
@Override
public void addCorsMappings(CorsRegistry registry){
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
.allowCredentials(true)
.maxAge(3600);
}
};
}
}

首先实现了WebMvcConfigurer接口,WebMvcConfigurer这个接口十分强大,里面还有很多可用的方法,在SpringBoot2.0里面可以解决WebMvcConfigurerAdapter曾经的部分任务。其中一个方法就是addCorsMappings(),是专门为开发人员解决跨域而诞生的接口。其中构造参数为CorsRegistry

CorsRegistry的源码

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

private final List<CorsRegistration> registrations = new ArrayList<>();

public CorsRegistration addMapping(String pathPattern) {
CorsRegistration registration = new CorsRegistration(pathPattern);
this.registrations.add(registration);
return registration;
}

protected Map<String, CorsConfiguration> getCorsConfigurations() {
Map<String, CorsConfiguration> configs = new LinkedHashMap<>(this.registrations.size());
for (CorsRegistration registration : this.registrations) {
configs.put(registration.getPathPattern(), registration.getCorsConfiguration());
}
return configs;
}
}

可以看出CorsRegistry有个属性registrations ,按道理可以根据不同的项目路径进行定制访问行为,但是我们示例直接将pathPattern 设置为/**,也就是说已覆盖项目所有路径,只需要创建一个CorsRegistration就好。getCorsConfigurations(),这个方法是获取所有CorsConfiguration的Map集合,key值为传入路径pathPattern
回到示例代码CorsConfig中,registry对象addMapping()增加完传入路径pathPattern之后,return了一个CorsRegistration对象,是进行更多的配置,看一下CorsRegistration的代码,看看我们能配些什么?

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
public class CorsRegistration {
//传入的路径
private final String pathPattern;
//配置信息实体类
private final CorsConfiguration config;
//构造方法
public CorsRegistration(String pathPattern) {
this.pathPattern = pathPattern;
//原生注释看到了一个 @CrossOrigin 这个注解,待会看看是什么
// Same implicit default values as the @CrossOrigin annotation + allows simple methods
this.config = new CorsConfiguration().applyPermitDefaultValues();
}
//允许哪些源网站访问,默认所有
public CorsRegistration allowedOrigins(String... origins) {
this.config.setAllowedOrigins(Arrays.asList(origins));
return this;
}
//允许何种方式访问,默认简单方式,即:GET,HEAD,POST
public CorsRegistration allowedMethods(String... methods) {
this.config.setAllowedMethods(Arrays.asList(methods));
return this;
}
//设置访问header,默认所有
public CorsRegistration allowedHeaders(String... headers) {
this.config.setAllowedHeaders(Arrays.asList(headers));
return this;
}
//设置response headers,默认没有(什么都不设置)
public CorsRegistration exposedHeaders(String... headers) {
this.config.setExposedHeaders(Arrays.asList(headers));
return this;
}
//是否浏览器应该发送credentials,例如cookies Access-Control-Allow-Credentials
public CorsRegistration allowCredentials(boolean allowCredentials) {
this.config.setAllowCredentials(allowCredentials);
return this;
}
//设置等待时间,默认1800秒
public CorsRegistration maxAge(long maxAge) {
this.config.setMaxAge(maxAge);
return this;
}

protected String getPathPattern() {
return this.pathPattern;
}

protected CorsConfiguration getCorsConfiguration() {
return this.config;
}

}

局部配置

在Controller上加入@CrossOrigin注解

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
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {

/** @deprecated as of Spring 5.0, in favor of {@link CorsConfiguration#applyPermitDefaultValues} */
@Deprecated
String[] DEFAULT_ORIGINS = { "*" };

/** @deprecated as of Spring 5.0, in favor of {@link CorsConfiguration#applyPermitDefaultValues} */
@Deprecated
String[] DEFAULT_ALLOWED_HEADERS = { "*" };

/** @deprecated as of Spring 5.0, in favor of {@link CorsConfiguration#applyPermitDefaultValues} */
@Deprecated
boolean DEFAULT_ALLOW_CREDENTIALS = false;

/** @deprecated as of Spring 5.0, in favor of {@link CorsConfiguration#applyPermitDefaultValues} */
@Deprecated
long DEFAULT_MAX_AGE = 1800

/**
* Alias for {@link #origins}.
*/
@AliasFor("origins")
String[] value() default {};

@AliasFor("value")
String[] origins() default {};

String[] allowedHeaders() default {};

String[] exposedHeaders() default {};

RequestMethod[] methods() default {};

String allowCredentials() default "";

long maxAge() default -1;
}

这个注解可以作用于方法或者类上,实现局部跨域,你会发现除了设置路径(因为没必要了,都定位到局部了)其他的参数与全局类似。

SpringBoot-Mybatis

Spring Boot-Mybatis

1. ORM框架选型

对比项 SPRING DATA JPA MYBATIS
单表操作方式 只需继承,代码量较少,非常方便。而且支持方法名用关键字生成SQL 可以使用代码生成工具,也很方便,但相对JPA单表弱很多。JPA单表操作非常简单
多表关联查询 友好,动态SQL使用不够方便,而且SQL和代码耦合到一起 非常友好,可以有非常直观的动态SQL
自定义SQL SQL写在注解里面,写动态SQL有些费劲 SQL可以写在XML里,独立管理,动态SQL语法也容易书写理解
学习成本 略高 较低,会写SQL就可以

JPA是规范,Hibernate是实现

  • Spring Data JPA 对开发人员更加友好,单表操作非常方便,多表关联也不麻烦
  • mybatis各方面都很优秀,使用范围更广
  • 大型项目建议mybatis

2. 整合MyBatis操作数据库

  • pom.xml
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
  • mybatis-config.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置全局属性-->
<settings>
<!-- 使用Jdbc的getGeneratedKeys获取数据库自增主键值 -->
<setting name="useGeneratedKeys" value="true"/>
<!-- 使用列表签替换列别名 -->
<setting name="useColumnLabel" value="true"/>
<!-- 开启驼峰命名转换: Table{create_time} -> Entity{createTime} -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
</configuration>
  • 同样的内容也可以写在Spring Boot配置文件application.yml中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
datasource:
username: root
password: Thwf1858
url: jdbc:mysql://localhost:3306/mybatis
driver-class-name: com.mysql.jdbc.Driver
logging:
level:
com.haven.mybatis.mapper: debug
mybatis:
configuration:
map-underscore-to-camel-case: true
use-generated-keys: true
use-column-label: true
  • 注解书写SQL
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
package com.hehe.mapper;
@Mapper
public interface UserMapper {
/**
* 方式1:使用注解编写SQL。
*/
@Select("select * from t_user")
List<User> list();

/**
* 方式2:使用注解指定某个工具类的方法来动态编写SQL.
*/
@SelectProvider(type = UserSqlProvider.class, method = "listByUsername")
List<User> listByUsername(String username);

/**
* 延伸:上述两种方式都可以附加@Results注解来指定结果集的映射关系.
*
* PS:如果符合下划线转驼峰的匹配项可以直接省略不写。
*/
@Results({
@Result(property = "userId", column = "USER_ID"),
@Result(property = "username", column = "USERNAME"),
@Result(property = "password", column = "PASSWORD"),
@Result(property = "mobileNum", column = "PHONE_NUM")
})
@Select("select * from t_user")
List<User> listSample();

/**
* 延伸:无论什么方式,如果涉及多个参数,则必须加上@Param注解,否则无法使用EL表达式获取参数。
*/
@Select("select * from t_user where username like #{username} and password like #{password}")
User get(@Param("username") String username, @Param("password") String password);

@SelectProvider(type = UserSqlProvider.class, method = "getBadUser")
User getBadUser(@Param("username") String username, @Param("password") String password);

}
  • 可以传入参数

    • JavaBean
    • Map
    • 多个参数,需要用@Param注解
  • @Results注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Select("select t_id, t_age, t_name  "
    + "from sys_user "
    + "where t_id = #{id} ")
    @Results(id="userResults", value={
    @Result(property="id", column="t_id"),
    @Result(property="age", column="t_age"),
    @Result(property="name", column="t_name"),
    })
       User selectUserById(@Param("id") String id);

    @Results可以给出一个id,其他方法根据该id可以通过@ResultMap重复使用:

    1
    2
    3
    4
    5
    @Select("select t_id, t_age, t_name  "
    + "from sys_user "
    + "where t_name = #{name} ")
    @ResultMap("userResults")
       User selectUserByName(@Param("name") String name);

Spring Boot-JDBC

Spring Boot & JDBC

1. Spring Boot整合JDBC操作数据库

JDBC操作数据库流程

  1. 加载数据库驱动
  2. 建立数据库连接
  3. 创建数据库操作对象
  4. 定义操作的SQL语句
  5. 执行数据库操作
  6. 获取并操作结果集
  7. 关闭对象,回收资源

不建议使用JDBC

将Spring JDBC整合到Spring Boot

  1. pom.xml引入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
  1. 修改application.yml
1
2
3
4
5
6
spring:
datasource:
url: jdbc:mysql://localhost:3306/jdbc
username: root
password: Thwf1858
driver-class-name: com.mysql.jdbc.Driver
  1. DAO层代码
  • jdbcTemplate.update适用于insert, update和delete操作

  • jdbcTemplate.queryForObject用于查询单条记录并返回结果

  • jdbcTemplate.query用于查询结果列表

  • BeanPropertyRowMapper可以将数据库字段的值向数据库映射,满足驼峰标识也可以自动映射

    e.x. 数据库create_time映射到createTime属性

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
package com.haven.dao;

import com.fasterxml.jackson.databind.BeanProperty;
import com.haven.model.Article;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import javax.annotation.Resource;
import java.util.List;

/**
* @author HavenTong
* @date 2019/10/30 10:39 下午
*/
@Repository // @Repository标注持久层
public class ArticleJDBCDAO {

@Resource
private JdbcTemplate jdbcTemplate;

// 保存文章
public void save(Article article){
jdbcTemplate.update("INSERT INTO article(author, title, content, create_time) values (?,?,?,?)",
article.getAuthor(),
article.getTitle(),
article.getContent(),
article.getCreateTime());
}

// 删除文章
// 传参可以用 new Object[]{}传,也可以一个一个设置
public void deleteById(int id){
jdbcTemplate.update("DELETE FROM article WHERE id=?", new Object[]{id});
}

// 更新文章
public void updateById(Article article){
jdbcTemplate.update("UPDATE article SET author=?, title=?, content=?, create_time=? WHERE id=?",
article.getAuthor(),
article.getTitle(),
article.getContent(),
article.getCreateTime(),
article.getId());
}

// 根据id查找文章
public Article findById(int id){
return (Article)jdbcTemplate.queryForObject("SELECT * FROM article WHERE id = ?",
new Object[]{id},
new BeanPropertyRowMapper(Article.class));
}

// 查询所有
public List<Article> findAll(){
return (List<Article>) jdbcTemplate.query("SELECT * FROM article", new BeanPropertyRowMapper(Article.class));
}

}

2. JDBC多数据源

(1) application.yml配置两个数据源,第一个叫primary, 第二个叫secondar,也可以自己取名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server:
port: 8080
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
datasource:
primary:
jdbc-url: jdbc:mysql://localhost:3306/jdbc
username: root
password: Thwf1858
driver-class-name: com.mysql.jdbc.Driver
secondary:
jdbc-url: jdbc:mysql://localhost:3306/test
username: root
password: Thwf1858
driver-class-name: com.mysql.jdbc.Driver

(2) 通过Java Config将数据源注入到Spring上下文

primaryJdbcTemplate使用primaryDataSource数据源操作数据库jdbc

secondaryJdbcTemplate使用secondaryDataSource数据源操作数据库test

DataSourceConfig.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
package com.haven.config;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;
import javax.xml.crypto.Data;

/**
* @author HavenTong
* @date 2019/10/31 12:08 上午
*/
@Configuration
public class DataSourceConfig {
@Primary
@Bean(name = "primaryDataSource")
@Qualifier("primaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource(){
return DataSourceBuilder.create().build();
}

@Bean(name = "secondaryDataSource")
@Qualifier("secondaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource(){
return DataSourceBuilder.create().build();
}

@Bean(name = "primaryJdbcTemplate")
public JdbcTemplate primaryJdbcTemplate(
@Qualifier("primaryDataSource") DataSource dataSource ){
return new JdbcTemplate(dataSource);
}

@Bean(name = "secondaryJdbcTemplate")
public JdbcTemplate secondaryTemplate(
@Qualifier("secondaryDataSource") DataSource dataSource){
return new JdbcTemplate(dataSource);
}
}

(3) 之后修改dao层代码:

ArticleJDBCDAO.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
package com.haven.dao;

import com.fasterxml.jackson.databind.BeanProperty;
import com.haven.model.Article;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import javax.annotation.Resource;
import java.util.List;

/**
* @author HavenTong
* @date 2019/10/30 10:39 下午
*/
@Repository
public class ArticleJDBCDAO {

// 保存文章
public void save(Article article, JdbcTemplate jdbcTemplate){
jdbcTemplate.update("INSERT INTO article(author, title, content, create_time) values (?,?,?,?)",
article.getAuthor(),
article.getTitle(),
article.getContent(),
article.getCreateTime());
}

// 删除文章
public void deleteById(int id, JdbcTemplate jdbcTemplate){

jdbcTemplate.update("DELETE FROM article WHERE id=?", new Object[]{id});
}

// 更新文章
public void updateById(Article article, JdbcTemplate jdbcTemplate){
jdbcTemplate.update("UPDATE article SET author=?, title=?, content=?, create_time=? WHERE id=?",
article.getAuthor(),
article.getTitle(),
article.getContent(),
article.getCreateTime(),
article.getId());
}

// 根据id查找文章
public Article findById(int id, JdbcTemplate jdbcTemplate){
return (Article)jdbcTemplate.queryForObject("SELECT * FROM article WHERE id = ?",
new Object[]{id},
new BeanPropertyRowMapper(Article.class));
}

// 查询所有
public List<Article> findAll(JdbcTemplate jdbcTemplate){
return (List<Article>) jdbcTemplate.query("SELECT * FROM article", new BeanPropertyRowMapper(Article.class));
}

}

(4) 修改service层代码

ArticleRestJDBCServiceImpl.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
package com.haven.service;

import com.haven.dao.ArticleJDBCDAO;
import com.haven.model.Article;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.List;

/**
* @author HavenTong
* @date 2019/10/30 10:56 下午
*/
@Slf4j
@Service
public class ArticleRestJDBCServiceImpl implements ArticleRestService {

@Resource
ArticleJDBCDAO articleJDBCDAO;

@Resource
JdbcTemplate primaryJdbcTemplate;

@Resource
JdbcTemplate secondaryJdbcTemplate;

@Transactional
@Override
public Article saveArticle(Article article) {
articleJDBCDAO.save(article, primaryJdbcTemplate);
articleJDBCDAO.save(article, secondaryJdbcTemplate);
return article;

}

@Override
public void deleteArticle(int id) {
articleJDBCDAO.deleteById(id, primaryJdbcTemplate);
articleJDBCDAO.deleteById(id, secondaryJdbcTemplate);
}

@Override
public void updateArticle(Article article) {
articleJDBCDAO.updateById(article, primaryJdbcTemplate);
}

@Override
public Article getArticle(int id) {
return articleJDBCDAO.findById(id, primaryJdbcTemplate);
}

@Override
public List<Article> getAll() {
return articleJDBCDAO.findAll(primaryJdbcTemplate);
}
}

3. Spring JDBC JTA实现分布式事务

@Transactional无法跨库完成分布式事务

通过整合JTA实现分布式事务

  • 引入maven依赖
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jta-atomikos -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
  • 修改application.yml配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
primarydb:
uniqueResourceName: primary
xaDataSourceClassName: com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
xaProperties:
url: jdbc:mysql://localhost:3306/jdbc
user: root
password: Thwf1858
exclusiveConnectionMode: true
minPoolSize: 3
maxPoolSize: 10
testQuery: SELECT 1 FROM dual
secondarydb:
uniqueResourceName: secondary
xaDataSourceClassName: com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
xaProperties:
url: jdbc:mysql://localhost:3306/test
user: root
password: Thwf1858
exclusiveConnectionMode: true
minPoolSize: 3
maxPoolSize: 10
testQuery: SELECT 1 FROM dual
  • 编写配置类DataSourceConfig.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
package com.haven.config;

import com.atomikos.jdbc.AtomikosDataSourceBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;
import javax.xml.crypto.Data;

/**
* @author HavenTong
* @date 2019/10/31 12:08 上午
*/
@Configuration
public class DataSourceConfig {
// 多数据源,分布式
@Bean(initMethod = "init", destroyMethod = "close", name = "primaryDataSource")
@Primary
@ConfigurationProperties(prefix = "primarydb")
public DataSource primaryDataSource(){
return new AtomikosDataSourceBean();
}

@Bean(initMethod = "init", destroyMethod = "close", name = "secondaryDataSource")
@ConfigurationProperties(prefix = "secondarydb")
public DataSource secondaryDataSource(){
return new AtomikosDataSourceBean();
}

@Bean
public JdbcTemplate primaryJdbcTemplate(@Qualifier("primaryDataSource")
DataSource primaryDataSource){
return new JdbcTemplate(primaryDataSource);
}

@Bean
public JdbcTemplate secondaryJdbcTemplate(@Qualifier("secondaryDataSource")
DataSource secondaryDataSource){
return new JdbcTemplate(secondaryDataSource);
}

}
  • 配置事务管理器™ TransactionManagerConfig.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
package com.haven.config;

import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.jta.JtaTransactionManager;

import javax.transaction.SystemException;
import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;

/**
* @author HavenTong
* @date 2019/10/31 3:57 下午
*/
@Configuration
public class TransactionManagerConfig {

@Bean
public UserTransaction userTransaction() throws SystemException {
UserTransactionImp userTransactionImp = new UserTransactionImp();
userTransactionImp.setTransactionTimeout(10000);
return userTransactionImp;
}

@Bean(name = "atomikosTransactionManager", initMethod = "init", destroyMethod = "close")
public TransactionManager atomikosTransactionManager() throws Throwable{
UserTransactionManager userTransactionManager = new UserTransactionManager();
userTransactionManager.setForceShutdown(false);
return userTransactionManager;
}

@Bean(name = "transactionManager")
@DependsOn({"userTransaction", "atomikosTransactionManger"})
public PlatformTransactionManager transactionManager() throws Throwable{
UserTransaction userTransaction = userTransaction();
JtaTransactionManager manager = new JtaTransactionManager(userTransaction, atomikosTransactionManager());
return manager;
}
}

Spring Boot Bean自动装配

Spring Boot Bean自动装配

1. 全局配置文件

修改Spring Boot自动配置的默认值,Spring Boot在底层自动加载

  • application.yml

  • application.properties

2. Bean自动装配原理

(1) Spring Boot启动时加载主配置类,开启了自动配置功能@EnableAutoConfiguration

(2) @EnableAutoConfiguration

​ 作用: 将类路径下META-INF/spring.factories里面配置的所有EnableAutoConfiguration的值(自动装配类)加入到执行计划中

(3) 每一个自动装配类进行自动配置功能

3. YAML规则

(1) 双引号

​ 不会转义特殊字符,特殊字符或作为本身想要表达的意思

name: "zhangsan \n lisi" 输出 zhangsan 换行 lisi

单引号

​ 会转义特殊字符,特殊字符最终只是一个普通的字符串数据,如:

name: 'zhangsan \n lisi' 输出 zhangsan \n lisi

(2) 支持松散的结构

family-name = familyName = family_name

(3) 占位符

1
2
3
4
5
6
${random.value}
${random.int}
${random.long}
${random.int(10)}
${random.int[1024, 65536]}
${xxxx.yyyy: 默认值}

4. 获取自定义配置

(1) @Value("${}")

​ 实现了单个属性的注入

(2) 对于复杂的数据结构,使用@ConfigurationProperties获取配置值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.haven.model.yaml;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
* @author HavenTong
* @date 2019/10/29 5:44 下午
*/

@Data
@Component
@ConfigurationProperties(prefix = "family")
public class Family {
// @Value("${family.family-name}")
private String familyName;
private Father father;
private Mother mother;
private Child child;
}

(3) 两种注解区别

@ConfigurationProperties @Value
功能 批量注入属性 一个个指定
松散语法绑定 支持 不支持
SpEL 不支持 支持
复杂数据类型嵌套 支持 不支持
JSR303数据校验 支持 不支持

5. 配置文件注入值数据校验

(1) 在需要校验的属性装配类上加@Validated注解

(2) 数据校验注解列表

(3) 若验证失败,会出现BindValidationException异常

6. Profile不同环境下不同配置

优先级: 外部大于内部,特指大于泛指

  • application.yml 全局配置文件
  • application-dev.yml 开发环境配置文件
  • application-test.yml 测试环境配置文件
  • Application-prod.yml 生产环境配置文件

(1) 配置application.yml

设置spring.profiles.active指定使用哪一个配置文件。

优先级以下面的dev/test/prod yml为优先

1
2
3
spring:
profiles:
active: dev

(2) 通过命令行启动

1
java -jar SpringBoot01HelloWorld-1.0-SNAPSHOT.jar --spring.profiles.active=dev

(3) IDEA设置Program Arguments

​ (a) Edit Configuration --> Environment --> Program Argument

1
--spring.profiles.active=dev

​ (b) Edit Configuration --> Environment --> VM options

1
-Dspring.profiles.active=dev

7. 项目内部配置文件加载位置

spring boot会扫描以下位置的application.properties / application.yml文件作为spring boot的默认配置文件

1
2
3
4
-file:./config/
-file:./
-classpath:/config/
-classpath:./

以上优先级从高到低,所有位置的文件都会被加载;高优先级会覆盖低优先级

8. 配置文件敏感字段加密

(1) 与spring boot整合

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.github.ulisesbocchio/jasypt-spring-boot-starter -->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>1.18</version>
</dependency>

(2) 需要加密的地方,使用ENC()进行包裹处理

(3) 在配置文件中设置密钥

SpringBoot RESTful接口

SpringBoot RESTful接口

1.常用注解开发RESTful接口

  • @RestController

    1. 将注解的类注入到Spring的环境
    2. 相当于@Controller + @ResponseBody
    3. JSON格式的数据响应
  • @RequestMapping

    1. 类上的注解表示注解的Controller类的路径
  • @PathVariable

    1. 路径上的变量
  • @PathVariable & RequestParam

    1. @PathVariable用于接收URL上的{参数}
    2. @RequestParam用于接收普通方式提供的参数
  • @RequestBody & @RequestParam

    1. JSON数据建议用@RequestBody,会分配实体类中的所有属性

    2. @RequestParam适合接收单个的参数

    3. @RequestBody可以接收嵌套的数据结构

2.JSON数据处理和Postman测试

Spring Boot默认使用Jackson

(1) 常用注解

  • @JsonIgnore:加在属性上表示在序列化和反序列化的过程中将它忽略

  • @JsonProperty:为属性起别名

  • @JsonPropertyOrder:加在类上

  • @JsonInclude(JsonInclude.Include.NON_NULL): 当属性不为空的时候,进行序列化;否则不进行

  • @JsonFormat(pattern = "", timezone = ""):配置时间格式

    1
    2
    3
    4
    spring:
    jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

(2) 序列化与反序列化

把对象转成可传输、可存储的格式(json, xml, 二进制,甚至自定义的格式)叫序列化,反序列化为逆过程

3. Postman使用

4. 使用Swagger 2发布API文档

  • 代码变,文档变,只需要少量的注解,使用Swagger就可以根据代码自动生成API文档,很好地保持了文档的及时性
  • 跨语言性,支持40多种语言
  • Swagge UI呈现出一份可交互式的API文档,我们可以直接在文档页面尝试API的调用,省去了准备复杂的

调用参数的过程

  • 还可以将文档规范导入相关的工具(e.x. SoapUI),这些工具会为我们创建自动化的测试

整合Swagger 2

pom.xml

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.6.1</version>
</dependency>

<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.6.1</version>
</dependency>
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
// config/Swagger2.java
package com.haven.config;

import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.w3c.dom.DocumentType;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
* @author HavenTong
* @date 2019/10/29 3:26 下午
*/
@Configuration
@EnableSwagger2
public class Swagger2 {

@Bean
public Docket createRestApi(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.haven"))
.paths(PathSelectors.regex("/rest/.*"))
.build();
}

private ApiInfo apiInfo(){
return new ApiInfoBuilder()
.title("springboot利用swagger构建api文档")
.description("简单优雅的restfun风格")
.termsOfServiceUrl("http://www.zimug.com")
.version("1.0")
.build();
}
}

之后启动项目,通过http://localhost:8080/swagger-ui.html即可访问swagger-ui

可以在方法上添加更详细的注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
    @ApiOperation(value = "添加文章", notes = "添加新的文章", tags = "Article", httpMethod = "POST")
// @ApiImplicitParams({
// @ApiImplicitParam(name = "title", value = "文章标题", required = true, dataType = "String"),
// @ApiImplicitParam(name = "content", value = "文章内容", required = true, dataType = "String"),
// @ApiImplicitParam(name = "author", value = "文章作者", required = true, dataType = "String")
// })
@ApiResponses({
@ApiResponse(code = 200, message = "成功", response = AjaxResponse.class),
@ApiResponse(code = 400, message = "用户输入错误", response = AjaxResponse.class),
@ApiResponse(code = 500, message = "系统内部错误", response = AjaxResponse.class)
})
@RequestMapping(value = "/article", method = RequestMethod.POST, produces = "application/json")
public AjaxResponse saveArticle(@RequestBody Article article){
  • 由于采用@RequestBody去接收参数,这里就不需要使用@ApiImplicitParam注解,@ApiImplicitParam注解与@RequestParam注解是一一对应的。
  • 建议有Swagger 2的情况下,减少与此对应的代码注释或不写

Swagger 2常用注解

  • @Api:用在请求的类上,表示对类的说明

    tags=“说明该类的作用,可以在UI界面上看到的注解”

    value=“该参数没有什么意义,在UI界面上也能看到,所以不需要配置”

  • @ApiOperation: 用在请求的方法上,说明方法的用途、作用

    value=“说明方法的用途,作用”

    notes=“方法的备注说明”

  • @ApiImplicitParams: 用在请求的方法上,表示一组参数说明

    @ApiImplicitParam: 用在@ApiImplicitParams注解中,指定一个请求参数的各个方面

    name=“参数名”

    value=“参数的汉字说明,解释”

    required=“参数是否必须要传”

    paramType=“参数放在哪个地方”

    ​ - header --> 请求参数的获取:@RequestHeader

    ​ - query --> 请求参数的获取:@RequestParam

    ​ - path(用于restful接口) --> 请求参数的获取: @PathVariable

    ​ - body,form不常用

    dataType= “参数类型”,默认String, 其他值dataType="Integer"

    defaultValue= “参数的默认值”

  • @ApiResponses: 用在请求的方法上,表示一组响应

    @ApiResponse: 用在@ApiResponses中,一般用于表达一个错误的响应信息

    code= 数字,e.x. 400

    message= 信息,例如"请求参数没填好"

    response= 抛出异常的类

  • @ApiModel: 用于响应类上,表示一个返回响应数据的信息。(这种一般用在post创建的时候, 使用@RequestBody的场景,请求参数无法使用@ApiImplicitParam注解进行描述的时候)

    @ApiModelProperty: 用在属性上,描述响应类的属性

Spring Boot-IDEA开发技巧

SpringBoot with IDEA

插件

lombok

  • IDEA中下载后

  • 在pom.xml文件中引入依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
    </dependency>
  • 常用注解

    1
    2
    3
    4
    5
    @Builder
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    ...

GsonFormat

  • 可以快速的将JSON转换为实体类
  • shortcut option + s

热部署

  • pom文件中引入依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional>
    </dependency>
  • pom文件中引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
    <fork>true</fork>
    </configuration>
    </plugin>
    </plugins>
    </build>
  • Preferences – Compiler – Build project automatically ✅

  • cmd + option + shift + / – registry – compiler.automake.allow.when.app.running – ✅

  • 配置完成后,修改项目文件后会自动build

SpringBoot配置

Spring Boot配置

1. 标记语言

  • 以前的配置文件:xxx.xml

  • YAML: 以数据为中心,比json, xml更适合作配置文件,实例如下

    1
    2
    server:
    port: 8081

    XML:

    1
    2
    3
    <server>
    <port>8081</port>
    </server>

2. YAML语法

(1) 基本语法

K :(空格)V — 表示一对键值对

以空格的缩进来控制层级关系,左对齐的一列数据都是同一层级

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

属性和值大小写敏感

(2) 值的写法

  • 字面量:普通的值(数字,字符串,布尔)

    k: v: 字面量直接来写

    ​ 字符串默认不需要加上单引号或双引号

    ​ 双引号:不会转义字符串里的特殊字符;特殊字符会作为本身想表示的意思

    name: "zhangsan \n lisi"输出zhangsan 换行 lisi

    ​ 单引号:会转义特殊字符,特殊字符最终只是一个普通的字符串

    name: "zhangsan \n lisi"输出zhangsan \n lisi

  • 对象(属性和值): 键值对

    k: v: 对象还是k: v的模式

    1
    2
    3
    friends:
    lastName: zhangsan
    age: 20

    行内写法:

    1
    friends: {lastName: zhangsan, age: 18}
  • 数组(List, Set)

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

    1
    2
    3
    4
    pets: 
    - cat
    - dog
    - pig

    行内写法

    1
    pets: [cat, dog, pig ]

    3. 配置文件值注入

    配置文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    server:
    port: 8081

    person:
    lastName: zhangsan
    age: 18
    boss: false
    birthDay: 2017/12/12
    maps: {k1: v1, k2: 12}
    list:
    - lisi
    - zhaoliu
    dog:
    name: myDog
    age: 2

    javaBean:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /*
    * 将配置文件中配置的每一个属性的值,映射到这个组件中
    * @ConfigurationProperties:告诉Spring Boot将本类中的所有属性和配置文件中相关的配置进行绑定
    * prefix = "person":配置文件中哪个下面的所有属性进行一一映射
    *
    * 只有这个组件是容器中的组件,才能使用容器提供的@ConfigurationProperties功能
    * */
    @Component
    @ConfigurationProperties(prefix = "person")
    public class Person {
    private String lastName;
    private Integer age;
    private Boolean boss;
    private Date birthDay;
    private Dog dog;

    private Map<String, Object> maps;
    private List<Object> list;

    我们可以导入配置文件处理器,以后配置文件进行绑定就可以有提示:

    1
    2
    3
    4
    5
    6
    <!-- 导入配置文件处理器,配置文件进行绑定就会有提示 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
    </dependency>

    @ConfigurationProperties获取值和@Value获取值的区别

    @ConfigurationProperties @Value
    功能 批量注入配置文件中的属性 一个一个指定
    松散绑定(松散语法) 支持松散语法绑定 不支持松散语法绑定
    SpEL 不支持 支持
    JSR30数据校验 支持 不支持
    复杂类型封装 支持 不支持

    配置文件ymlproperties都可以获取值

    如果只是在某个业务逻辑中,需要获取一下某个配置文件中的某项值,使用@Value

    如果说,我们专门编写了一个javaBean来和配置文件进行映射,我们就直接使用@ConfigurationProperties

    配置文件注入值数据校验

    1
    2
    3
    4
    5
    6
    7
    @Component
    @ConfigurationProperties(prefix = "person")
    @Validated
    public class Person {
    // lastName必须为邮箱格式
    @Email
    private String lastName;

4. @PropertySource&ImportResource

@PropertySource:加载指定的配置文件

1
2
3
4
5
6
7
8
9
/*
* @ConfigurationProperties(prefix = "person")
* 默认从全局配置文件中获取值
* */
@PropertySource(value = {"classpath:person.properties"})
@Component
@ConfigurationProperties(prefix = "person")
//@Validated
public class Person {

@ImportResource:导入Spring的配置文件,让配置文件中的内容生效

Spring Boot里面没有Spring的配置文件,我们自己编写的配置文件,也不能自动识别;

想让Spring的配置文件生效,加载进来;

需要将@ImportResource标注在一个配置类上

1
2
3
4
5
6
7
8
9
@ImportResource(locations = {"classpath:beans.xml"})
@SpringBootApplication
public class SpringBoot01HelloworldQuickApplication {

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

}

导入Spring的配置文件,让其生效

但不希望编写Spring的配置文件

1
2
3
4
5
6
7
8
<?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 class="com.haven.springboot.sevice.HelloService" id="helloService">

</bean>
</beans>

SpringBoot推荐给容器中添加组件的方式: 推荐使用全注解方式

  1. 配置类===Spring配置文件
  2. 使用@Bean给容器中添加组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* @Configuration:告诉Spring Boot这是一个配置类
* 就是来替代之前Spring的配置文件
*
* 之前在Spring配置文件中用 <bean></bean> 标签添加组件
* */
@Configuration
public class MyAppConfig {

// 将方法的返回值添加到容器中,容器中这个组件默认的id就是方法名
@Bean
public HelloService helloService02(){
System.out.println("配置类@Bean给容器中添加组件");
return new HelloService();
}

}

5. 配置文件占位符

(1) 随机数

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

(2) 占位符获取之前配置的值,如果没有可以使用:指定默认值

1
2
3
4
5
6
7
8
9
person.last-name=张三${random.uuid}
person.age=${random.int}
person.birth-day=2017/12/15
person.boss=false
person.maps.k1=v1
person.maps.k2=14
person.list=a,b,c
person.dog.name=${person.hello:hello}_dog
person.dog.age=15

6. Profile

(1) 多Profile文件

我们在主配置文件编写时,文件名可以是application-{profile}.properties/yml

默认使用application.properties的配置

(2)激活指定profile

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

  • 命令行

    1
    --spring.profiles.active=dev

    Edit Configuration—>Program arguments

(3) yml多文档块

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

server:
port: 8083
spring:
profiles: dev

---

server:
port: 8084
spring:
profiles: prod #指定属于那个环境

7. 配置文件加载位置

优先级由高到低,高优先级会覆盖低优先级的配置:

  • ./config/
  • ./
  • ./src/main/resources/config/
  • ./src/main/resources/

SpringBoot会从这四个位置全部加载主配置文件:互补配置

可以通过spring.config.location来改变默认的配置文件的位置

项目打包好后,可以使用命令行参数--spring.config.location=?的形式,启动项目的时候来指定配置文件的新位置;指定配置文件和默认加载的配置文件共同起作用,形成互补配置

8. 自动配置原理

配置文件能配置的属性参照

https://docs.spring.io/spring-boot/docs/2.1.8.RELEASE/reference/html/common-application-properties.html

自动配置原理

  1. SpringBoot启动时加载主配置类,开启了自动配置功能@EnableAutoConfiguration

  2. @EnableAutoConfiguration原理

    • 利用AutoConfigurationImportSelector给容器中导入一些组件

    • 查看AutoConfigurationImportSelector中的selectImports()方法

    1
    2
    // 获取候选的配置
    List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
    1
    2
    // getCandidateConfigurations()
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
    1
    2
    3
    4
    SpringFactoriesLoader.loadFactoryNames()
    // 扫描所有jar包类路径下的 META-INF/spring.factories
    // 把扫描到的文件的内容包装成properties对象
    // 从properties获取到EnableAutoConfiguration.class的类名对应的值,然后把他们添加在容器中

    将类路径下 META-INF/spring.factories里面配置的所有EnableAutoConfiguration的值加到了容器中

    每一个这样的 xxxAutoConfiguration类都是容器中的一个组件,都加入到容器中,用他们做自动配置。

  3. 每一个自动配置类进行自动配置功能

  4. HttpEncodingAutoConfiguration(HTTP编码自动配置)为例,介绍自动配置原理

    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
    @Configuration	// 表示是一个配置类,可以给容器中添加组件
    @EnableConfigurationProperties({HttpProperties.class}) // 启用指定类的ConfigurationProperties功能;将配置文件中对应的值和HttpProperties绑定起来,并把HttpProperties加入到ioc容器中

    @ConditionalOnWebApplication(
    type = Type.SERVLET
    ) // Spring底层@Conditional注解,根据不同的条件,如果满足指定的条件,整个配置类里面的配置就会生效;
    // 判断当前应用是否是web应用,如果是,则当前配置类生效;

    // 判断当前项目有没有这个类
    // CharacterEncodingFilter:SpringMVC中进行乱码解决的过滤器
    @ConditionalOnClass({CharacterEncodingFilter.class})

    // 判断配置文件中是否存在某个配置 spring.http.encoding;如果不存在,判断也是成立的
    // 即使配置文件中不配置spring.http.encoding=enabled,也是默认生效的
    @ConditionalOnProperty(
    prefix = "spring.http.encoding",
    value = {"enabled"},
    matchIfMissing = true
    )
    public class HttpEncodingAutoConfiguration {
    // 他已经和SpringBoot的配置文件映射了
    private final Encoding properties;

    // 只有一个有参构造器的情况下,参数的值会从容器中拿
    public HttpEncodingAutoConfiguration(HttpProperties properties) {
    this.properties = properties.getEncoding();
    }

    @Bean // 给容器中添加一个组件,这个组件的某些值需要从properties中获取
    @ConditionalOnMissingBean
    public CharacterEncodingFilter characterEncodingFilter() {
    CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
    filter.setEncoding(this.properties.getCharset().name());
    filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
    filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
    return filter;
    }

    根据当前不同的条件判断,决定这个配置类是否生效。

    一旦配置类生效,这个配置类就会个容器中添加各种组件,这些组件是从对应的properties类中获取的,而这些类里面的每一个属性又是和配置文件绑定的。

  5. 所有在配置文件中能配置的属性,都是在xxxProperties类中封装着;配置文件能配置什么,就可以参照某个功能对应的属性类

    1
    2
    3
    4
    @ConfigurationProperties(	
    prefix = "spring.http"
    ) // 从配置文件中获取指定的值和bean的属性进行绑定
    public class HttpProperties {

精髓

  1. SpringBoot启动会加载大量的配置类

  2. 我们看我们的功能有没有SpringBoot默认写好的自动配置类;

  3. 再看这个自动配置类中到底配置了哪些组件(只要我们要用的组件有,我们就不需要再来配置)

  4. 给容器中自动配置类添加组件时,会从propertie中获取某些属性,我们就可以在配置文件中指定这些属性的值

    xxxAutoConfiguration:自动配置类,给容器中添加组件

    xxxProperties:封装配置文件中相关属性

9. 细节

1. @Conditional派生注解

作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置类里面的所有内容才生效

自动配置类必须在一定的条件下才能生效

通过

1
debug=true

让控制台打印自动配置报告,这样我们就可以很方便地知道哪些自动配置类生效

分为Positive matchesNegative matches

SpringBoot HelloWorld

SpringBoot HelloWorld

一个功能:

浏览器发送一个hello请求,浏览器接受请求并处理,响应hello字符串

1. 创建一个maven工程(jar)

2. 导入SpringBoot相关依赖

maven选择自动导入

1
2
3
4
5
6
7
8
9
10
11
12
13
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
</dependencies>

(1) 父项目

1
2
3
4
5
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
</parent>

它的父项目是

1
2
3
4
5
6
7
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath>../../spring-boot-dependencies</relativePath>
</parent>
来真正管理Spring Boot里的所有依赖

SpringBoot的版本仲裁中心;

导入依赖默认不需要写版本(没有在dependencies中进行管理的自然需要声明版本号)

(2) 导入的依赖

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>

Spring-boot-starter-web

  • spring boot场景启动器: 帮我们导入了web模块正常运行所依赖的组件

  • SpringBoot将所有功能场景都抽取出来,做成一个个starters(启动器),只需要在项目中引入这些starter,相关场景的所有依赖都会导入进来。要用什么功能就导入什么场景的启动器

3. 编写主程序

1
@SpringBootApplication	// 来标注一个主程序类,说明这是一个Spring Boot类

@SpringBootApplication:SpringBoot标注在某个类上,说明这个类是SpringBoot的主配置类。SpringBoot就应该运行这个类的main 方法来启动SpringBoot类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication
  • @SpringBootConfiguration:SpringBoot的配置类

    标注在某个类上,表示这是一个Spring Boot配置类

  • @Configuration:配置类上标注这个注解

    配置类==配置文件;配置类也是容器中的一个组件;@Component

  • @EnableAutoConfiguration:开启自动配置功能

    以前我们需要配置的东西,SpringBoot自动帮我们配置

    1
    2
    3
    @AutoConfigurationPackage
    @Import({AutoConfigurationImportSelector.class})
    public @interface EnableAutoConfiguration
  • @AutoConfigurationPackage:自动配置包

    @Import({Registrar.class})

    Spring的底层注解,@Import给容器中导入一个组件;导入的组件由Registrar.class

    将主配置类(@SpringBootApplication标注的类)的所在包及下面所有子包里面的所有组件都扫描到Spring容器中。

  • @Import({AutoConfigurationImportSelector.class})

    给容器中导入组件,

    AutoConfigurationImportSelector.class:导入哪些组件的选择器

    将所有需要导入的组件以全类名的方式返回;

    这些组件就会被添加到容器中;

    会给容器中导入非常多的自动配置类(xxxAutoConfiguration);就是给容器中导入这个场景需要的所有组件,并配置好这些组件

    有了自动配置类,就免去了手动编写配置注入功能组件的工作

    Spring Boot在启动时从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值,将这些值作为自动配置类导入到容器中,自动配置类就生效,帮我们进行自动配置工作

    J2EE的整体解决方案和自动配置都在spring-boot-autoconfiguration

4. 编写相关的Controller, Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.haven.Controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
* @author HavenTong
* @date 2019-09-09 00:56
*/

@Controller
public class HelloController {

@ResponseBody
@RequestMapping("/hello")
private String hello(){
return ("Hello World");
}
}

5. 运行主程序测试

6. 简化部署

https://docs.spring.io/spring-boot/docs/2.1.8.RELEASE/reference/html/getting-started-first-application.html#getting-started-first-application-executable-jar

11.5

导入Spring Boot的maven插件

1
2
3
4
5
6
7
8
9
<!-- 可以将应用打包成一个可执行的jar包    -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

侧边栏—>Maven—>项目—>LifeCycle—>package

1
java -jar

进行执行

7. 使用SpringBoot Initializer创建的项目

  • resources

    • static: 保存的所有静态资源 js, css, 图片

    • templates: 保存所有的模版页面。(Spring Boot默认jar包使用嵌入式的Tomcat,默认不支持jsp的页面);

      可以使用模版引擎(freemarker, thymeleaf);

    • **application.properties: **Spring Boot应用的配置文件