0%

Spring-Boot-Demo

阅读更多

1 环境

  1. IDEA
  2. Maven 3.5.2
  3. Spring Boot

2 Demo工程目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.
├── pom.xml
└── src
└── main
└── java
└── org
└── liuyehcf
└── spring
└── boot
├── SampleApplication.java
├── controller
│   └── SampleController.java
└── dto
├── LoginRequestDTO.java
└── LoginResponseDTO.java

3 pom文件

3.1 继承自spring-boot

pom文件可以直接继承自org.springframework.boot:spring-boot-starter-parent

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- 继承自Spring Boot Parent -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
</parent>

<modelVersion>4.0.0</modelVersion>

<artifactId>spring-boot</artifactId>

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

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.5.9.RELEASE</version>
<configuration>
<fork>true</fork>
<mainClass>org.liuyehcf.spring.boot.SampleApplication</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

注意,如果是Web应用的话,org.springframework.boot:spring-boot-starter-web是必须的,这个依赖项包含了内嵌的Tomcat容器

3.2 不继承自spring-boot

如果不想继承自org.springframework.boot:spring-boot-starter-parent,那么需要通过<dependencyManagement>元素引入依赖

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<groupId>org.liuyehcf</groupId>
<artifactId>spring-boot</artifactId>
<version>1.0-SNAPSHOT</version>
<modelVersion>4.0.0</modelVersion>

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

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.5.9.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.5.9.RELEASE</version>
<configuration>
<fork>true</fork>
<mainClass>org.liuyehcf.spring.boot.SampleApplication</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

4 Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
package org.liuyehcf.spring.boot.controller;

import org.liuyehcf.spring.boot.dto.LoginRequestDTO;
import org.liuyehcf.spring.boot.dto.LoginResponseDTO;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

/**
* Created by HCF on 2017/11/24.
*/
@Controller
@RequestMapping("/")
public class SampleController {

@RequestMapping(value = "/home", method = RequestMethod.GET)
@ResponseBody
public String home() {
return "Hello world!";
}

@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public LoginResponseDTO login(@RequestBody LoginRequestDTO request) {
LoginResponseDTO loginResponse = new LoginResponseDTO();
loginResponse.setState("OK");
loginResponse.setMessage("欢迎登陆" + request.getName());
return loginResponse;
}

@RequestMapping(value = "/compute", method = RequestMethod.GET)
@ResponseBody
public String compute(@RequestParam String value1,
@RequestParam String value2,
@RequestHeader String operator) {
switch (operator) {
case "+":
return Float.toString(
Float.parseFloat(value1)
+ Float.parseFloat(value2));
case "-":
return Float.toString(
Float.parseFloat(value1)
- Float.parseFloat(value2));
case "*":
return Float.toString(
Float.parseFloat(value1)
* Float.parseFloat(value2));
default:
return "wrong operation";
}
}
}

4.1 DTO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.liuyehcf.spring.boot.dto;

/**
* Created by Liuye on 2017/12/15.
*/
public class LoginRequestDTO {
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
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
package org.liuyehcf.spring.boot.dto;

/**
* Created by Liuye on 2017/12/15.
*/
public class LoginResponseDTO {
private String state;

private String message;

public String getState() {
return state;
}

public void setState(String state) {
this.state = state;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
}

5 Application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.liuyehcf.spring.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;

@EnableAutoConfiguration
@ComponentScan("org.liuyehcf.*")
public class SampleApplication {

public static void main(String[] args) throws Exception {
SpringApplication.run(SampleApplication.class, args);
}
}
  1. @EnableAutoConfiguration:能够自动配置spring的上下文,试图猜测和配置你想要的bean类,通常会自动根据你的类路径和你的bean定义自动配置
  2. @ComponentScan:会自动扫描指定包下的全部标有@Component的类,并注册成bean,当然包括@Component下的子注解@Service,@Repository,@Controller
  3. @SpringBootApplication:@SpringBootApplication = (默认属性)@Configuration + @EnableAutoConfiguration + @ComponentScan

测试

  1. http://localhost:8080/home/
  2. 其他两个API可以通过post man测试

6 Bean的Java配置

从Spring3.0开始,就提供了一种与xml配置文件对称的Java版本的配置

核心注解

  1. @Configuration
  2. @Bean
  3. @Value

6.1 情景1

1
2
3
<bean id="service" class="org.liuyehcf.springboot.Service">
<property name="name" value="${my.name}" />
</bean>

这对这种配置,建议直接在Service类的name属性上标记@Value注解。如下

1
2
3
4
5
6
7
8
9
10
11
@Component
public class Service{
@Value("${my.name}")
private String name;

@PostConstruct
public void init() {
...
}
...
}

属性注入优先于@PostConstruct

6.2 情景2

1
2
3
4
5
<bean id="service" class="org.liuyehcf.springboot.Service">
<constructor-arg name="name" value="${my.name}" />
<constructor-arg name="age" value="${my.age}" />
<constructor-arg name="robot" ref="${my.robot}" />
</bean>

对于这种配置,也可以在构造方法的参数列表中加上@Value注解,以及@Autowired注解。如下

1
2
3
4
5
6
7
8
9
10
@Component
public class Service{
public Service(@Value("${my.name}") String name,
@Value("${my.name}") String name,
@Autowired("${my.robot}") Robot robot){
...
}

...
}

注意,构造方法注入对象,只能用@Autowired而不能用@Resource

6.3 情景3

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
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"  destroy-method="close">  
<property name="driverClassName" value="${db.driverClass}"></property>
<property name="url" value="${db.url}"></property>
<property name="username" value="${db.username}"></property>
<property name="password" value="${db.password}"></property>
</bean>

<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<bean id="transactionTemplate"
class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager"/>
</bean>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:dal/mybatis_config.xml"/>
</bean>

<bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="org.liuyehcf"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>

<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>

对于这种三方类而言,我们没法在源码上增加注解来注入属性或者对象,我们可以通过@Bean注解来配置。如下

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
@Configuration
@MapperScan(basePackages = "org.liuyehcf", sqlSessionFactoryRef = "sqlSessionFactory")
@EnableTransactionManagement
public class DataSourceConfig{
//@Bean注解还有initMethod属性
@Bean(name = "dataSource", destroyMethod = "close")
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("${db.url}");
dataSource.setUsername("${db.username}");
dataSource.setPassword("${db.password}");
return dataSource;
}

@Bean(name = "transactionManager")
public DataSourceTransactionManager transactionManager() {
DataSourceTransactionManager manager = new DataSourceTransactionManager();
manager.setDataSource(dataSource());

return manager;
}

@Bean(name = "transactionTemplate")
public TransactionTemplate transactionTemplate() {
TransactionTemplate template = new TransactionTemplate();
template.setTransactionManager(transactionManager());
return template;
}

@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource());
sqlSessionFactoryBean.setConfigLocation(new ClassPathResource("dal/mybatis_config.xml"));
return sqlSessionFactoryBean.getObject();
}
}

Spring会为DataSourceConfig生成代理类(Cglib),不用担心多次调用dataSource()方法会创建多个不同的对象

注意,MapperScannerConfigurer的等效配置必须用@MapperScan注解,否则,整个配置类就会有问题(原因尚不清楚)

此外,<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>的等效配置,不知道是不是@EnableTransactionManagement

6.4 情景4

1
2
3
4
<bean id="tool" class="com.baeldung.factorybean.ToolFactory">
<property name="factoryId" value="9090"/>
<property name="toolId" value="1"/>
</bean>

对于FactoryBean,我们仍然可以像配置普通Bean一样配置它。注意必须返回FactoryBean(不要调用getObject()返回Bean对象),如果这个FactoryBean实现了一些Aware接口,那么在生成FactoryBean对象时会进行一些额外操作,然后再调用getObject方法创建Bean

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class FactoryBeanAppConfig {

@Bean(name = "tool")
public ToolFactory toolFactory() {
ToolFactory factory = new ToolFactory();
factory.setFactoryId(7070);
factory.setToolId(2);
return factory;
}
}

7 属性注入

Spring的属性注入(形如${xxx.yyy.zzz}的占位符)有如下几种方式

  1. @Value注解
  2. xml配置文件中,例如<property name = "Jack" value = "${jack.name}/>

注意,像logback配置文件(logback.xml)中的属性占位符,Spring默认是不解析的。如果想要使其生效,可以采用如下方式

  1. logback.xml改名为logback-spring.xml,就可以利用springProperty元素来引入Spring属性值
    • <springProperty scope="context" name="fluentHost" source="myapp.fluentd.host"/>
    • 其中,source的内容就是Spring属性文件中的属性名称
    • 然后,就可以在logback的配置文件中引用${fluentHost}
  2. 利用property元素导入Spring属性配置文件
    • <property resource="application.properties"/>
    • 这种方式不需要Spring配合,完全是logback的一种方式

8 Https

application.properties中配置如下配置项即可

1
2
3
4
5
6
7
8
9
10
11
#https端口号.
server.port: 443
#证书的路径.
server.ssl.key-store: classpath:liuyehcf_server_ks
#证书密码,请修改为您自己证书的密码.
server.ssl.key-store-password: 123456
server.ssl.key-password: 123456
#秘钥库类型
server.ssl.keyStoreType: PKCS12
#证书别名
server.ssl.keyAlias: liuyehcf_server_key

其中,KeyStore的生成命令如下

1
keytool -genkey -v -alias liuyehcf_server_key -keyalg RSA -keystore ~/liuyehcf_server_ks -storetype PKCS12 -dname "CN=localhost,OU=cn,O=cn,L=cn,ST=cn,C=cn" -storepass 123456

9 Test

9.1 @ComponentScan.excludeFilters

