CommandLineRunner
我们可以通过以下方法运行自己的初始代码:
- @PostConstruct注解
- ApplicationReadyEvent事件
- CommandLineRunner/ApplicationRunner接口
Spring Boot提供了两个接口:CommandLineRunner、ApplicationRunner,用于启动应用时做特殊处理,这些代码会在SpringApplication的run()方法运行完成之前被执行。通常用于应用启动前的特殊代码执行、特殊数据加载、垃圾数据清理、微服务的服务发现注册、系统启动成功后的通知等。相当于Spring的ApplicationListener、Servlet的ServletContextListener。CommandLineRunner 和 ApplicationRunner 的区别是run()方法的参数不同,也可以两个接口同时实现,但是没有必要。
ApplicationStartup.java
@Component
public class ApplicationStartup implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(ApplicationStartup.class);
@Override
public void run(String... args) throws Exception {
logger.info("Application started with command-line arguments: {} . \n To kill this application, press Ctrl + C.", Arrays.toString(args));
}
}
多环境配置
1 可选方案
在实际开发过程中,一般需要dev、prod、test三个环境,解决方案主要有:
- 使用 maven profile,在 pom.xml 里配置多个环境。这种方式会事先设置好所有环境,缺点就是每次也需要手动指定环境。打包时需要提前用 -P指定要打的环境。
- 使用 SpringBoot的 spring.profiles.active,这种方式,打包时不需要指定环境,运行时可以通过修改 spring.profiles.active值来切换环境,如:
java -jar test.jar --spring.profiles.active=dev
这里我推荐并讲解第二种,因为这种方式打包时不需要事先指定环境,使用时指定就好了,比较方便灵活。尤其是当我们的应用部署在 docker 中时,比如我们要将默认安装后的host值从 127.0.0.1改为具体的IP,以便外网可以访问。此时就不能用maven profile方式了。
spring boot允许你通过命名约定按照一定的格式application-{profile}.properties来定义多个配置文件,然后在 application.properties 文件中通过spring.profiles.active来激活一个或多个配置文件,如果没有指定任何profile的配置文件的话,spring boot默认会启动application-default.properties。spring boot 2.x中,profile的配置文件可以按照 application.properyies的放置位置一样,放于以下四个位置,不需要设置spring.config.location
- 当前目录的 “/config”的子目录下
- 当前目录下
- classpath根目录的“/config”包下
- classpath的根目录下
具体实现代码在 ConfigFileApplicationListener 类中。
SpringBoot方式启动时,如果需要从其他地方读取配置文件,而不是默认上面四个地方,可以在IDEA 的 VM options 中添加:
-Dspring.config.additional-location=file:\falhome\falpolicy\config\,如图:




