前言
在上一篇文章Spring Boot 学习笔记1——初体验之3分钟启动你的Web应用已经对Spring Boot的基本体系与基本使用进行了学习,本文主要目的是更加进一步的来说明对于Spring Boot使用上的具体的细节以及使用上的最佳实践, 经过了几天的文档阅读和实验,将自己这几天的学习心得在这里记录下来。如果有不对的地方,请指正!
1.依赖管理的配置
1.1 依赖管理的原理及最佳实践
我们在使用Spring Boot时,通常最好的方式是继承spring-boot-starter-parent,然后指定其对应的版本。spring-boot-starter-parent主要完成如下工作:
- 定义项目的中常用的依赖Jar包及其版本
- 定义项目中常用的maven的插件及其版本
- 定义了默认的JDK编译级别、源文件字符编码等通用信息
- 敏感资源文件的过滤(敏感资源文件是指:application.properties 和application.yml,同是也包括application-foo.properties、 application-foo.yml等)
- 将maven中的占位符进行了重新定义为@,为了防止和Spring中的$占位符起冲突。
spring-boot-starter-parent的pom文件.png
通过上面的截图可以看到,Spring Boot也是通过maven项目之间的继承从而完成依赖的继承。spring-boot-dependencies 文件中几乎将我们平时开发常见的依赖全部给定义了,有了它,妈妈再也不用担心我找不到JAR包了。
spring-boot-dependencies的pom文件.png
这样做除了帮助对依赖有管理,其还有2个优点:
- 如果之后想对依赖进行升级,只需修改spring-boot-starter-parent的版本即可,其管理的第三方依赖即可以完成自动化的一致性升级。
- 帮助我们解决第三方框架由于版本不兼容而引发的冲突,稍微有点开发经验的朋友可能都有这样经历,常常因为项目中引入了框架的JAR包,但是由于它们互相之间版本不兼容而导致项目启动失败等问题,而现在这些版本之间管理都由Spring Boot来帮助我们完成,其互相之间出现冲突就基本上不复存在了。
因此,Spring官方强烈建议用户在使用Spring Boot,不要再对已经定义好的Spring版本进行重写。
ps:对于第三方依赖的版本重写问题,Spring官方没有给出强制的议要求不要重写,但是个人建议无需重写,除非你的项目中需要使用到第三方框架的新功能时再版本重写,进行升级。
在继承了spring-boot-starter-parent之后,如果我们的项目需要将spring和其他的第三方框架进行整合,则只需引入其对应的依赖描述符即可。比如我们要将Spring和JPA整合,则只需引入spring-boot-starter-data-jpa
,在自己项目的POM文件中加入如下该依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
其会通过maven的依赖传递性,将所有和Spring以及JPA相关的JAR包都可以自动引入。
1.2 依赖描述符的说明
依赖描述符也被称为Starter POM。所有由Spring官方所提供的Starter-POM文件,都以spring-boot-starter-*
来进行命名,这样的设计命名方式是帮助我们更加方便的进行定位查找(我们在以后的设计中也可以对此进行借鉴)。
如果我们自己所编写的框架和Spring整合构成一个类似spring-boot-starter-data-jpa
这样的一个模块,我们在命名时请使用*-spring-boot-starter
。比如:Spring Boot官方并没有提供Mybatis和Spring的快速整合框架,GitHub上就有老外提供了一个这样的框架,其名字为mybatis-spring-boot-starter
。所有官方和社区所提供的Starter POM都在 这里。大家可以到这里查看
2.Spring Boot 代码的标准结构
Spring Boot并没有强制我们将代码必须按照某种目录格式进行存放,但是其提供了一种最佳实践。官方建议将应用启动类放在项目其他类的根包下面,其对应的项目结构如下:
com
+- example
+- myproject
+- Application.java
|
+- domain
| +- Customer.java
| +- CustomerRepository.java
|
+- service
| +- CustomerService.java
|
+- web
+- CustomerController.java
在应用的启动类中使用@EnableAutoConfiguration注解来标识,该注解的作用我们之前说过,它可以自动根据当前classpath添加的JAR、和配置,自动进行运行环境的推断,然后帮助我们自动创建项目中需要的对象并加以配置,然后将其加入Spring容器中(比如在前面文章提及的EmbeddedServletContainerFactory,该对象是运行内嵌Tomcat所需要创建的)。除了该功能,它还有一个更加牛X的功能,它可以自动扫描使用@Entity注解所标注的包,然后告诉JPA的我们所定义的实体类的位置。(注:前提是按照官方给出的代码结构进行类的存放)
下面我们通过一个例子来证明一下@EnableAutoConfiguration是不是真的有文档中说的如此强大,我们以一个Spring和JPA整合的例子进行说明:
引入maven依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.3.RELEASE</version>
</parent>
<dependencies>
<!-- 加入JPA所需要的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 加入单元测试需要的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- 这里只是简单测试,因此我们使用h2数据库,该数据库是一个基于内存的关系型数据库,非常方便我们测试。
我们无需对该数据库进行安装
-->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
创建实体类,方便测试就简单定义一个City类:
package com.panlingxiao.springboot.jpa.domain;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class City implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue
private Long id;
private String name;
private String state;
private String country;
public City() {
}
public City(String name, String state, String country) {
super();
this.name = name;
this.state = state;
this.country = country;
}
public String getName() {
return this.name;
}
public String getState() {
return this.state;
}
public String getCountry() {
return this.country;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setState(String state) {
this.state = state;
}
public void setCountry(String country) {
this.country = country;
}
@Override
public String toString() {
return getId()+","+getName() + "," + getState() + "," + getCountry();
}
}
编写应用启动类:
package com.panlingxiao.springboot.jpa;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
/**
* 非常简单,只需一个注解就搞定一切
*/
@EnableAutoConfiguration
public class JpaApplication {
}
编写单元测试:
package com.panlingxiao.springboot.jpa.test;
import javax.persistence.EntityManager;
import javax.transaction.Transactional;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.panlingxiao.springboot.jpa.JpaApplication;
import com.panlingxiao.springboot.jpa.domain.City;
/**
* 使用Spring提供的Junit的运行器
*/
@RunWith(SpringJUnit4ClassRunner.class)
/**
* 指定配置类,用于加载Spring容器,在标准的Spring环境下,使用的是
* @ContextConfiguration ,但是在Spring Boot中该@SpringApplicationConfiguration来进行代替。
*/
@SpringApplicationConfiguration(classes=JpaApplication.class)
public class TestJpaApplication {
/**
* 注入EntityManager,@EnableAutoConfiguration注解会自动帮助我们创建,就是这么牛X
*/
@Autowired
private EntityManager em;
/**
*@EnableAutoConfiguration注解会自动帮助我们创建数据源
*/
@Autowired
private DataSource dataSource;
private static Logger LOGGER = LoggerFactory.getLogger(TestJpaApplication.class);
/**
* JPA底层使用了Hibernate的参考实现,其运行必须使用事务管理器
* 因此这里必须开启事务,否则运行出错。但是在单元测试换进下,默认
* 事务是回滚的,如果希望事务提交需要添加 @Rollback(false)
*/
@Transactional
@Test
public void testPersistCity(){
City city = new City("ShenZhen", "China","GuangDong");
em.persist(city);
//这里将数据源输入看看,它为我们提供的数据源是什么数据源
LOGGER.info("dataSource:{}",dataSource);
}
}
Spring Boot默认使用的日志界别为INFO,我们为了更加清晰看到JPA执行的结果,因此我们需要修改一下Hibernate的SQL日志级别。
在src/test/resources目录下创建application.properties文件,用于指定日志界别。
logging.level.org.hibernate.SQL=debug
从上面的例子的运行结果可以看到,Spring Boot通过@EnableAutoConfiguration可以隐式地自动帮助我们创建数据源、JPA的EntityManager,并且对其进行配置,同时还自动完成实体类所在包的搜索,这一切都会让我们的开发更简单。
3. 基于注解的容器配置
3.1 使用@Configuration配置Spring容器
Spring Boot虽然可以支持通过xml进行对Spring容器,但是官方通常建议我们将容器的主要配置。并且推荐将@Configuration标注在应用的启动类之上。
3.2 导入其他配置类
使用过Spring的朋友都应用知道,通常我们不会将所有的配置信息写在一个文件中,而是根据功能进行划分成多个不同的配置,然后在主配置中导入。使用@Configuration也同样如此,我们同样可以将Bean的定义根据功能和作用的不同,分别定义在不同的配置类中,然后通过@Import注解导入其他额外的配置类,其所提供的功能与xml中定义的<import/>功能是一样的。
下面通过一个例子来说明,其实这个例子的主要目的是为了说明@Import使用时的注意事项。
为了简单测试功能,因此在这里定义一个简单的Bean:
package com.plx.spring.chapter3.annotation.ioc.bean;
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
编写主配置类:
package com.plx.spring.chapter3.annotation.ioc;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import com.plx.spring.chapter3.annotation.ioc.bean.Person;
import com.plx.spring.chapter3.annotation.ioc.sub.MyAppSubConfig;
/**
* 定义容器的主配置类
*/
@Configuration
//引入其他的配置类
@Import({MyAppSubConfig.class})
public class MyAppConfig {
/**
* 在主配置类中
* @return
*/
@Bean(name="p1")
public Person getPerson(){
Person person = new Person();
person.setName("Main");
return person;
}
}
编写容器的其他配置类:
package com.plx.spring.chapter3.annotation.ioc.sub;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.plx.spring.chapter3.annotation.ioc.bean.Person;
@Configuration
public class MyAppSubConfig {
@Bean(name="p2")
public Person getPerson(){
Person p =new Person();
p.setName("Sub");
return p;
}
}
测试代码:
package com.plx.spring.chapter3.annotation.ioc;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.plx.spring.chapter3.annotation.ioc.bean.Person;
public class AnnotationIocTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
MyAppConfig.class);
Person p1 = ctx.getBean("p1", Person.class);
System.out.println(p1.getName());
Person p2 = ctx.getBean("p2", Person.class);
System.out.println(p2.getName());
}
}
运行结果如下:
需要注意的是,使用@Import导入的类必须满足符合以下的某一个条件:
- 导入的类使用@Configuration进行标注
- 导入的类中至少有一个使用@Bean标准的方法
- 导入的类实现了ImportSelector接口
- 导入的类实现了ImportBeanDefinitionRegistrar接口
否则运行将会出现如下异常:
另一种更加简单的导入的方式可以使用@ComponentScan去扫描指定包下的类,对于使用@Configuration所标识的类也会被@ComponentScan所扫描处理,由于比较简单,这里就不演示了。
3.3 导入XML配置
即使我们将所有的Bean的定义都写在了xml文件中,Spring Boot还是建议我们我们使用@Configuration来完成配置文件的加载。通过使用@ImportResource就可以完成xml文件的加载。
3.4 @EnableAutoConfiguration使用总结
前面已经好几次使用到了@EnableAutoConfiguration,下面我们对其功能做一个比较全面的总结:
@EnableAutoConfiguration的作用是开启Spring的Application Context的自动配置功能,它会根据当前classpath下我们所添加的Jar包以及我们自己定义的类,然后进行推测我们可能所需要的Bean,最后创建这些我们需要的Bean,然后加入到Spring的Application Context中。
Spring Boot的自动装配功能做得非常灵活,它可以让用户手动指定对于哪些类不需要自动装配,通过在注解上指定
exclue
或者excludeName
来让Spring对这里类的自动创建,也可以在配置文件中指定spring.autoconfigure.exclude
属性来进行排除;如果用户在容器中自己定义了一个Spring容器将要会自动装配对象,Spring容器将会取消对该Bean的自动装配,而是使用用户所提供的Bean,因为自动装配的的过程是在用户定义的Bean在容器中注册完成后才发生的。在使用时通常将该注解标注在主配置类上,并且主配置类放在根包中,方面对其他类以及子包中的类进行查找和扫描。
前面我们在测试JPA的时候,看到Spring Boot默认情况下帮助我们创建的数据源是使用Tomcat所提供的数据源,目前我们现在用得比较多的可能是阿里的druid数据源,下面通过这个例子来实现使用自己提供的数据源来替换Spring Boot的默认配置。
我们首先需要引入Druid的依赖,在原来项目的POM中添加如下内容
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.9</version>
</dependency>
将原来启动程序进行修改:
package com.panlingxiao.springboot.jpa;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import com.alibaba.druid.pool.DruidDataSource;
/**
* 非常简单,只需一个注解就搞定一切
*/
@EnableAutoConfiguration
public class JpaApplication {
/**
* application.proeprties会被Spring设置到Enviroment
* 通过env可以读取用户配置的属性信息和系统属性
*/
@Autowired
private Environment env;
/**
* 在配置类自定定义DataSource,在用户自己定义了之后,
* Spring Boot将不会实现对数据源的自动配置功能
* @return
*/
@Bean
public DataSource getDruidDataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(env.getProperty("jdbc.driver"));
dataSource.setUrl(env.getProperty("jdbc.url"));
dataSource.setUsername(env.getProperty("jdbc.user"));
dataSource.setPassword(env.getProperty("jdbc.password"));
return dataSource;
}
}
修改application.proeprties,设置数据库的连接信息,因为Spring Boot不再帮助我们创建数据源,因此我们需要自己指定数据库信息。
logging.level.org.hibernate.SQL=debug
jdbc.driver=org.h2.Driver
jdbc.url= jdbc:h2:mem:testdb
jdbc.username=sa
jdbc.password=sa
运行结果:
从上面的输出结果可以看到,Spring Boot发现我们自己定义了数据源后,就不会再做自动配置,而是使用我们自定义的数据源,从这点我们也可以看出Spring Boot在设计的时候确实是非侵入式的设计。
3.5 扩展问题思考
既然Spring Boot可以发现完成当H2的JAR包出现在Classpath中,就可以自动创建数据源,然后提供给JPA使用。那么我们能否让其发现我们需要的JAR包,也来完成自动配置,从而能够达到一劳永逸的效果,我们该如何实现呢?
这个问题本人目前还没有解决,但我的思路是首先看看其是否提供了默认的扩展机制,如果有是最理想的;如果没有,则需要进一步源码其源码,通过修改其源码来实现。
3.6 使用@SpringBootApplication
因为我们通常会在应用启动类上同时使用@Configuration, @EnableAutoConfiguration和@ComponentScan,Spring Boot为了方便将这3个注解集成在一起,形成了@SpringBootApplication,在上一节我们已经看过该注解的源码实现了,故这里不再细说。@SpringApplication可以通过exclude和excludeName属性排除自动装配的对象,通过scanBasePackages属性设置指定的包进行扫描。
4. 热部署
由于Spring Boot应用通常都是一个普通的Java Application,如果我们在开发中修改了源代码而不希望关闭重启,Spring Boot也为我们提供了一个工具,完成应用的热部署功能,我们只需要引入spring-boot-devtools
模块就可以完成应用的热部署效果。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
使用spring-boot-devtools会监控ClassPath下资源的改变,对于大多数资源文件一旦发生变化,它就会触发自动重启功能,但是对于特殊的一些目录下的资源文件发生改变这些目录分别为/META-INF/maven, /META-INF/resources ,/resources,/static,/public ,/templates(注意:这些目录文件都位于classpath下),spring-boot-devtools是不会触发重启应用的,但是会触发这些资源文件的重新加载。
如果修改了static目录下的a.txt文件是不会触发自动重启功能的,用户可以通过spring.devtools.restart.exclude
属性自定义不触发重启的目录。当在application.properties中加入如下内容:
spring.devtools.restart.exclude=public/**
此时,修改static目录下的文件就会自动触发重启,但是当修改public目录下的文件依旧不会触发重启。如果既想要保持默认的哪些目录,并且同时还想添加一些目录不触发重启,可以通过spring.devtools.restart.additional-exclude
来进行指定。
5. 参考内容
- http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/
- https://github.com/spring-projects/spring-boot/tree/master/spring-boot-samples
至此,Spring Boot的基本使用以及使用上的最佳实践已经讨论完了,后面将进一步讨论如何对Spring Boot进行定制化和细粒度的修改从而满足我们自己的需求以及和各个模块的整合使用。用心写作真的不易,这篇文章我放假整整写了3天,如果觉得对您有点帮助,请点个喜欢,就我对我写作最大的鼓励,谢谢!
链接:https://www.jianshu.com/p/cb0f96958919