上一篇带领你们初步了解了如何使用 Spring Boot 搭建框架,经过 Spring Boot 和传统的 SpringMVC 架构的对比,咱们清晰地发现 Spring Boot 的好处,它使咱们的代码更加简单,结构更加清晰。html
从这一篇开始,我将带领你们更加深刻的认识 Spring Boot,将 Spring Boot 涉及到东西进行拆解,从而了解 Spring Boot 的方方面面。学完本文后,读者能够基于 Spring Boot 搭建更加复杂的系统框架。前端
咱们知道,Spring Boot 是一个大容器,它将不少第三方框架都进行了集成,咱们在实际项目中用到哪一个模块,再引入哪一个模块。好比咱们项目中的持久化框架用 MyBatis,则在 pom.xml 添加以下依赖:java
<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 文件mysql
咱们知道整个 Spring Boot 项目只有一个配置文件,那就是 application.yml,Spring Boot 在启动时,就会从 application.yml 中读取配置信息,并加载到内存中。上一篇咱们只是粗略的列举了几个配置项,其实 Spring Boot 的配置项是不少的,本文咱们将学习在实际项目中经常使用的配置项(注:为了方便说明,配置项均以 properties 文件的格式写出,后续的实际配置都会写成 yaml 格式)。web
下面是我参与的某个项目的 application.yml 配置文件内容:spring
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
以上列举了经常使用的配置项,全部配置项信息均可以在官网中找到,本课程就不一一列举了。sql
多环境配置数据库
在一个企业级系统中,咱们可能会遇到这样一个问题:开发时使用开发环境,测试时使用测试环境,上线时使用生产环境。每一个环境的配置均可能不同,好比开发环境的数据库是本地地址,而测试环境的数据库是测试地址。那咱们在打包的时候如何生成不一样环境的包呢?json
这里的解决方案有不少:api
1.每次编译以前手动把全部配置信息修改为当前运行的环境信息。这种方式致使每次都须要修改,至关麻烦,也容易出错。
2.利用 Maven,在 pom.xml 里配置多个环境,每次编译以前将 settings.xml 里面修改为当前要编译的环境 ID。这种方式会事先设置好全部环境,缺点就是每次也须要手动指定环境,若是环境指定错误,发布时是不知道的。
3.第三种方案就是本文重点介绍的,也是做者强烈推荐的方式。
首先,建立 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 注解就能够将其取出来。
它的好处不言而喻:
1.定义在配置文件里,变量发生变化,无需修改代码。
2.变量交给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")//GetMapping是RequestMapping(method=method.GET)的组合 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,就能够看到如图所示: