Spring Boot扩展
在Spring Boot中可以集成第三方的框架如MyBatis、MyBatis-Plus和RabbitMQ等统称为扩展。每一个扩展会封装成一个集成,即Spring Boot的starter(依赖组件)。starter是一种非常重要的机制,不需要烦琐的配置,开发者只需要在项目的依赖中加入starter依赖,Spring Boot就能根据依赖信息自动扫描到要加载的信息并启用相应的默认配置。starter的出现让开发者不再需要查找各种依赖库及相应的配置。所有stater模块都遵循着约定成俗的默认配置,并允许自定义配置,即遵循“约定大于配置”的原则。
常用的starter及其说明如表6.1所示。
表6.1 Spring Boot的starter列表
日志管理
项目的日志主要包括系统日志、应用程序日志和安全日志。运维人员和项目开发人员可以通过日志了解服务器软/硬件信息,检查配置过程中的错误及错误发生的原因。通过分析日志还可以了解服务器的负荷、性能安全性,从而及时采取措施解决发生的问题。
因此,项目人员需要在系统开发和运行时保存日志。关于什么时候保存日志有以下几个要点:
(1)系统初始化时:记录系统和服务的启动参数。在核心模块初始化过程中会依赖一些关键配置项,根据参数不同提供不同的服务,记录当前的参数有助于发生错误后排除问题。
(2)系统提示异常:代码中的异常捕获机制,此类异常的错误级别非常高,是系统在告知开发人员需要关注的错误信息。一般用WARN或者ERROR级别来记录当前的错误日志。
(3)业务流程预期不符:记录与正常流程不同的业务参数,如外部参数不正确、未知的请求信息等。
(4)系统核心角色和组件的关键动作的记录:包括核心业务的日志记录,INFO级别的日志记录,保存微服务各服务节点交互的数据日志记录、系统核心数据表的增、删、改操作的日志记录,以及核心组件运行情况的日志记录等。
(5)第三方服务远程调用:对第三方的服务调用需要保存调用前后的日志记录,方便在发生错误时排查问题。
常用的日志框架
在Java项目开发过程中,最简单的方式是使用System.out.println打印日志,但这种方式有很多缺陷,如I/O瓶颈,而且不利于日志的统一管理。目前市面上有很多日志组件可以集成到Spring Boot中,它们能够快速地实现不同级别的日志分类,以及在不同的时间进行保存。常用的日志框架有以下几个:
1. JUL简介
JUL即java.util.logging.Logger,是JDK自带的日志系统,从JDK 1.4开始出现。其优点是系统自带,缺点是相较于其他的日志框架来说功能不够强大。
2. Apache Commons Logging简介
Apache Commons Logging是Apache提供的一个通用日志API,可以让程序不再依赖于具体的日志实现工具。Apache Commons Logging包中还对其他日志工具(包括Log4j、JUL)进行了简单的包装,可以让应用程序在运行时直接将Apache Commons Logging适配到对应的日志实现工具中。
提示:Apache Common Logging通过动态查找机制,在程序运行时会自动找出真正使用的日志库。这一点与SLF4J不同,SLF4J是在编译时静态绑定真正的Log实现库。
3. Log4j简介
Log4j是Ceki Gülcü实现出来的,后来捐献给Apache,又被称为Log4j1.x,它是Apache的开放源代码项目。在系统中使用Log4j,可以控制日志信息输送的目的地是控制台、文件及数据库等,还可以自定义每一条日志的输出格式,通过定义每一条日志信息的级别,还可以控制日志的生成过程。
Log4j主要是由Loggers(日志记录器)、Appender(输出端)和Layout(日志格式化器)组成。其中:
Logger用于控制日志的输出级别与是否输出日志;
Appender用于指定日志的输出方式(输出到控制台、文件等);Layout用于控制日志信息的输出格式。
Log4j有7种不同的log级别,按照等级从低到高依次为TRACE、DEBUG、INFO、WARN、ERROR、FATAL和OFF。如果配置为OFF级别,表示关闭log。
Log4j支持两种格式的配置文件:properties和XML。
4. Logback简介
Logback是由log4j的创立者Ceki Gülcü设计,是Log4j的升级版。
Logback当前分成3个模块:logback-core、logback- classic和logbackaccess。logback-core是另外两个模块的基础模块。logback-classic是Log4j的一个改良版本,目前依然建议在生产环境中使用。
5. Log4j2简介
Log4j2也是由log4j的创立者Ceki Gulcu设计的,它是Log4j 1.x和Logback的改进版。在项目中使用Log4j2作为日志记录的组件,在日志的吞吐量和性能方面比log4j 1.x提高了10倍,并可以解决一些死锁的Bug,配置也更加简单、灵活。
6. SLF4J
SLF4J是对所有日志框架制定的一种规范、标准和接口,并不是一个具体框架。因为接口并不能独立使用,需要和具体的日志框架配合使用(如Log4j2、Logback)。使用接口的好处是,当项目需要更换日志框架时,只需要更换jar和配置,不需要更改相关的Java代码,SLF4J相当于Java设计模式的门面模式。目前项目的开发中多使用SLF4J+Logback或者SLF4J+Log4J2的组合方式来记录日志。
日志的输出级别
日志的输出是分级别的,不同的日志级别在不同的场合打印不同的日志。常见的日志级别有以下4个:DEBUG:该级别的日志主要输出与调试相关的内容,主要在开发、测试阶段输出。DEUBG日志应尽可能详细,开发者会把各类详细信息记录到DEBUG里,起到调试的作用,包括参数信息、调试细节信息、返回值信息等,方便在开发、测试阶段出现问题或者异常时对问题进行分析和修改。
INFO:该级别的日志主要记录系统关键信息,用来保留系统正常工作期间的关键信息指标。开发者可以将初始化系统配置、业务状态变化信息或者用户业务流程中的核心处理记录到INFO日志中,方便运维及错误回溯时进行场景复现。当在项目完成后,一般会把项目日志级别从DEBUG调成INFO,对于不需要再调试的日志,将通过INFO级别的日志记录这个应用的运行情况,如果出现问题,根据记录的INFO级别的日志来排查问题。
WARN:该级别的日志主要输出警告性质的内容,这类日志可以预知问题的发生,如某个方法入参为空或者参数的值不满足运行方法的条件时。在输出WARN级别的日志时应输出详尽的提示信息,方便开发者和运维人员对日志进行分析。
ERROR:该级别主要指系统错误信息,如错误、异常等。例如,在catch中抓获的网络通信和数据库连接等异常,若异常对系统的整个流程影响不大,可以输出WARN级别的日志。在输出ERROR级别的日志时,要记录方法入参和方法执行过程中产生的对象等数据,在输出带有错误和异常对象的数据时,需要将该对象全部记录,方便后续的Bug修复。
日志的等级由低到高分别是DEBUG<INFO<WARN<ERROR,日志记录一般会记录设置级别及其以下级别的日志。例如,设置日志的级别为INFO,则系统会记录INFO和DEBUG级别的日志,超过INFO级别的日志不会记录。
综上所述,在项目中保存好日志有以下好处:
打印调试:用日志记录变量或者逻辑的变化,方便进行断点调试。
问题定位:程序出现异常后可根据日志快速定位问题所在,方便后期解决问题。
用户行为日志:记录用户的关键操作行为。重要系统逻辑日志记录:方便以后问题的排查和记录。
实战:日志管理之使用AOP记录日志
本小节将新建一个项目,实现使用日志组件和Spring的AOP记录所有Controller入参的功能,本次使用SLF4J+log4j2的方式实现日志的记录。
(1)新建一个项目spring-extend-demo,在pom.xml中添加Web、Log4j2、SLF4J和AOP的依赖坐标,具体如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>spring-extend-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-extend-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter
logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter
logging</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
添加完依赖后,可以查看项目的依赖库,部分依赖库如图6.1和图6.2所示,当前项目中已经引入了SLG4J和Log4j2依赖。
图6.2 SLF4J的依赖
(2)在resources目录下新建一个log4j2.xml配置文件,配置日志的记录如下:
<?xml version="1.0" encoding="UTF-8"?>
<!--
Configuration后面的配置,用于设置log4j2内部的信息输出,可以不设置。当设置成
trace时可以看到log4j2内部的各种详细输出。
-->
<!--
monitorInterval:Log4j能够自动检测、修改配置文件,并设置间隔秒数。
-->
<configuration status="error" monitorInterval="30">
<!--先定义所有的Appender-->
<appenders>
<!--这个输出控制台的配置-->
<Console name="Console" target="SYSTEM_OUT">
<!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝
(onMismatch)-->
<ThresholdFilter level="trace" onMatch="ACCEPT"
onMismatch="DENY"/>
<!--输出日志的格式-->
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level
%class{36} %L%M - %msg%xEx%n"/>
</Console>
<!--文件会打印出所有信息,该日志在每次运行程序时会自动清空,由append属性
决定,适合临时测试用-->
<File name="log" fileName="log/test.log" append="false">
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level
%class{36} %L%M - %msg%xEx%n"/>
</File>
<!-- 打印出所有的信息,如果大小超过size,则超出部分的日志会自动存入按年
份-月份建立的文件夹下面并进行压缩作为存档-->
<RollingFile name="RollingFile" fileName="D:/log/log.log"
filePattern="D:/log/log-${date:yyyy-MM}/log-
%d{MM-dd-yyyy}-%i.log">
<PatternLayout pattern="%d{yyyy-MM-dd 'at' HH:mm:ss z}
%-5level%class{36} %L %M - %msg%xEx%n"/> <!-- 如果一个文件超过50 MB,就会生成下一个日志文件 -->
<SizeBasedTriggeringPolicy size="50MB"/>
<!-- 如不设置DefaultRolloverStrategy属性,则默认同一文件夹下最多
有7个文件,这里设置为20 -->
<DefaultRolloverStrategy max="20"/>
</RollingFile>
</appenders>
<!--定义logger,只有定义了logger并引入上面配置的Appender,当前的Appender
才会生效-->
<loggers>
<!--建立一个默认用户的logger,将其作为日志记录的根配置-->
<root level="info">
<appender-ref ref="RollingFile"/>
<appender-ref ref="Console"/>
</root>
</loggers>
</configuration>
(3)配置保存成功后,每天会根据前面的配置生成一个日志文件,一个日志文件的最大容量为50MB,超过50MB就再新建一个日志文件。新建一个Web入口类HelloController,代码如下:
package com.example.springextenddemo.controller;
import com.example.springextenddemo.vo.UserVO;
import org.springframework.web.bind.annotation.*;
@RestController
public class HelloController {
@GetMapping("/hi")
public String hi(@RequestParam("name")String name){
return "hi "+name;
}
@PostMapping("/hi-post")
public String hiPost(@RequestBody UserVO userVO){ return "hi-post "+userVO;
}
}
Hellocontroller中的参数接收实体类UserVO如下:
package com.example.springextenddemo.vo;
import java.util.StringJoiner;
public class UserVO {
private String name;
private String address;
private int age;
//省略GET和SET方法
}
(4)新建一个AOP类记录日志:
package com.example.springextenddemo.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.Extended
ServletRequestDataBinder;
import javax.servlet.http.HttpServletResponseWrapper;import java.util.HashMap;
import java.util.Map;
/**
* 第一个执行
*/
@Order(1)
/**
* aspect 切面
*/
@Aspect
@Component
public class RequestParamLogAop {
private static final Logger log =
LoggerFactory.getLogger(RequestParamLogAop.class);
/**
* Controller层切点
*/
@Pointcut("execution (*
com.example.springextenddemo.controller..*.*(..))")
public void controllerAspect() {
}
/**
* 环绕通知
*
* @param joinPoint
* @throws Throwable
*/
@Around(value = "controllerAspect()")
public Object around(ProceedingJoinPoint joinPoint) throws
Throwable {
Signature signature = joinPoint.getSignature();
methodBefore(joinPoint,signature);
Object result = joinPoint.proceed();
methodAfterReturn(result, signature);
return result;
} /**
* 方法执行前执行
*
* @param joinPoint
* @param signature
*/
private void methodBefore(JoinPoint joinPoint, Signature
signature) {
//在两个数组中,参数值和参数名的个数和位置是一一对应的
Object[] objs = joinPoint.getArgs();
// 参数名
String[] argNames = ((MethodSignature)
signature).getParameterNames();
Map<String, Object> paramMap = new HashMap<String, Object>();
for (int i = 0; i < objs.length; i++) {
if (!(objs[i] instanceof ExtendedServletRequestDataBinder)
&& !(objs[i] instanceof
HttpServletResponseWrapper)) {
paramMap.put(argNames[i], objs[i]);
}
}
log.info("请求前-方法:{} 的请求参数:{}", signature, paramMap);
}
/**
* 方法执行后的返回值
*/
private void methodAfterReturn(Object result, Signature signature)
{
log.info("请求后-方法:{} 的返回参数是:{}", signature, result);
}
}
(5)新建一个Spring Boot启动类:
package com.example.springextenddemo;
import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringExtendDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringExtendDemoApplication.class,
args);
}
}
(6)修改配置文件application.properties,将日志配置文件添加到Spring Boot中:
logging.config=classpath:log4j2.xml
(7)启动项目,可以在控制台看到新的日志格式,如图6.3所示。
在浏览器中访问localhost:8080/hi?name= cc,结果如图6.4所示。
再访问localhost:8080/hi-post,结果如图6.5所示。
查看日志的配置目录,打开D:\log可以看到日志文件,如图6.6所示,日志内容如图6.7所示,控制台的输出日志和保存日志文件内容一样,如图6.8所示。
图6.7 log日志内容
图6.8 控制台打印的日志
过AOP简单地完成了对所有Controller入口的请求参数的记录,这个功能一般在项目中必须要有,请求入参必须进行记录,以方便问题的回溯。
实战:日志管理之自定义Appender
上面定义的日志配置使用的是Log4j2自带的日志Appender,在Log4j2中常用的Appender如表6.2所示,它们有不同的功能。
表6.2 Log4j2常用的Appender
在项目开发中可以直接使用上面的Appender,也可以自定义一个Appender。下面完成一个自定义的Appender,在打印的日志前面加上自定义的内容,完成自定义日志的开发。
(1)新建一个Appeder的实现类,此类需要继承自类AbstractAppender,代码如下:
package com.example.springextenddemo.appender;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.layout.PatternLayout;
import java.io.Serializable;
/**
* 自定义实现Appender
* @Plugin注解:在log4j2.xml配置文件中使用,指定的Appender Tag
*/
@Plugin(name = "myAppender", category = "Core", elementType =
"appender",
printObject = true)
public class MyLog4j2Appender extends AbstractAppender {
String printString;
/**
*构造函数 可自定义参数 这里直接传入一个常量并输出
*
*/
protected MyLog4j2Appender(String name, Filter filter, Layout<?
extends Serializable> layout,
String printString) {
super(name, filter, layout);
this.printString = printString;
}
/**
* 重写append()方法:在该方法里需要实现具体的逻辑、日志输出格式的设置
* 自定义实现输出
* 获取输出值:event.getMessage().toString()
* @param event
*/
@Override
public void append(LogEvent event) {
if (event != null && event.getMessage() != null) {
//格式化输出
System.out.print("自定义appender"+printString + ":" +
getLayout().toSerializable(event));
}
} /**
* 接收log4j2-spring.xml中的配置项
* @PluginAttribute 是XML节点的attribute值,如<book name="sanguo">
</book>,这里的name是attribute
* @PluginElement 表示XML子节点的元素,例如:
* <book name="sanguo">
* <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l -
%m%n"/>
* </book>
* 其中,PatternLayout是{@link Layout}的实现类
*/
@PluginFactory
public static MyLog4j2Appender createAppender(
@PluginAttribute("name") String name,
@PluginElement("Filter") final Filter filter,
@PluginElement("Layout") Layout<? extends Serializable>
layout,
@PluginAttribute("printString") String printString) {
if (name == null) {
LOGGER.error("no name defined in conf.");
return null;
}
//默认使用 PatternLayout
if (layout == null) {
layout = PatternLayout.createDefaultLayout();
}
//使用自定义的Appender
return new MyLog4j2Appender(name, filter, layout, printString);
}
@Override
public void start() {
System.out.println("log4j2-start方法被调用");
super.start();
}
@Override
public void stop() {
System.out.println("log4j2-stop方法被调用"); super.stop();
}
}
重写的start()方法为初始时调用,在数据库入库、连接缓存或者MQ时,可以在这个方法里进行初始化操作。stop()方法是在项目停止时调用,用来释放资源。
(2)将之前项目中的日志配置文件log4j.xml修改为log4j.xm.bak,再新建一个自定义的Appender的log4j2的配置文件。注意,自定义的Appender的名称要和Java代码中的Appender的名字相同,其配置文件的内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="INFO" monitorInterval="30"
packages="com.example.
springextenddemo">
<!--定义Appenders-->
<appenders>
<myAppender name="myAppender" printString=":start log:">
<!--输出日志的格式-->
<PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l -
%m%n"/>
</myAppender>
</appenders>
<!--自定义logger,只有定义了logger并引入appender,appender才会生效-->
<loggers>
<!--spring和mybatis的日志级别为info-->
<logger name="org.springframework" level="INFO"></logger>
<logger name="org.mybatis" level="INFO"></logger>
<!-- 如果在自定义包中设置为INFO,则可以看见输出的日志不包含debug输出了
-->
<logger name="com.example.springextenddemo" level="INFO"/>
<root level="all">
<appender-ref ref="myAppender"/>
</root> </loggers>
</configuration>
(3)重新启动项目,在浏览器中访问http://localhost:8080/hi?name=cc,可以看到控制台显示的自定义日志如图6.9所示。日志前已经加上了前缀自定义appender:start log,达到了本次自定义Appender的目的。
图6.9 自定义Appender输出