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輸出