当我们在项目中需要做集成测试的时候,我们可以选择h2 database来代替mysql数据库,但通常数据源的配置仍然包含在指定的包扫描路径下。那么如何让Spring加载h2 database的数据源配置,而不是加载mysql的数据源配置呢?

我们可以用@ComponentScan注解的excludeFilters属性来实现这个目标@ComponentScan注解可以指定排除某个或某些Bean。可选的匹配类型有如下几种

  1. FilterType.ANNOTATION:排除指定注解标记的Bean,注解的类用classes属性指定
  2. FilterType.ASSIGNABLE_TYPE:排除指定类,用classes属性指定
  3. FilterType.ASPECTJ:排除匹配指定模式的类,用pattern属性指定ASPECTJ格式的通配符
  4. FilterType.REGEX:排除匹配指定模式的类,用pattern属性指定正则表达式
  5. FilterType.CUSTOM:即用户自定义的org.springframework.core.type.filter.TypeFilter

配置示例:排除项目中的数据源配置

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
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {TestApplication.class})
public class BaseConfig {
}

@Configuration
@MapperScan(basePackages = {"xxx.yyy.zzz"})
public class TestEmbeddedDatabaseConfig {

@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("db/create_db.sql")
.build();
}

@Bean
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}

@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource());
sessionFactory.setTypeAliasesPackage("xxx.yyy.zzz");
return sessionFactory.getObject();
}

@Bean
public SqlSessionTemplate sqlSessionTemplate() throws Exception {
return new SqlSessionTemplate(sqlSessionFactory());
}
}

@SpringBootApplication
@ComponentScan(basePackages = {"xxx.yyy.aaa", "xxx.yyy.bbb"},
excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {DataSourceConfig.class, Application.class})})
@PropertySource("classpath:application-test.properties")
public class TestApplication {

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

注意,在上面的@ComponentScan注解中,排除了两个类,一个是DataSourceConfig,即数据源配置;另一个是Application。这么做是有必要的,如果仅排除了DataSourceConfig(仅对当前@ComponentScan有效),Application仍然会被扫描到,而Application是应用的启动类,也会配置@ComponentScan注解,仍然会扫描到DataSourceConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
# 不配置excludeFilters属性
TestApplication
├── Application
| ├── DataSourceConfig
├── DataSourceConfig

# 配置了excludeFilters属性,但只排除了DataSourceConfig
TestApplication
├── Application
| ├── DataSourceConfig

# 配置了excludeFilters属性,同时排除了DataSourceConfig以及Application
TestApplication

9.2 @ContextHierarchy

9.3 Spring集成测试&Mockito

@MockBean:生成一个mock对象,并且添加到Spring的上下文中,将替换掉原有的bean,被注入到其他依赖该beanbean当中

9.4 在测试类中定义Controller

将注解SpringBootTest的属性值webEnvironment设置为SpringBootTest.WebEnvironment.DEFINED_PORT,例如:

  • @SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
  • 否则,会忽视server.port属性值,使用一个随机端口来提供http服务

10 配置项

SpringBoot推崇约定大于配置,通常情况下,我们只需要配置少数几个参数,应用就可以正常启动。但是,知道SpringBoot究竟提供了多少默认的配置也是很有用的,给一个传送门。在页面上搜索server.port=8080,就能定位到配置项说明的地方

此外,spring不同组件的配置项可以参考spring-configuration-metadata.json,该文件位于org.springframework.boot:spring-boot-autoconfigure模块当中

SpringBoot默认加载的属性文件,其路径为classpath:application.properties或者classpath:application.yml。若要修改这个路径,必须用@PropertySource注解来标注(而不是用@ImportResource注解哦)

11 Auto-Configuration

Spring集成了非常多的优秀项目,我们在使用这些项目时,仅仅只需要引入相关的依赖即可(对于Spring-Boot集成的项目,通常有spring-boot-starter后缀),例如Flowable

1
2
3
4
5
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>6.3.0</version>
</dependency>

我们无需做任何配置,Spring就会为我们自动初始化这些项目。那么Spring如何实现这种code-freeAuto-Configuration呢?

答案就是基于约定,Spring会默认加载classpath:META-INF/spring.factories这个配置文件(加载的代码在org.springframework.core.io.support.SpringFactoriesLoader类中)

12 排错

当我采用第二种pom文件时(不继承spring boot的pom文件),启动时会产生如下异常信息

1
2
3
...
Caused by: java.lang.NoSuchMethodError: org.springframework.web.accept.ContentNegotiationManagerFactoryBean.build()Lorg/springframework/web/accept/ContentNegotiationManager;
...

这是由于我在项目的父pom文件中引入了5.X.X版本的Spring依赖,这与spring-boot-dependencies引入的Spring依赖会冲突(例如,加载了低版本的class文件,但是运行时用到了较高版本特有的方法,于是会抛出NoSuchMethodError),将项目父pom文件中引入的Spring的版本改为4.3.13.RELEASE就行

13 参考