为了保证服务的高可用,发现问题一定要即使,解决问题一定要迅速,所以生产环境一旦出现问题,预警系统就会通过邮件、短信甚至电话的方式实施多维轰炸模式,确保相关负责人不错过每一个可能的bug。
预警系统判断疑似bug大部分源于日志。比如某个微服务接口由于各种原因导致频繁调用出错,此时调用端会捕获这样的异常并打印ERROR级别的日志,当该错误日志达到一定次数出现的时候,就会触发报警。
try {
调用某服务
} catch(Exception e) {
LOG.error("错误信息", e);
}
所以日志至关重要,这篇就来介绍下在Spring Boot如何配置日志。
Spring Boot默认日志系统
Spring Boot默认使用LogBack日志系统,如果不需要更改为其他日志系统如Log4j2等,则无需多余的配置,LogBack默认将日志打印到控制台上。
如果要使用LogBack,原则上是需要添加dependency依赖的
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
但是因为新建的Spring Boot项目一般都会引用spring-boot-starter或者spring-boot-starter-web,而这两个起步依赖中都已经包含了对于spring-boot-starter-logging的依赖,所以,无需额外添加依赖。
如果需要做一些定制的日志配置比如将日志存储到文件等应该如何配置,下面就通过几个小问题来看看Spring Boot下是如何解决这些问题的。
如何在项目中打印日志
新建一个配置类LogConfig,注入一个Bean,并在方法中打印日志
import com.jackie.springbootdemo.model.Person;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration public class LogConfig {
private static final Logger LOG = LoggerFactory.getLogger(LogConfig.class);
@Bean
public Person logMethod() {
LOG.info("==========print log==========");
return new Person();
}
}
运行SpringbootTeach4Application,可以看到控制台的日志
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.0.RELEASE)
2020-03-09 15:46:27.643 INFO 88964 --- [ main] s.s.teach4.SpringbootTeach4Application : Starting SpringbootTeach4Application on SHDN049 with PID 88964 (E:\workspace-train\springboot-teach4\target\classes started by indexvc in E:\workspace-train\springboot-teach4)
2020-03-09 15:46:27.646 INFO 88964 --- [ main] s.s.teach4.SpringbootTeach4Application : No active profile set, falling back to default profiles: default
2020-03-09 15:46:27.779 INFO 88964 --- [ main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@3e77a1ed: startup date [Mon Mar 09 15:46:27 CST 2020]; root of context hierarchy
2020-03-09 15:46:28.190 INFO 88964 --- [ main] sample.springboot.teach4.LogConfig : ==========print log==========
2020-03-09 15:46:29.089 INFO 88964 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2020-03-09 15:46:29.099 INFO 88964 --- [ main] s.s.teach4.SpringbootTeach4Application : Started SpringbootTeach4Application in 2.325 seconds (JVM running for 3.42)
2020-03-09 15:46:29.101 INFO 88964 --- [ Thread-2] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@3e77a1ed: startup date [Mon Mar 09 15:46:27 CST 2020]; root of context hierarchy
2020-03-09 15:46:29.102 INFO 88964 --- [ Thread-2] o.s.j.e.a.AnnotationMBeanExporter : Unregistering JMX-exposed beans on shutdown
Spring Boot默认的日志级别为INFO,这里打印的是INFO级别的日志所以可以显示。
很多开发者在日常写private static final Logger LOG = LoggerFactory.getLogger(LogConfig.class);总觉得后面的LogConfig.class可有可无,因为随便写个其他类也不会报错,但是准确编写class信息能够提供快速定位日志的效率。
我们看到打印的日志内容左侧就是对应的类名称,这个是通过private static final Logger LOG = LoggerFactory.getLogger(LogConfig.class);实现的。
如果将LogConfig.class换成xxx.class,输出日志就会显示对应的xxx类名。这样声明的好处就是方便定位日志。
如何将日志信息存储到文件
在本机环境,我们习惯在控制台看日志,但是线上我们还是要通过将日志信息保存到日志文件中,查询日志文件即可。
那么应该如何配置才能将日志信息保存到文件呢?
在我们创建的springboot-teach4项目中,resources目录下有个application.properties文件(如果是application.yml文件也是同样的道理,只是采用的不同的编写风格而已)。添加如下配置
server.port=8081
#它们俩不会同时生效, 只配置其中一个就好了。
#logging.path=E://workspace-train//springboot-teach4//2
logging.file=E://workspace-train//springboot-teach4//1//springbootdemo.log
该属性用来配置日志文件的路径
logging.file
该属性用来配置日志文件名,如果该属性不配置,默认文件名为spring.log
运行SpringBootDemoApplication
可以看到在指定路径下生成了springbootdemo.log文件,该文件内容和控制台打印内容一致。
如果注释logging.file=springbootdemo.log则生成默认文件名spring.log
日志级别总共有TRACE < DEBUG < INFO < WARN < ERROR < FATAL ,且级别是逐渐提供,如果日志级别设置为INFO,则意味TRACE和DEBUG级别的日志都看不到。
上例中我们打印了一个INFO级别的日志,因为Spring Boot默认级别就是INFO,如果我们改为WARN,是否还能看到这行日志信息。
logging.level
该属性用于配置日志级别。
在applicaition.properties中添加
logging.level.root=warn
这里是用的root级别,即项目的所有日志,我们也可以使用package级别,即指定包下使用相应的日志级别,下面再看。
启动SpringBootDemoApplication
你没看错,这个项目是成功启动了,但是几乎没有内容,这是因为之前打印的日志级别都是INFO,这里设置为WARN,所以INFO级别的日志都不显示。
这里我们可以改动root还是INFO级别,将指定包下的日志级别设置为WARN
logging.level.root=INFO
logging.level.com.jackie.springbootdemo.config=WARN
启动SpringBootDemoApplication
可以看到除了LogConfig类中的INFO级别的日志没有打印出来,其他的INFO级别的日志都正常输出了。
如何定制自己的日志格式
在application.properties中添加
logging.pattern.console=%d{yyyy/MM/dd-HH:mm:ss} [%thread] %-5level %logger- %msg%n
logging.pattern.file=%d{yyyy/MM/dd-HH:mm} [%thread] %-5level %logger- %msg%n
该属性用于定制日志输出格式。
上述配置的编码中,对应符号的含义如下
%d{HH:mm:ss.SSS}——日志输出时间
%thread——输出日志的进程名字,这在Web应用以及异步任务处理中很有用
%-5level——日志级别,并且使用5个字符靠左对齐
%logger- ——日志输出者的名字
%msg——日志消息
%n——平台的换行符
启动SpringBootDemoApplication
限于篇幅,还有相关特性,这里不一一陈述了。
在实际的项目上,一般会分三种环境dev、test、prod来方便我们的开发和部署,要求我们在开发的时候可以方便地进行环境的切换,又要满足在发布版本的时候可以尽可能减少测试人员的配置。
Spring Profile
多环境
为了实现多环境配置,我们可以在本地开发的时候在Resource文件夹下新建不同环境的配置文件,如下图所示:
这时候我们只需要在主配置文件application.properties文件使用spring.profiles.active = dev/test/prod来进行不同环境的切换
spring.profiles.active = dev
#加载dev配置文件
经过上述步骤,我们即可在本地开发时根据不同的环境进行测试。
主配置文件与不同环境的配置文件
Spring Profile有一个特性即可以在application.properties配置通用的内容,在application-dev.properties文件中配置不同文件的差异化配置,这样可以精简我们的配置文件。配置如下:
# 主配置文件主要用来存放公共设置,不受环境配置影响
spring.profiles.active = @profile.active@ #此处由maven的环境选择决定
spring.server.context-path = /server
# 指定访问资源的根路径
server.port=8081
dev配置文件
# 主要用来存放不同环境差异化配置,包括ftp地址、数据库连接等
spring.datasource.username= root
spring.datasource.password=
spring.datasource.url= localhost:3307/springboot?useUnicode=true&characterEncoding=utf8
spring.datasource.driver-class-name= com.mysql.jdbc.Driver
上述解决方案虽然可以解决我们的环境问题,但是不够优雅,还有一些值得优化的地方,比如打包出的配置文件有四个、每次需要手动修改主配置文件的环境信息等。
Maven Profile
Profile配置
Maven 也提供了 Profile 支持,它允许我们在 pom.xml 中定义多个 Profile ,每个 profile 可以指定自己的一些配置、依赖、触发条件等。例如:
<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<profile.active>dev</profile.active>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<profile.active>prod</profile.active>
</properties>
</profile>
</profiles>
按照上述的配置,我们配置了dev/test/prod三种配置并默认选择dev环境。
资源过滤
在进行打包的时候,我们并不需要把dev或者test等配置文件打包进行,所有我们在打包的时候进行如下配置
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<!--先排除所有的配置文件-->
<excludes>
<exclude>application*.properties</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<!--引入所需环境的配置文件-->
<filtering>true</filtering>
<includes>
<include>application.properties</include>
<include>application-${profile.active}.properties</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>exec</classifier>
</configuration>
</plugin>
</plugins>
</build>
主要是分为两个步骤:
先排除所有的配置文件
引入我们所需要的配置文件,根据${profile.active}来动态指定
从上面的配置文件可以看出,我们引入了两个配置文件,一个是主配置文件(application.properties),一个是实际环境(application-dev.properties)的配置文件,但是到这里还没有结束。我们知道主配置文件的dev/test/prod在没有使用Maven Profile的时候,我们是需要手动修改的,但是使用Maven Profile之后,我们可以把这个工作交给maven去做。
在application.properties进行下面的修改:
spring.profiles.active = @profile.active@ #此处由maven的环境选择决定
这样我们就可以完全把配置文件的指定和生成交给maven。
Spring Profile与Maven Profile具体使用
配置如下:
# 主配置文件主要用来存放公共设置,不受环境配置影响
spring.profiles.active = @profile.active@ #此处由maven的环境选择决定
spring.server.context-path = /server
# 指定访问资源的根路径
server.port=8081
应用在执行业务逻辑之前,必须通过校验保证接受到的输入数据是合法正确的,但很多时候同样的校验出现了多次,在不同的层,不同的方法上,导致代码冗余,浪费时间,违反DRY原则。
每一个控制器都要校验
过多的校验参数会导致代码太长
代码的复用率太差,同样的代码如果出现多次,在业务越来越复杂的情况下,维护成本呈指数上升。
可以考虑把校验的代码封装起来,来解决出现的这些问题。
JSR-303
JSR-303是Java为Bean数据合法性校验提供的标准框架,它定义了一套可标注在成员变量,属性方法上的校验注解。
Hibernate Validation提供了这套标准的实现,在我们引入Spring Boot web starter或者Spring boot starter validation的时候,默认会引入Hibernate Validation。
用法实例
说了这么多废话,上代码。
引入SpringBoot项目
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<!-- 引入lomhok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
编写校验对象
@Data
public class User {
// 名字不允许为空,并且名字的长度在2位到30位之间
// 如果名字的长度校验不通过,那么提示错误信息
@NotNull
@Size(min = 2, max = 30, message = "请检查名字的长度是否有问题")
private String name;
// 不允许为空,并且密码的最小值为6
@NotNull
@Min(6)
@Size(min = 6, max = 12, message = "请检查密码的长度是否有问题")
private String password;
}
创建控制器
@RestController
public class UserController {
// 1. 要校验的参数前,加上@Valid注解
// 2. 紧随其后的,跟上一个BindingResult来存储校验信息
@RequestMapping("/test1")
public Object test1(@Valid User user, BindingResult bindingResult) {
// 如果检验出了问题,就返回错误信息
// 这里我们返回的是全部的错误信息,实际中可根据bindingResult的方法根据需要返回自定义的信息。
// 通常的解决方案为:JSR-303 + 全局ExceptionHandler
if (bindingResult.hasErrors()) {
return bindingResult.getAllErrors();
}
return "OK";
}
}
运行应用
稍作演示下运行的结果,可以看出校验框架已经生效了。
@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 {
@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};
@AliasFor(annotation = EnableAutoConfiguration.class)
String[] excludeName() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};
}
这是RequestMapping的注解,这个注解里包含了4个其他的注解,我大致介绍一下(其实源码里面说的很清楚):
1.@Target
定义注解修饰的目标,比如@RequestMapping 就是用来修饰方法和类
2.@Retention
定义注解的生命周期,分为以下三种:
/源码级别/
// Annotations are to be discarded by the compiler.
SOURCE,
/编译期级别/
/Annotations are to be recorded in the class file by the compiler,but need not be retained by the VM at run time. This is the defaultbehavior./
CLASS,
/运行期级别/
/*Annotations are to be recorded in the class file by the compiler and retained by the VM at run time, so they may be read reflectively.
RUNTIME
3.@Documented
定义注解会被javadoc或者其他类似工具文档化
4.@Mapping
定义注解是一个web mapping annotation
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyFirstAnnotation {
String value() default "";
}
创建完自定义注解后,很显然的思路是如何让注解起作用。这里以输出日志的注解为例,当用自定义注解来修饰方法时,我们期望在方法执行的前后输出日志记录,那么我们必须采用AOP(面向切面编程)的思想,理所当然地,我们需要定义切面类:
@Aspect
@Component
public class HelloAspect {
@Pointcut("@annotation(HelloAnnotation)")
public void annotationPointcut() {}
@Before("annotationPointcut()")
public void beforePointcut(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
Method method = methodSignature.getMethod();
HelloAnnotation annotation = method.getAnnotation(HelloAnnotation.class);
String value = annotation.value();
System.out.println("准备" + value);
}
@After("annotationPointcut()")
public void afterPointcut(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
Method method = methodSignature.getMethod();
HelloAnnotation annotation = method.getAnnotation(HelloAnnotation.class);
String value = annotation.value();
System.out.println("结束" + value);
}
}
重点需要关注的是:切点的定义,切点可以定义成execute(public String sayHello()) 的形式,但是这种形式就和咱们的注解不相关了,因此我们采用@annotation(MyFirstAnnotation) 的形式,这样切点就变成了我们自定义注解所修饰的方法
@HelloAnnotation("hello")
@RequestMapping("/hello")
public String hello() {
System.out.println("hello world!");
return "hello world!";
}
控制台结果输出:
准备hello
hello world!
结束hello
https://gitee.com/xiaolaifeng/sample.springboot/tree/master/springboot-teach4