2 通过在配置文件或java option中设置 spring.profiles.active 来指定环境
application.properties
# Spring Boot Default Config # 我们可以将不同环境都共同的配置信息写到这个文件中。 # 当前项目默认环境为 dev,即项目启动时如果不指定任何环境,Spring Boot 会自动从 dev 环境文件中读取配置信息。 # 项目启动时,改用其他环境,如:java -jar test.jar --spring.profiles.active=prod。 # 如果 dev配置中含有"wanghua.appName",那么此默认配置上的"wanghua.appName"不会起作用。 spring.profiles.active=dev wanghua.appName="DefaultApp"
application-dev.properties
# Spring Boot Dev Config wanghua.appName="DevApp"
application-prod.properties
# Spring Boot Prod Config wanghua.appName="ProdApp"
ProfileServiceImpl.class
@Service
public class ProfileServiceImpl implements ProfileService {
@Value("${wanghua.appName}")
private String appName;
@PostConstruct
private void initInstance() {
showAppName();
}
@Override
public void showAppName() {
System.out.println(">>> " + appName);
}
}
3 使用@Profile 控制Bean实例是否创建
某些情况下,应用的某些业务逻辑可能需要有不同的实现。例如邮件服务,假设EmailService中包含的send(String email)方法向指定地址发送电子邮件,但是我们仅仅希望在生产环境中才执行真正发送邮件的代码,而开发环境里则不发送以免向用户发送无意义的垃圾邮件。我们可以借助@Profile实现这样的功能,这需要定义两个实现EmailService接口的类,且Controller层使用接口来调用send方法,而不是实现类。
DevEmailServiceImpl
@Service
@Profile("dev")
public class DevEmailServiceImpl implements EmailService {
@PostConstruct
private void initInstance() {
send();
}
@Override
public void send() {
System.out.println(">> Send email for dev.");
}
}
ProdEmailServiceImpl
@Service
@Profile("prod")
public class ProdEmailServiceImpl implements EmailService {
@PostConstruct
private void initInstance() {
send();
}
@Override
public void send() {
System.out.println(">> Send email for prod.");
}
}
执行命令查看效果:java -jar test.jar --spring.profiles.active=prod
常用注解
1 @SpringBootApplication
Spring Boot 支持 main 方法启动,在我们需要启动的主类中加入此注解,告诉 Spring Boot,这个类是程序的入口。查看源码,可以看到这个注解包含了下面三个注解:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
SpringBootConfiguration 表示 Spring Boot 的配置注解;
EnableAutoConfiguration 表示自动配置;
ComponentScan 表示 Spring Boot 扫描 Bean 的规则,比如扫描哪些包。可以通过 basePakcages 来指定其扫描的范围,如果不指定,则默认从标注了 @ComponentScan 注解的类所在包开始扫描。因此,Spring Boot 的启动类最好放在 root package 下面,因为默认不指定 basePackages,这样能保证扫描到所有包。
2 @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。
3 @Bean
这个注解是方法级别上的注解,主要添加在 @Configuration 或 @SpringBootConfiguration 注解的类,有时也可以添加在 @Component 注解的类。它的作用是定义一个Bean。
@Bean
public ApiInterceptor interceptor(){
return new ApiInterceptor();
}
使用@bean创建的bean只会出现一个,即使在多个Configuration中定义了 。
SpringBoot 内置的 Configuration,有的通过@ConditionalOnProperty来控制Configuration是否生效,比如: MailSenderPropertiesConfiguration 类上就有此注解。
@ConditionalOnMissingBean 仅当 BeanFactory 中不包含指定的 bean class 和/或 name 时条件匹配,该条件只能匹配到目前为止 application context 已经处理的 bean 定义,
因此强烈建议仅在自动配置类上使用此条件。
4 @Value
通常情况下,我们需要定义一些全局变量,都会想到的方法是定义一个 public static 变量,在需要时调用,是否有其他更好的方案呢?答案是肯定的,就是将这些变量写在配置里,而后使用 @Value 取出来,如:(“${server.port}”)。
它的好处不言而喻:
- 定义在配置文件里,变量发生变化,无需修改代码。
- 变量交给Spring来管理,性能更好。
5 @DependsOn
SpringBoot 中可以使用@DependsOn来控制 @Configuration注解的类中的 @Bean的顺序。
这个 @DependsOn可以设置在@Configuration类上,也可以设置在 @Bean 方法上。
注意:经测试,@Order 在 SpringBoot中不起作用的!
6 @ConfigurationProperties 与 @PropertySource
@Data
@Validated
@Configuration
@ConfigurationProperties(prefix = "wsl.swagger2")
//@PropertySource("classpath:application.properties")
public class Swagger2Properties {
private String basePackage;
private boolean enable;
private String title;
private String version;
}
这里只是需要注意一点,如果将配置写在 application.properties 中,那么这个类根据不需要设置 @PropertySource,如果设置,
那么必须加上“classpath”,否则SPringBoot打包war部署到tomcat出现错误:Could not open ServletContext resource [/application.properties],但是在开发测试时不会报错的,因为部署到Tomcat后默认的访问路劲是classpath。
拦截器 Interceptor
我们经常需要对 API 进行统一的拦截,比如进行接口的安全性校验,此时可以在SpringBoot中使用Servlet的拦截器。
TestFilter.class
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class TestFilter extends HandlerInterceptorAdapter {
private final Logger logger = LoggerFactory.getLogger(TestFilter.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
logger.info("request 请求地址 path[{}] uri[{}]", request.getServletPath(), request.getRequestURI());
//request.getHeader(String) 从请求头中获取数据
String userId = request.getHeader("H-User-Token");
if (checkAuth(userId, request.getRequestURI())) {
return true;
}
//实际项目中可能会抛出 HttpStatus 401
return false;
}
//校验用户访问权限
private boolean checkAuth(String userId, String requestURI) {
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
注意:HandlerInterceptorAdapter继承自HandlerInterceptor接口。其实这里并不需要适配器版本,直接用HandlerInterceptor接口就好了,只是想让大家了解下,还有这么一个适配器拦截器呢。
TestConfiguration.class
package com.wanghua.study.configuration;
import com.wanghua.study.Interceptor.ApiInterceptor;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
@SpringBootConfiguration
public class TestConfiguration extends WebMvcConfigurationSupport {
@Override
protected void addInterceptors(InterceptorRegistry registry) {
super.addInterceptors(registry);
// 添加自定义拦截器
registry.addInterceptor(new ApiInterceptor());
}
}
异常处理
1 使用 @ControllerAdvice + @ExceptionHandler + @ResponseStatus(推荐)
- @ControllerAdvice是一个@Component,用于定义@ExceptionHandler,@InitBinder和@ModelAttribute方法,适用于所有使用@RequestMapping方法。
- Spring4之前,@ControllerAdvice在同一调度的Servlet中协助所有控制器。Spring4已经改变:@ControllerAdvice支持配置控制器的子集,而默认的行为仍然可以利用。
- 在Spring4中,@ControllerAdvice通过annotations(), basePackageClasses(), basePackages()方法定制用于选择控制器子集。
不过据经验之谈,只有配合@ExceptionHandler最有用,其它两个不常用。
如果单使用@ExceptionHandler和@ResponseStatus,只能在当前Controller中处理异常。但当配合@ControllerAdvice一起使用的时候,就可以摆脱那个限制了。
@ControllerAdvice
public class FalExceptionControllerAdvice {
@ResponseBody
@ExceptionHandler(value = FalRuntimeException.class)
public JsonWrapper FalRuntimeExceptionHandler(Exception ex) {
FalRuntimeException falEx = (FalRuntimeException) ex;
FalError FalError = falEx.getFalError();
String userMessage = null;
if (StringUtils.isBlank(userMessage)) {
userMessage = falEx.getMessage();
}
JsonWrapper jsonWrapper = JsonWrapper.createErrorResponse(null, HttpStatus.OK.toString(), userMessage, ex);
ex.printStackTrace();
return jsonWrapper;
}
@ResponseBody
@ExceptionHandler(value = UnauthorizedException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public JsonWrapper unauthorizedExceptionHandler(Exception ex) {
String message = "You does not have permission.";
// String message = ex.getMessage();
JsonWrapper jsonWrapper = JsonWrapper.createErrorResponse(null, HttpStatus.UNAUTHORIZED.toString(), message, ex);
ex.printStackTrace();
return jsonWrapper;
}
// 如果是其他异常,则在这里捕捉处理
@ResponseBody
@ExceptionHandler(value = Throwable.class)
public JsonWrapper exceptionHandler(Exception ex) {
String message = ex.getMessage();
JsonWrapper jsonWrapper = JsonWrapper.createErrorResponse(null, HttpStatus.OK.toString(), message, ex);
ex.printStackTrace();
return jsonWrapper;
}
}
2 使用 Spring AOP
首先需要添加下面依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
WebExceptionAspect.class
package com.wanghua.study.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@Aspect
@Component
public class WebExceptionAspect {
private static final Logger logger = LoggerFactory.getLogger(WebExceptionAspect.class);
/*
若我们想统计请求的处理时间,就需要在doBefore处记录时间,并在doAfterReturning处通过当前时间与开始处记录的时间计算得到请求处理的消耗时间。
我们可以在WebExceptionAspect切面中定义一个成员变量来给doBefore和doAfterReturning一起访问,但得考虑同步问题,所以我们可以引入ThreadLocal对象。
*/
private ThreadLocal<Long> startTime = new ThreadLocal<>();
// 凡是注解了 RequestMapping的方法都被拦截
@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
private void webPointcut() {
}
// 拦截web层异常,并返回友好信息到前端 目前只拦截Exception
@AfterThrowing(pointcut = "webPointcut()", throwing = "e")
public void handleThrowing(Exception e) {
e.printStackTrace();
//logger.error(JSON.toJSONString(e.getStackTrace()));
//这里输入友好性信息
writeContent("服务器出现异常");
}
// 将内容输出到浏览器
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();
}
@Before("webPointcut()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
startTime.set(System.currentTimeMillis());
}
@AfterReturning(returning = "ret", pointcut = "webPointcut()")
public void doAfterReturning(Object ret) throws Throwable {
logger.info(">>> spend time: " + (System.currentTimeMillis() - startTime.get()));
}
}
使用 BindingResult 合法性校验
在 controller 的方法需要校验的参数后面跟 BindingResult 就可以进行校验。
@RequestMapping(method = {RequestMethod.POST})
public JsonWrapper insert(@RequestBody @Valid ScheduleObjectDto scheduleObjectDto, BindingResult result)
throws Exception {
ValidationUtil.validateFieldErrors(result);
ScheduleObject scheduleObject = posScheduleObjectService.saveScheduleObject(scheduleObjectDto);
return new JsonWrapper(scheduleObject);
}
public class ValidationUtil {
public static void validateFieldErrors(BindingResult bindingResult) throws FalUserParameterException {
StringBuilder sb = new StringBuilder();
if (bindingResult.hasErrors()) {
for (ObjectError objectError : bindingResult.getAllErrors()) {
FieldError fieldError = ((FieldError) objectError);
sb.append(MessageFormat.format("{0} {1}!", fieldError.getField(), fieldError.getDefaultMessage()));
break; //仅返回一个
}
throw new FalUserParameterException(sb.toString()); //由@ControllerAdvice所在类统一捕捉
}
}
}
推荐使用 捕捉MethodArgumentNotValidException 方式。
接口版本控制
一个系统上线后会不断迭代更新,需求也会不断变化,有可能接口的参数也会发生变化,如果在原有的参数上直接修改,可能会影响线上系统的正常运行,这时我们就需要设置不同的版本,在新的版本上修改,由于老版本没有变化,因此不会影响上线系统的运行。
package com.wanghua.study.controller;
import com.alibaba.fastjson.JSONObject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@RequestMapping({"/{version:v0}/helloworld"})
@Controller
public class HelloWorldController {
@RequestMapping(method = {RequestMethod.GET}, produces = {"application/json"})
@ResponseBody
public Object helloWorld() {
JSONObject jsonObject = new JSONObject();
jsonObject.put("message", "helloworld");
return jsonObject;
}
}
请求格式如:http://localhost:8080/v0/helloworld,这里的v0就是我们指定的版本。
如果增加了一个版本,则创建一个新的 Controller,方法名一致,version 设置为v1即可。
自定义 JSON 解析
SpringMvc中对象的序列化和反序列化,默认都使用Jackson,本节我们将 JSON 引擎替换为 fastJSON。首先得加入依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.54</version>
</dependency>
然后,在 WebConfig 类重写 configureMessageConverters 方法:
@SpringBootConfiguration
public class WebConfiguration extends WebMvcConfigurationSupport {
@Override
protected void addInterceptors(InterceptorRegistry registry) {
super.addInterceptors(registry);
registry.addInterceptor(new ApiInterceptor()); // 添加自定义拦截器
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
super.configureMessageConverters(converters);
//1 添加配置
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
//2 定义消息转换器
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
fastConverter.setFastJsonConfig(fastJsonConfig);
converters.add(fastConverter);
}
}
模板引擎
Spring Boot 没有 webapps,更没有 web.xml,但官方集成了几种模板引擎:freemarker、thymeleaf、groovy。这里我们使用freemarker讲解。首先加入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
其中 static 目录用于存放静态资源,譬如:CSS、JS、HTML 等,templates 目录存放模板引擎文件,我们可以在 templates 下面创建一个文件:index.ftl(freemarker 默认后缀为 .ftl),并添加内容:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<h1>${message}</h1>
</body>
</html>
然后创建 FreeMarkerController 并添加内容:
@Controller
public class FreeMarkerController {
/*
1. 这里不是走 HTTP + JSON 模式,使用了 @Controller 而不是先前的 @RestController。
2. 方法返回值是 String 类型,和 application.properties 配置的 Freemarker 文件配置
路径下的各个 *.ftl 文件名一致。这样才会准确地把数据渲染到 ftl 文件里面进行展示。
3. 用 Model 类,向 Model 加入数据,并指定在该数据在 Freemarker 取值指定的名称。
*/
@RequestMapping("index.html")
public String index(Model model) {
model.addAttribute("message", "This is my first freemarker page.");
return "index";
}
}