阅读更多
1 环境
IDEA
Maven 3.5.2
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"> <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.*;
@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;
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;
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); } }
|
@EnableAutoConfiguration
:能够自动配置spring的上下文,试图猜测和配置你想要的bean类,通常会自动根据你的类路径和你的bean定义自动配置
@ComponentScan
:会自动扫描指定包下的全部标有@Component的类,并注册成bean,当然包括@Component下的子注解@Service,@Repository,@Controller
@SpringBootApplication
:@SpringBootApplication = (默认属性)@Configuration + @EnableAutoConfiguration + @ComponentScan
测试
http://localhost:8080/home/
- 其他两个API可以通过post man测试
6 Bean的Java配置
从Spring3.0开始,就提供了一种与xml配置文件对称的Java版本的配置
核心注解
- @Configuration
- @Bean
- @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(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}
的占位符)有如下几种方式
- @Value注解
- xml配置文件中,例如
<property name = "Jack" value = "${jack.name}/>
注意,像logback配置文件(logback.xml
)中的属性占位符,Spring默认是不解析的。如果想要使其生效,可以采用如下方式
- 将
logback.xml
改名为logback-spring.xml
,就可以利用springProperty
元素来引入Spring属性值
<springProperty scope="context" name="fluentHost" source="myapp.fluentd.host"/>
- 其中,
source
的内容就是Spring属性文件中的属性名称
- 然后,就可以在logback的配置文件中引用
${fluentHost}
- 利用
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
。可选的匹配类型有如下几种
FilterType.ANNOTATION
:排除指定注解标记的Bean,注解的类用classes
属性指定
FilterType.ASSIGNABLE_TYPE
:排除指定类,用classes
属性指定
FilterType.ASPECTJ
:排除匹配指定模式的类,用pattern
属性指定ASPECTJ
格式的通配符
FilterType.REGEX
:排除匹配指定模式的类,用pattern
属性指定正则表达式
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
,被注入到其他依赖该bean
的bean
当中
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-free
的Auto-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 参考