什么是 Spring Boot
Spring Boot 是由 Pivotal 团队提供的基于 Spring 的全新框架,其设计目的是为了简化 Spring 应用的搭建和开发过程。该框架遵循“约定大于配置”原则,采用特定的方式进行配置,从而使开发者无需定义大量的 XML 配置。通过这种方式,Spring Boot 致力于在蓬勃发展的快速应用开发领域成为领导者。
Spring Boot 并不重复造轮子,而且在原有 Spring 的框架基础上封装了一层,并且它集成了一些类库,用于简化开发。换句话说,Spring Boot 就是一个大容器。
下面几张图展示了官网上提供的 Spring Boot 所集成的所有类库:
Spring Boot 官方推荐使用 Maven 或 Gradle 来构建项目,本文这里使用Maven。毕竟Maven企业用户已经占多数。
第一个 Spring Boot 项目
大多数教程都是以 Hello World 入门,本教程也不例外,接下来,我们就来搭建一个最简单的 Spring Boot 项目。
首先创建一个 Maven 工程,请看下图:
然后在 pom.xml 加入 Spring Boot 依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
创建一个 Controller 类 HelloController:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication
public class HelloController
@RequestMapping("hello")
String hello() {
return "Hello World!";
}
public static void main(String[] args) {
SpringApplication.run(HelloController.class, args);
}
}
运行 main 方法,Spring Boot 默认会启动自带的 Tomcat 容器,启动成功后,浏览器访问:http://localhost:8080/hello,则会看到下图:
我们可以注意到,没有写任何的配置文件,更没有显示的使用任何容器,它是如何启动程序的呢,具体原理我将在下面具体分析。
这里我们可以初步分析出,Spring Boot 提供了默认的配置,在启动类里加入 @SpringBootApplication
注解,则这个类就是整个应用程序的启动类。
properties 和 yaml
Spring Boot 整个应用程序只有一个配置文件,那就是 .properties
或 .yml
文件。但是,在前面的示例代码中,我们并没有看到该配置文件,那是因为 Spring Boot 对每个配置项都有默认值。当然,我们也可以添加配置文件,用以覆盖其默认值,这里以 .properties
文件为例,首先在 resources 下新建一个名为 application.properties(注意:文件名必须是 application)的文件,键入内容为:
server.port=8081
server.servlet.context-path=/api
并且启动 main 方法,这时程序请求地址则变成了:http://localhost:8081/api/hello。
Spring Boot 支持 properties 和 yaml 两种格式的文件,文件名分别对应 application.properties 和 application.yml,下面贴出 yaml 文件格式供大家参考:
server:
port: 8080
servlet:
context-path: /api
可以看出 properties 是以逗号隔开,而 yaml 则换行+ tab 隔开,这里需要注意的是冒号后面必须空格,否则会报错。yaml 文件格式更清晰,更易读,这里建议大家都采用 yaml 文件来配置。
我这里的所有配置均采用 yaml 文件。
打包、运行
Spring Boot 打包分为 war 和 jar两个格式,下面将分别演示如何构建这两种格式的启动包。
在 pom.xml 加入如下配置:
<packaging>war</packaging>
<build>
<finalName>index</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>2.5</version>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
这个时候运行 mvn package 就会生成 war 包,然后放到 Tomcat 当中就能启动,但是我们单纯这样配置在 Tomcat 是不能成功运行的,会报错,需要通过编码指定 Tomcat 容器启动,修改 HelloController 类:
@RestController
@SpringBootApplication
public class HelloController extends SpringBootServletInitializer{
@RequestMapping("hello")
String hello() {
return "Hello World!";
}
public static void main(String[] args) {
SpringApplication.run(HelloController.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
}
这时再打包放到 Tomcat,启动就不会报错了。
接下来我们继续看如果达成 jar 包,在 pom.xml 加入如下配置:
<packaging>jar</packaging>
<build>
<finalName>api</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<mainClass>com.lynn.yiyi.Application</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>2.5</version>
<configuration>
<encoding>UTF-8</encoding>
<useDefaultDelimiters>true</useDefaultDelimiters>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
然后通过 mvn package 打包,最后通过 java 命令启动:
java -jar api.jar
这样,最简单的 Spring Boot 就完成了,但是对于一个大型项目,这是远远不够的,Spring Boot 的详细操作可以参照官网。
下面展示一个最基础的企业级 Spring Boot 项目的结构:
其中,Application.java 是程序的启动类,Startup.java 是程序启动完成前执行的类,WebConfig.java 是配置类,所有 bean 注入、配置、拦截器注入等都放在这个类里面。
我们知道,Spring Boot 是一个大容器,它将很多第三方框架都进行了集成,我们在实际项目中用到哪个模块,再引入哪个模块。比如我们项目中的持久化框架用 MyBatis,则在 pom.xml 添加如下依赖:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.40</version>
</dependency>
yaml/properties 文件
我们知道整个 Spring Boot 项目只有一个配置文件,那就是 application.yml,Spring Boot 在启动时,就会从 application.yml 中读取配置信息,并加载到内存中。上一篇我们只是粗略的列举了几个配置项,其实 Spring Boot 的配置项是很多的,本文我们将学习在实际项目中常用的配置项(注:为了方便说明,配置项均以 properties 文件的格式写出,后续的实际配置都会写成 yaml 格式)。
配置项 | 说明 | 举例 |
---|---|---|
server.port | 应用程序启动端口 | server.port=8080,定义应用程序启动端口为8080 |
server.context-path | 应用程序上下文 | server.port=/api,则访问地址为:http://ip:port/api |
spring.http.multipart.maxFileSize | 最大文件上传大小,-1为不限制 | spring.http.multipart.maxFileSize=-1 |
spring.jpa.database | 数据库类型 | spring.jpa.database=MYSQL,指定数据库为mysql |
spring.jpa.properties.hibernate.dialect | hql方言 | spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect |
spring.datasource.url | 数据库连接字符串 | spring.datasource.url=jdbc:mysql://localhost:3306/database?useUnicode=true&characterEncoding=UTF-8&useSSL=true |
spring.datasource.username | 数据库用户名 | spring.datasource.username=root |
spring.datasource.password | 数据库密码 | spring.datasource.password=root |
spring.datasource.driverClassName | 数据库驱动 | spring.datasource.driverClassName=com.mysql.jdbc.Driver |
spring.jpa.showSql | 控制台是否打印sql语句 | spring.jpa.showSql=true |
下面是我参与的某个项目的 application.yml 配置文件内容:
server:
port: 8080
context-path: /api
tomcat:
max-threads: 1000
min-spare-threads: 50
connection-timeout: 5000
spring:
profiles:
active: dev
http:
multipart:
maxFileSize: -1
datasource:
url: jdbc:mysql://localhost:3306/database?useUnicode=true&characterEncoding=UTF-8&useSSL=true
username: root
password: root
driverClassName: com.mysql.jdbc.Driver
jpa:
database: MYSQL
showSql: true
hibernate:
namingStrategy: org.hibernate.cfg.ImprovedNamingStrategy
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL5Dialect
mybatis:
configuration:
#配置项:开启下划线到驼峰的自动转换. 作用:将数据库字段根据驼峰规则自动注入到对象属性。
map-underscore-to-camel-case: true
以上列举了常用的配置项,所有配置项信息都可以在官网中找到,本课程就不一一列举了。
多环境配置
在一个企业级系统中,我们可能会遇到这样一个问题:开发时使用开发环境,测试时使用测试环境,上线时使用生产环境。每个环境的配置都可能不一样,比如开发环境的数据库是本地地址,而测试环境的数据库是测试地址。那我们在打包的时候如何生成不同环境的包呢?
这里的解决方案有很多:
- 每次编译之前手动把所有配置信息修改成当前运行的环境信息。这种方式导致每次都需要修改,相当麻烦,也容易出错。
- 利用 Maven,在 pom.xml 里配置多个环境,每次编译之前将 settings.xml 里面修改成当前要编译的环境 ID。这种方式会事先设置好所有环境,缺点就是每次也需要手动指定环境,如果环境指定错误,发布时是不知道的。
- 第三种方案就是本文重点介绍的,也是作者强烈推荐的方式。
首先,创建 application.yml 文件,在里面添加如下内容:
spring:
profiles:
active: dev
含义是指定当前项目的默认环境为 dev,即项目启动时如果不指定任何环境,Spring Boot 会自动从 dev 环境文件中读取配置信息。我们可以将不同环境都共同的配置信息写到这个文件中。
然后创建多环境配置文件,文件名的格式为:application-{profile}.yml,其中,{profile} 替换为环境名字,如 application-dev.yml,我们可以在其中添加当前环境的配置信息,如添加数据源:
spring:
datasource:
url: jdbc:mysql://localhost:3306/database?useUnicode=true&characterEncoding=UTF-8&useSSL=true
username: root
password: root
driverClassName: com.mysql.jdbc.Driver
这样,我们就实现了多环境的配置,每次编译打包我们无需修改任何东西,编译为 jar 文件后,运行命令:
java -jar api.jar --spring.profiles.active=dev
其中 --spring.profiles.active
就是我们要指定的环境。
常用注解
我们知道,Spring Boot 主要采用注解的方式,在上一篇的入门实例中,我们也用到了一些注解。
本文,我将详细介绍在实际项目中常用的注解。
@SpringBootApplication
我们可以注意到 Spring Boot 支持 main 方法启动,在我们需要启动的主类中加入此注解,告诉 Spring Boot,这个类是程序的入口。如:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
如果不加这个注解,程序是无法启动的。
我们查看下 SpringBootApplication 的源码,源码如下:
@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 {
/**
* Exclude specific auto-configuration classes such that they will never be applied.
* @return the classes to exclude
*/
@AliasFor(annotation = EnableAutoConfiguration.class, attribute = "exclude")
Class<?>[] exclude() default {};
/**
* Exclude specific auto-configuration class names such that they will never be
* applied.
* @return the class names to exclude
* @since 1.3.0
*/
@AliasFor(annotation = EnableAutoConfiguration.class, attribute = "excludeName")
String[] excludeName() default {};
/**
* Base packages to scan for annotated components. Use {@link #scanBasePackageClasses}
* for a type-safe alternative to String-based package names.
* @return base packages to scan
* @since 1.3.0
*/
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
/**
* Type-safe alternative to {@link #scanBasePackages} for specifying the packages to
* scan for annotated components. The package of each class specified will be scanned.
* <p>
* Consider creating a special no-op marker class or interface in each package that
* serves no purpose other than being referenced by this attribute.
* @return base packages to scan
* @since 1.3.0
*/
@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};
}
在这个注解类上有3个注解,如下:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
因此,我们可以用这三个注解代替 SpringBootApplication,如:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
其中,SpringBootConfiguration 表示 Spring Boot 的配置注解,EnableAutoConfiguration 表示自动配置,ComponentScan 表示 Spring Boot 扫描 Bean 的规则,比如扫描哪些包。
@Configuration
加入了这个注解的类被认为是 Spring Boot 的配置类,我们知道可以在 application.yml 设置一些配置,也可以通过代码设置配置。
如果我们要通过代码设置配置,就必须在这个类上标注 Configuration 注解。如下代码:
@Configuration
public class WebConfig extends WebMvcConfigurationSupport{
@Override
protected void addInterceptors(InterceptorRegistry registry) {
super.addInterceptors(registry);
registry.addInterceptor(new ApiInterceptor());
}
}
不过 Spring Boot 官方推荐 Spring Boot 项目用 SpringBootConfiguration 来代替 Configuration。
@Bean
这个注解是方法级别上的注解,主要添加在 @Configuration
或 @SpringBootConfiguration
注解的类,有时也可以添加在 @Component
注解的类。它的作用是定义一个Bean。
请看下面代码:
@Bean
public ApiInterceptor interceptor(){
return new ApiInterceptor();
}
那么,我们可以在 ApiInterceptor 里面注入其他 Bean,也可以在其他 Bean 注入这个类。
@Value
通常情况下,我们需要定义一些全局变量,都会想到的方法是定义一个 public static 变量,在需要时调用,是否有其他更好的方案呢?答案是肯定的。下面请看代码:
@Value("${server.port}")
String port;
@RequestMapping("/hello")
public String home(String name) {
return "hi "+name+",i am from port:" +port;
}
其中,server.port 就是我们在 application.yml 里面定义的属性,我们可以自定义任意属性名,通过 @Value
注解就可以将其取出来。
它的好处不言而喻:
- 定义在配置文件里,变量发生变化,无需修改代码。
- 变量交给Spring来管理,性能更好。
注: 本课程默认针对于对 SpringMVC 有所了解的读者,Spring Boot 本身基于 Spring 开发的,因此,本文不再讲解其他 Spring 的注解。
注入任何类
本节通过一个实际的例子来讲解如何注入一个普通类,并且说明这样做的好处。
假设一个需求是这样的:项目要求使用阿里云的 OSS 进行文件上传。
我们知道,一个项目一般会分为开发环境、测试环境和生产环境。OSS 文件上传一般有如下几个参数:appKey、appSecret、bucket、endpoint 等。不同环境的参数都可能不一样,这样便于区分。按照传统的做法,我们在代码里设置这些参数,这样做的话,每次发布不同的环境包都需要手动修改代码。
这个时候,我们就可以考虑将这些参数定义到配置文件里面,通过前面提到的 @Value
注解取出来,再通过 @Bean
将其定义为一个 Bean,这时我们只需要在需要使用的地方注入该 Bean 即可。
首先在 application.yml 加入如下内容:
appKey: 1
appSecret: 1
bucket: lynn
endPoint: https://www.aliyun.com
其次创建一个普通类:
public class Aliyun {
private String appKey;
private String appSecret;
private String bucket;
private String endPoint;
public static class Builder{
private String appKey;
private String appSecret;
private String bucket;
private String endPoint;
public Builder setAppKey(String appKey){
this.appKey = appKey;
return this;
}
public Builder setAppSecret(String appSecret){
this.appSecret = appSecret;
return this;
}
public Builder setBucket(String bucket){
this.bucket = bucket;
return this;
}
public Builder setEndPoint(String endPoint){
this.endPoint = endPoint;
return this;
}
public Aliyun build(){
return new Aliyun(this);
}
}
public static Builder options(){
return new Aliyun.Builder();
}
private Aliyun(Builder builder){
this.appKey = builder.appKey;
this.appSecret = builder.appSecret;
this.bucket = builder.bucket;
this.endPoint = builder.endPoint;
}
public String getAppKey() {
return appKey;
}
public String getAppSecret() {
return appSecret;
}
public String getBucket() {
return bucket;
}
public String getEndPoint() {
return endPoint;
}
}
然后在 @SpringBootConfiguration
注解的类添加如下代码:
@Value("${appKey}")
private String appKey;
@Value("${appSecret}")
private String appSecret;
@Value("${bucket}")
private String bucket;
@Value("${endPoint}")
private String endPoint;
@Bean
public Aliyun aliyun(){
return Aliyun.options()
.setAppKey(appKey)
.setAppSecret(appSecret)
.setBucket(bucket)
.setEndPoint(endPoint)
.build();
}
最后在需要的地方注入这个 Bean 即可:
@Autowired
private Aliyun aliyun;
拦截器
我们在提供 API 的时候,经常需要对 API 进行统一的拦截,比如进行接口的安全性校验。
本节,我会讲解 Spring Boot 是如何进行拦截器设置的,请看接下来的代码。
创建一个拦截器类:ApiInterceptor,并实现 HandlerInterceptor 接口:
public class ApiInterceptor implements HandlerInterceptor {
//请求之前
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
System.out.println("进入拦截器");
return true;
}
//请求时
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
//请求完成
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
@SpringBootConfiguration
注解的类继承 WebMvcConfigurationSupport 类,并重写 addInterceptors 方法,将 ApiInterceptor 拦截器类添加进去,代码如下:
@SpringBootConfiguration
public class WebConfig extends WebMvcConfigurationSupport{
@Override
protected void addInterceptors(InterceptorRegistry registry) {
super.addInterceptors(registry);
registry.addInterceptor(new ApiInterceptor());
}
}
异常处理
我们在 Controller 里提供接口,通常需要捕捉异常,并进行友好提示,否则一旦出错,界面上就会显示报错信息,给用户一种不好的体验。最简单的做法就是每个方法都使用 try catch 进行捕捉,报错后,则在 catch 里面设置友好的报错提示。如果方法很多,每个都需要 try catch,代码会显得臃肿,写起来也比较麻烦。
我们可不可以提供一个公共的入口进行统一的异常处理呢?当然可以。方法很多,这里我们通过 Spring 的 AOP 特性就可以很方便的实现异常的统一处理。
@Aspect
@Component
public class WebExceptionAspect {
private static final Logger logger = LoggerFactory.getLogger(WebExceptionAspect.class);
//凡是注解了RequestMapping的方法都被拦截 @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
private void webPointcut() {
}
/**
* 拦截web层异常,记录异常日志,并返回友好信息到前端 目前只拦截Exception,是否要拦截Error需再做考虑
*
* @param e
* 异常对象
*/
@AfterThrowing(pointcut = "webPointcut()", throwing = "e")
public void handleThrowing(Exception e) {
e.printStackTrace();
logger.error("发现异常!" + e.getMessage());
logger.error(JSON.toJSONString(e.getStackTrace()));
//这里输入友好性信息
writeContent("出现异常");
}
/**
* 将内容输出到浏览器
*
* @param content
* 输出内容
*/
private void writeContent(String content) {
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getResponse();
response.reset();
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Type", "text/plain;charset=UTF-8");
response.setHeader("icop-content-type", "exception");
PrintWriter writer = null;
try {
writer = response.getWriter();
} catch (IOException e) {
e.printStackTrace();
}
writer.print(content);
writer.flush();
writer.close();
}
}
这样,我们无需每个方法都添加 try catch,一旦报错,则会执行 handleThrowing 方法。
优雅的输入合法性校验
为了接口的健壮性,我们通常除了客户端进行输入合法性校验外,在 Controller 的方法里,我们也需要对参数进行合法性校验,传统的做法是每个方法的参数都做一遍判断,这种方式和上一节讲的异常处理一个道理,不太优雅,也不易维护。
其实,SpringMVC 提供了验证接口,下面请看代码:
@GetMapping("authorize")
public void authorize(@Valid AuthorizeIn authorize, BindingResult ret){
if(result.hasFieldErrors()){
List<FieldError> errorList = result.getFieldErrors();
//通过断言抛出参数不合法的异常
errorList.stream().forEach(item -> Assert.isTrue(false,item.getDefaultMessage()));
}
}
public class AuthorizeIn extends BaseModel{
@NotBlank(message = "缺少response_type参数")
private String responseType;
@NotBlank(message = "缺少client_id参数")
private String ClientId;
private String state;
@NotBlank(message = "缺少redirect_uri参数")
private String redirectUri;
public String getResponseType() {
return responseType;
}
public void setResponseType(String responseType) {
this.responseType = responseType;
}
public String getClientId() {
return ClientId;
}
public void setClientId(String clientId) {
ClientId = clientId;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getRedirectUri() {
return redirectUri;
}
public void setRedirectUri(String redirectUri) {
this.redirectUri = redirectUri;
}
}
在 controller 的方法需要校验的参数后面必须跟 BindingResult,否则无法进行校验。但是这样会抛出异常,对用户而言不太友好!
那怎么办呢?
很简单,我们可以利用上一节讲的异常处理,对报错进行拦截:
@Component
@Aspect
public class WebExceptionAspect implements ThrowsAdvice{
public static final Logger logger = LoggerFactory.getLogger(WebExceptionAspect.class);
//拦截被GetMapping注解的方法 @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
private void webPointcut() {
}
@AfterThrowing(pointcut = "webPointcut()",throwing = "e")
public void afterThrowing(Exception e) throws Throwable {
logger.debug("exception 来了!");
if(StringUtils.isNotBlank(e.getMessage())){
writeContent(e.getMessage());
}else{
writeContent("参数错误!");
}
}
/**
* 将内容输出到浏览器
*
* @param content
* 输出内容
*/
private void writeContent(String content) {
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getResponse();
response.reset();
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Type", "text/plain;charset=UTF-8");
response.setHeader("icop-content-type", "exception");
PrintWriter writer = null;
try {
writer = response.getWriter();
writer.print((content == null) ? "" : content);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
这样当我们传入不合法的参数时就会进入 WebExceptionAspect 类,从而输出友好参数。
我们再把验证的代码单独封装成方法:
protected void validate(BindingResult result){
if(result.hasFieldErrors()){
List<FieldError> errorList = result.getFieldErrors();
errorList.stream().forEach(item -> Assert.isTrue(false,item.getDefaultMessage()));
}
}
这样每次参数校验只需要调用 validate 方法就行了,我们可以看到代码的可读性也大大的提高了。
接口版本控制
一个系统上线后会不断迭代更新,需求也会不断变化,有可能接口的参数也会发生变化,如果在原有的参数上直接修改,可能会影响线上系统的正常运行,这时我们就需要设置不同的版本,这样即使参数发生变化,由于老版本没有变化,因此不会影响上线系统的运行。
一般我们可以在地址上带上版本号,也可以在参数上带上版本号,还可以再 header 里带上版本号,这里我们在地址上带上版本号,大致的地址如:http://api.example.com/v1/test,其中,v1 即代表的是版本号。具体做法请看代码:
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
/**
* 标识版本号
* @return
*/
int value();
}
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
// 路径中版本的前缀, 这里用 /v[1-9]/的形式
private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+)/");
private int apiVersion;
public ApiVersionCondition(int apiVersion){
this.apiVersion = apiVersion;
}
@Override
public ApiVersionCondition combine(ApiVersionCondition other) {
// 采用最后定义优先原则,则方法上的定义覆盖类上面的定义
return new ApiVersionCondition(other.getApiVersion());
}
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI());
if(m.find()){
Integer version = Integer.valueOf(m.group(1));
if(version >= this.apiVersion)
{
return this;
}
}
return null;
}
@Override
public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
// 优先匹配最新的版本号
return other.getApiVersion() - this.apiVersion;
}
public int getApiVersion() {
return apiVersion;
}
}
public class CustomRequestMappingHandlerMapping extends
RequestMappingHandlerMapping {
@Override
protected RequestCondition<ApiVersionCondition> getCustomTypeCondition(Class<?> handlerType) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
return createCondition(apiVersion);
}
@Override
protected RequestCondition<ApiVersionCondition> getCustomMethodCondition(Method method) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
return createCondition(apiVersion);
}
private RequestCondition<ApiVersionCondition> createCondition(ApiVersion apiVersion) {
return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value());
}
}
@SpringBootConfiguration
public class WebConfig extends WebMvcConfigurationSupport {
@Bean
public AuthInterceptor interceptor(){
return new AuthInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor());
}
@Override
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping();
handlerMapping.setOrder(0);
handlerMapping.setInterceptors(getInterceptors());
return handlerMapping;
}
}
Controller 类的接口定义如下:
@ApiVersion(1)
@RequestMapping("{version}/dd")
public class HelloController{}
这样我们就实现了版本控制,如果增加了一个版本,则创建一个新的 Controller,方法名一致,ApiVersion 设置为2,则地址中 v1 会找到 ApiVersion 为1的方法,v2 会找到 ApiVersion 为2的方法。
自定义 JSON 解析
Spring Boot 中 RestController 返回的字符串默认使用 Jackson 引擎,它也提供了工厂类,我们可以自定义 JSON 引擎,本节实例我们将 JSON 引擎替换为 fastJSON,首先需要引入 fastJSON:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
其次,在 WebConfig 类重写 configureMessageConverters 方法:
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
super.configureMessageConverters(converters);
/*
1.需要先定义一个convert转换消息的对象;
2.添加fastjson的配置信息,比如是否要格式化返回的json数据
3.在convert中添加配置信息
4.将convert添加到converters中
*/
//1.定义一个convert转换消息对象
FastJsonHttpMessageConverter fastConverter=new FastJsonHttpMessageConverter();
//2.添加fastjson的配置信息,比如:是否要格式化返回json数据
FastJsonConfig fastJsonConfig=new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(
SerializerFeature.PrettyFormat
);
fastConverter.setFastJsonConfig(fastJsonConfig);
converters.add(fastConverter);
}
单元测试
Spring Boot 的单元测试很简单,直接看代码:
@SpringBootTest(classes = Application.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class TestDB {
@Test
public void test(){
}
}
模板引擎
在传统的 SpringMVC 架构中,我们一般将 JSP、HTML 页面放到 webapps 目录下面,但是 Spring Boot 没有 webapps,更没有 web.xml,如果我们要写界面的话,该如何做呢?
Spring Boot 官方提供了几种模板引擎:FreeMarker、Velocity、Thymeleaf、Groovy、mustache、JSP。
这里以 FreeMarker 为例讲解 Spring Boot 的使用。
首先引入 FreeMarker 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
在 resources 下面建立两个目录:static 和 templates,如图所示:
其中 static 目录用于存放静态资源,譬如:CSS、JS、HTML 等,templates 目录存放模板引擎文件,我们可以在 templates 下面创建一个文件:index.ftl(freemarker 默认后缀为 .ftl
),并添加内容:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
然后创建 PageController 并添加内容:
@Controller
public class PageController {
@RequestMapping("index.html")
public String index(){
return "index";
}
}
启动 Application.java,访问:http://localhost:8080/index.html,就可以看到如图所示:
开发任何基于 Spring Boot 的项目,我们都会使用以下的启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
可以看到,Application 类中定义了注解 @SpringBootApplication
,main 方法里通过 SpringApplication.run 来启动整个应用程序。因此要研究 Spring Boot 的启动原理,我们就需要从这两个地方入手。
强大的 SpringBootApplication
首先,我们先来看看 SpringBootApplication 源码是怎么定义这个注解的:
/**
* Indicates a {@link Configuration configuration} class that declares one or more
* {@link Bean @Bean} methods and also triggers {@link EnableAutoConfiguration
* auto-configuration} and {@link ComponentScan component scanning}. This is a convenience
* annotation that is equivalent to declaring {@code @Configuration},
* {@code @EnableAutoConfiguration} and {@code @ComponentScan}.
*
* @author Phillip Webb
* @author Stephane Nicoll
* @since 1.2.0
*/
@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 {
/**
* Exclude specific auto-configuration classes such that they will never be applied.
* @return the classes to exclude
*/
@AliasFor(annotation = EnableAutoConfiguration.class, attribute = "exclude")
Class<?>[] exclude() default {};
/**
* Exclude specific auto-configuration class names such that they will never be
* applied.
* @return the class names to exclude
* @since 1.3.0
*/
@AliasFor(annotation = EnableAutoConfiguration.class, attribute = "excludeName")
String[] excludeName() default {};
/**
* Base packages to scan for annotated components. Use {@link #scanBasePackageClasses}
* for a type-safe alternative to String-based package names.
* @return base packages to scan
* @since 1.3.0
*/
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
/**
* Type-safe alternative to {@link #scanBasePackages} for specifying the packages to
* scan for annotated components. The package of each class specified will be scanned.
* <p>
* Consider creating a special no-op marker class or interface in each package that
* serves no purpose other than being referenced by this attribute.
* @return base packages to scan
* @since 1.3.0
*/
@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};
}
可以看到,除了最基础的注解外,还增加了三个 @SpringBootConfiguration
、@EnableAutoConfiguration
、@ComponentScan
。因此,正如上一篇所讲的一样,我们将 SpringBootApplication 替换成这三个注解也是相同的效果:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
每次我们都写这三个注解比较麻烦,因此我们只写 @SpringBootApplication
就行了。
下面,我们分别来介绍这三个注解。
SpringBootConfiguration
我们先来看看它的源码:
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Configuration;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
}
它其实就是一个 Configuration,但是 Spring Boot 推荐用 SpringBootConfiguration 来代替 Configuration。
Spring Boot 社区推荐使用 JavaConfig 配置,所以要用到 @Configuration
。
我们先来看看 SpringMVC 基于 XML 是如何配置的:
<?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-3.0.xsd"
default-lazy-init="true">
<!--bean定义-->
</beans>
而 JavaConfig 的配置是这样的:
import org.springframework.boot.SpringBootConfiguration;
@SpringBootConfiguration
public class WebConfig {
//bean定义
}
任何标注了 SpringBootConfiguration 或 Configuration 的类都是一个 JavaConfig。
我们再来看看基于 XML 的 Bean 是如何定义的:
<bean id="service" class="ServiceImpl">
</bean>
而 JavaConfig 的配置是这样的:
import org.springframework.boot.SpringBootConfiguration;
@SpringBootConfiguration
public class WebConfig {
//bean定义
@Bean
public Service service(){
return new ServiceImpl();
}
}
任何标注了 Bean 的方法都被定义为一个 Bean,我们可以在任何 Spring 的 IoC 容器中注入进去。
EnableAutoConfiguration
这个注解尤为重要,它的作用是自动将 JavaConfig 中的 Bean 装载到 IoC 容器中。
ComponentScan
这个注解的作用是自动扫描并加载符合条件的组件(如:Component、Bean 等),我们可以通过 basePakcages 来指定其扫描的范围,如果不指定,则默认从标注了 @ComponentScan
注解的类所在包开始扫描。如下代码:
@ComponentScan(basePackages = "com.lynn")
因此,Spring Boot 的启动类最好放在 root package 下面,因为默认不指定 basePackages,这样能保证扫描到所有包。
以上只是从表面来研究 Spring Boot 的启动原理,那么,为什么通过 SpringBootApplication 和 SpringApplication.run() 就能启动一个应用程序,它的底层到底是怎么实现的呢?别急,我们马上来一探究竟。
源码解析
我们知道,启动类先调用了 SpringApplication 的静态方法 run,跟踪进去后发现,它会先实例化 SpringApplication,然后调用 run 方法。
/**
* Static helper that can be used to run a {@link SpringApplication} from the
* specified sources using default settings and user supplied arguments.
* @param sources the sources to load
* @param args the application arguments (usually passed from a Java main method)
* @return the running {@link ApplicationContext}
*/
public static ConfigurableApplicationContext run(Object[] sources, String[] args) {
return new SpringApplication(sources).run(args);
}
所以,要分析它的启动源码,首先要分析 SpringApplicaiton 的构造过程。
SpringApplication 构造器
在 SpringApplication 构造函数内部,它会调用内部的一个定义为 private 的方法 initialize:
public SpringApplication(Object... sources) {
initialize(sources);
}
private void initialize(Object[] sources) {
if (sources != null && sources.length > 0) {
this.sources.addAll(Arrays.asList(sources));
}
this.webEnvironment = deduceWebEnvironment();
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
通过上述代码,我们分析到 SpringApplication 实例化时有以下几个步骤:
1.将所有 sources 加入到全局 sources 中,目前只有一个 Application。
2.判断是否为 Web 程序(javax.servlet.Servlet、org.springframework.web.context.ConfigurableWebApplicationContext 这两个类必须存在于类加载器中)。
判断过程可以参看以下源码:
private static final String[] WEB_ENVIRONMENT_CLASSES = new String[]{"javax.servlet.Servlet", "org.springframework.web.context.ConfigurableWebApplicationContext"};
private boolean deduceWebEnvironment() {
for (String className : WEB_ENVIRONMENT_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return false;
}
}
return true;
}
3.设置应用程序初始化器 ApplicationContextInitializer,做一些初始化的工作。
4.设置应用程序事件监听器 ApplicationListener。
5.找出启动类,设置到 mainApplicationClass 中。
SpringApplication 的执行流程
SpringApplication 构造完成后,就会调用 run 方法,这时才真正的开始应用程序的执行。
先来看看源码:
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
FailureAnalyzers analyzers = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
analyzers = new FailureAnalyzers(context);
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
listeners.finished(context, null);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
return context;
}
catch (Throwable ex) {
handleRunFailure(context, listeners, analyzers, ex);
throw new IllegalStateException(ex);
}
}
通过上述源码,将执行流程分解如下:
- 初始化 StopWatch,调用其 start 方法开始计时。
- 调用 configureHeadlessProperty 设置系统属性 java.awt.headless,这里设置为 true,表示运行在服务器端,在没有显示器和鼠标键盘的模式下工作,模拟输入输出设备功能。
- 遍历 SpringApplicationRunListeners 并调用 starting 方法。
- 创建一个 DefaultApplicationArguments 对象,它持有 args 参数,就是 main 函数传进来的参数调用 prepareEnvironment 方法。
- 打印 banner。
- 创建 Spring Boot 上下文。
- 初始化 FailureAnalyzers。
- 调用 prepareContext。
- 调用 AbstractApplicationContext 的 refresh 方法,并注册钩子。
- 在容器完成刷新后,依次调用注册的 Runners。
- 调用 SpringApplicationRunListeners 的 finished 方法。
- 启动完成并停止计时。
- 初始化过程中出现异常时调用 handleRunFailure 进行处理,然后抛出 IllegalStateException 异常。