SpringBoot使用AOP記錄接口操作日志的方法
**前言:**我們項目中可能有這種需求,每個人請求了哪些接口?做了什么事情?參數(shù)是什么?重要的接口我們需要記錄操作日志以便查找。操作日志和系統(tǒng)日志不一樣,操作日志必須要做到簡單易懂。所以如何讓操作日志不跟業(yè)務(wù)邏輯耦合,如何讓操作日志的內(nèi)容易于理解,如何讓操作日志的接入更加簡單?我們不可能在每個接口中去一一處理,可以借助Spring提供的AOP能力+自定義注解輕松應(yīng)對。
一、操作日志簡介
日志記錄量是很大的,所以只記錄關(guān)鍵地方并按期歸檔,最好是存在如elasticsearch中;如果存在數(shù)據(jù)庫中,分表是不錯的選擇。
1.1、系統(tǒng)日志和操作日志的區(qū)別
系統(tǒng)日志:系統(tǒng)日志主要是為開發(fā)排查問題提供依據(jù),一般打印在日志文件中;系統(tǒng)日志的可讀性要求沒那么高,日志中會包含代碼的信息,比如在某個類的某一行打印了一個日志。
操作日志:主要是對某個對象進行新增操作或者修改操作后記錄下這個新增或者修改,操作日志要求可讀性比較強,因為它主要是給用戶看的,比如訂單的物流信息,用戶需要知道在什么時間發(fā)生了什么事情。再比如,客服對工單的處理記錄信息。
操作日志記錄的是:某一個“時間”“誰”對“什么”做了什么“事情”。
比如LogUtil.log(orderNo,“訂單創(chuàng)建”,“小明”) :這里解釋下為什么記錄操作日志的時候都綁定了一個 OrderNo,因為操作日志記錄的是:某一個“時間”“誰”對“什么”做了什么“事情”。當(dāng)查詢業(yè)務(wù)的操作日志的時候,會查詢針對這個訂單的的所有操作,所以代碼中加上了 OrderNo,記錄操作日志的時候需要記錄下操作人,所以傳了操作人“小明”進來。
操作日志的記錄格式大概分為下面幾種:
- 單純的文字記錄,比如:2021-09-16 10:00 訂單創(chuàng)建。
- 簡單的動態(tài)的文本記錄,比如:2021-09-16 10:00 訂單創(chuàng)建,訂單號:NO.11089999,其中涉及變量訂單號“NO.11089999”。
- 修改類型的文本,包含修改前和修改后的值,比如:2021-09-16 10:00 用戶小明修改了訂單的配送地址:從“金燦燦小區(qū)”修改到“銀盞盞小區(qū)” ,其中涉及變量配送的原地址“金燦燦小區(qū)”和新地址“銀盞盞小區(qū)”。
- 修改表單,一次會修改多個字段。
1.2、操作日志記錄實現(xiàn)方式
(1)使用 Canal 監(jiān)聽數(shù)據(jù)庫記錄操作日志
Canal是阿里開源的一款基于 MySQL 數(shù)據(jù)庫增量日志解析中間件,提供增量數(shù)據(jù)訂閱和消費的開源組件,通過采用監(jiān)聽數(shù)據(jù)庫 Binlog 的方式,這樣可以從底層知道是哪些數(shù)據(jù)做了修改,然后根據(jù)更改的數(shù)據(jù)記錄操作日志。
這種方式的優(yōu)點是和業(yè)務(wù)邏輯完全分離。缺點也很明顯,局限性太高,只能針對數(shù)據(jù)庫的更改做操作日志記錄,如果修改涉及到其他團隊的 RPC 的調(diào)用,就沒辦法監(jiān)聽數(shù)據(jù)庫了。舉個例子:給用戶發(fā)送通知,通知服務(wù)一般都是公司內(nèi)部的公共組件,這時候只能在調(diào)用 RPC 的時候手工記錄發(fā)送通知的操作日志了。
(2)高度代碼耦合:在業(yè)務(wù)邏輯中直接調(diào)用日志記錄接口
通過日志文件的方式記錄,這樣就可以把日志單獨保存在一個文件中,然后通過日志收集可以把日志保存在 Elasticsearch或者數(shù)據(jù)庫中,接下來我們看下如何生成可讀的操作日志。
//操作日志記錄的是:某一個“時間”“誰”對“什么”做了什么“事情”。
log.info("訂單創(chuàng)建")
log.info("訂單已經(jīng)創(chuàng)建,訂單編號:{}",?orderNo)
log.info("修改了訂單的配送地址:從“{}”修改到“{}”,?"金燦燦小區(qū)",?"銀盞盞小區(qū)")
但是當(dāng)業(yè)務(wù)變得復(fù)雜后,記錄操作日志放在業(yè)務(wù)代碼中會導(dǎo)致業(yè)務(wù)的邏輯比較繁雜。
(3)采用AOP方式:AOP方式能和業(yè)務(wù)邏輯解耦
為了解決上面問題,一般采用 AOP 的方式記錄日志,讓操作日志和業(yè)務(wù)邏輯解耦,接下來看一個簡單的 AOP 日志的例子。
@LogRecord(content="修改了配送地址")
public void modifyAddress(updateDeliveryRequest request){
//?更新派送信息?電話,收件人、地址
doUpdate(request);
}我們可以在注解的操作日志上記錄固定文案,這樣業(yè)務(wù)邏輯和業(yè)務(wù)代碼可以做到解耦,讓我們的業(yè)務(wù)代碼變得純凈起來??赡苡型瑢W(xué)注意到,上面的方式雖然解耦了操作日志的代碼,但是記錄的文案并不符合我們的預(yù)期,文案是靜態(tài)的,沒有包含動態(tài)的文案,因為我們需要記錄的操作日志是:用戶%s修改了訂單的配送地址,從“%s”修改到“%s”。接下來,我們介紹一下如何優(yōu)雅地使用 AOP 生成動態(tài)的操作日志。
二、AOP面向切面編程
2.1、AOP簡介
AOP為Aspect Oriented Programming的縮寫,意為:面向切面編程,通過預(yù)編譯方式和運行期間動態(tài)代理實現(xiàn)程序功能的統(tǒng)一維護的一種技術(shù)。**這種在運行時,動態(tài)地將代碼切入到類的指定方法或指定位置上的編程思想就是面向切面的編程。**利用AOP可以將日志記錄,性能統(tǒng)計,安全控制,事務(wù)處理,異常處理等代碼從業(yè)務(wù)邏輯代碼中劃分出來作為公共部分,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發(fā)的效率。
2.2、AOP作用
日志記錄,性能統(tǒng)計,安全控制,事務(wù)處理,異常處理等等。
在面向切面編程AOP的思想里面,核心業(yè)務(wù)和切面通用功能(例如事務(wù)處理、日志管理、權(quán)限控制等)分別獨立進行開發(fā),然后把切面功能和核心業(yè)務(wù)功能 “編織” 在一起,這就叫AOP。這種思想有利于減少系統(tǒng)的重復(fù)代碼,降低模塊間的耦合度,并有利于未來的可拓展性和可維護性。
2.3、AOP相關(guān)術(shù)語
通知(Advice)
通知描述了切面要完成的工作以及何時執(zhí)行。比如我們的日志切面需要記錄每個接口調(diào)用時長,就需要在接口調(diào)用前后分別記錄當(dāng)前時間,再取差值。
- 前置通知(Before):在目標(biāo)方法調(diào)用前調(diào)用通知功能;
- 后置通知(After):在目標(biāo)方法調(diào)用之后調(diào)用通知功能,不關(guān)心方法的返回結(jié)果;
- 返回通知(AfterReturning):在目標(biāo)方法成功執(zhí)行之后調(diào)用通知功能;
- 異常通知(AfterThrowing):在目標(biāo)方法拋出異常后調(diào)用通知功能;
- 環(huán)繞通知(Around):通知包裹了目標(biāo)方法,在目標(biāo)方法調(diào)用之前和之后執(zhí)行自定義的行為。
切點(Pointcut)
切點定義了通知功能被應(yīng)用的范圍。比如日志切面的應(yīng)用范圍就是所有接口,即所有controller層的接口方法。
切面(Aspect)
切面是通知和切點的結(jié)合,定義了何時、何地應(yīng)用通知功能。
引入(Introduction)
在無需修改現(xiàn)有類的情況下,向現(xiàn)有的類添加新方法或?qū)傩浴?/p>
織入(Weaving)
把切面應(yīng)用到目標(biāo)對象并創(chuàng)建新的代理對象的過程。
連接點(JoinPoint)
通知功能被應(yīng)用的時機。比如接口方法被調(diào)用的時候就是日志切面的連接點。
2.4、JointPoint和ProceedingJoinPoint
JointPoint是程序運行過程中可識別的連接點,這個點可以用來作為AOP切入點。JointPoint對象則包含了和切入相關(guān)的很多信息,比如切入點的方法,參數(shù)、注解、對象和屬性等。我們可以通過反射的方式獲取這些點的狀態(tài)和信息,用于追蹤tracing和記錄logging應(yīng)用信息。
(1)JointPoint
通過JpointPoint對象可以獲取到下面信息
# 返回目標(biāo)對象,即被代理的對象 Object getTarget(); # 返回切入點的參數(shù) Object[] getArgs(); # 返回切入點的Signature Signature getSignature(); # 返回切入的類型,比如method-call,field-get等等,感覺不重要? String getKind();
(2)ProceedingJoinPoint
Proceedingjoinpoint 繼承了 JoinPoint。是在JoinPoint的基礎(chǔ)上暴露出 proceed 這個方法。proceed很重要,這個是aop代理鏈執(zhí)行的方法。環(huán)繞通知=前置+目標(biāo)方法執(zhí)行+后置通知,proceed方法就是用于啟動目標(biāo)方法執(zhí)行的。
暴露出這個方法,就能支持 aop:around 這種切面(而其他的幾種切面只需要用到JoinPoint,,這也是環(huán)繞通知和前置、后置通知方法的一個最大區(qū)別。這跟切面類型有關(guān)), 能決定是否走代理鏈還是走自己攔截的其他邏輯。
ProceedingJoinPoint可以獲取切入點的信息:
- 切入點的方法名字及其參數(shù)
- 切入點方法標(biāo)注的注解對象(通過該對象可以獲取注解信息)
- 切入點目標(biāo)對象(可以通過反射獲取對象的類名,屬性和方法名)
//獲取切入點方法的名字,getSignature());是獲取到這樣的信息 :修飾符+ 包名+組件名(類名) +方法名
String methodName = joinPoint.getSignature().getName()
//獲取方法的參數(shù),這里返回的是切入點方法的參數(shù)列表
Object[] args = joinPoint.getArgs();
//獲取方法上的注解
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null)
{
xxxxxx annoObj= method.getAnnotation(xxxxxx.class);
}
//獲取切入點所在目標(biāo)對象
Object targetObj =joinPoint.getTarget();
//可以發(fā)揮反射的功能獲取關(guān)于類的任何信息,例如獲取類名如下
String className = joinPoint.getTarget().getClass().getName();2.5、AOP相關(guān)注解
Spring中使用注解創(chuàng)建切面
- @Aspect:用于定義切面
- @Before:通知方法會在目標(biāo)方法調(diào)用之前執(zhí)行
- @After:通知方法會在目標(biāo)方法返回或拋出異常后執(zhí)行
- @AfterReturning:通知方法會在目標(biāo)方法返回后執(zhí)行
- @AfterThrowing:通知方法會在目標(biāo)方法拋出異常后執(zhí)行
- @Around:通知方法會將目標(biāo)方法封裝起來
- @Pointcut:定義切點表達式
切點表達式:指定了通知被應(yīng)用的范圍,表達式格式:
execution(方法修飾符 返回類型 方法所屬的包.類名.方法名稱(方法參數(shù)) //com.hs.demo.controller包中所有類的public方法都應(yīng)用切面里的通知 execution(public * com.hs.demo.controller..(…)) //com.hs.demo.service包及其子包下所有類中的所有方法都應(yīng)用切面里的通知 execution(* com.hs.demo.service….(…)) //com.hs.demo.service.EmployeeService類中的所有方法都應(yīng)用切面里的通知 execution(* com.hs.demo.service.EmployeeService.*(…))
(1)@POINTCUT定義切入點,有以下2種方式:
方式一:設(shè)置為注解@LogFilter標(biāo)記的方法,有標(biāo)記注解的方法觸發(fā)該AOP,沒有標(biāo)記就沒有。
@Aspect
@Component
public class LogFilter1Aspect {
@Pointcut(value = "@annotation(com.hs.aop.annotation.LogFilter)")
public void pointCut(){
}
}自定義注解LogFilter:
@Target(ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface LogFilter1 {
}對應(yīng)的Controller方法如下,手動添加@LogFilter注解:
@RestController
public class AopController {
@RequestMapping("/aop")
@LogFilter
public String aop(){
System.out.println("這是執(zhí)行方法");
return "success";
}
}方式二:采用表達式批量添加切入點,如下方法,表示AopController下的所有public方法都添加LogFilter1切面。
@Pointcut(value = "execution(public * com.train.aop.controller.AopController.*(..))")
public void pointCut(){
}(2)@Around環(huán)繞通知
@Around集成了@Before、@AfterReturing、@AfterThrowing、@After四大通知。需要注意的是,他和其他四大通知注解最大的不同是需要手動進行接口內(nèi)方法的反射后才能執(zhí)行接口中的方法,換言之,@Around其實就是一個動態(tài)代理。
/**
* 環(huán)繞通知是spring框架為我們提供的一種可以在代碼中手動控制增強部分什么時候執(zhí)行的方式。
*
*/
public void aroundPringLog(ProceedingJoinPoint pjp)
{
//拿到目標(biāo)方法的方法簽名
Signature signature = pjp.getSignature();
//獲取方法名
String name = signature.getName();
try {
//@Before
System.out.println("【環(huán)繞前置通知】【"+name+"方法開始】");
//這句相當(dāng)于method.invoke(obj,args),通過反射來執(zhí)行接口中的方法
proceed = pjp.proceed();
//@AfterReturning
System.out.println("【環(huán)繞返回通知】【"+name+"方法返回,返回值:"+proceed+"】");
} catch (Exception e) {
//@AfterThrowing
System.out.println("【環(huán)繞異常通知】【"+name+"方法異常,異常信息:"+e+"】");
}
finally{
//@After
System.out.println("【環(huán)繞后置通知】【"+name+"方法結(jié)束】");
}
}
proceed = pjp.proceed(args)這條語句其實就是method.invoke,以前手寫版的動態(tài)代理,也是method.invoke執(zhí)行了,jdk才會利用反射進行動態(tài)代理的操作,在Spring的環(huán)繞通知里面,只有這條語句執(zhí)行了,spring才會去切入到目標(biāo)方法中。
為什么說環(huán)繞通知就是一個動態(tài)代理呢?
proceed = pjp.proceed(args)這條語句就是動態(tài)代理的開始,當(dāng)我們把這條語句用try-catch包圍起來的時候,在這條語句前面寫的信息,就相當(dāng)于前置通知,在它后面寫的就相當(dāng)于返回通知,在catch里面寫的就相當(dāng)于異常通知,在finally里寫的就相當(dāng)于后置通知。
三、AOP切面實現(xiàn)接口日志記錄
通過AOP面向切面編程技術(shù),實現(xiàn)操作日志記錄功能以便進行信息監(jiān)控和信息統(tǒng)計,不侵入業(yè)務(wù)代碼邏輯,提高代碼的可重用性。
3.1、引入AOP依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
3.2、創(chuàng)建日志信息封裝類WebLog
用于封裝需要記錄的日志信息,包括操作的描述、時間、消耗時間、url、請求參數(shù)和返回結(jié)果等信息。
@Data
public class WebLog {
/**
* 操作描述
*/
private String description;
/**
* 操作用戶
*/
private String username;
/**
* 操作時間
*/
private Long startTime;
/**
* 消耗時間
*/
private Integer spendTime;
/**
* 根路徑
*/
private String basePath;
/**
* URI
*/
private String uri;
/**
* URL
*/
private String url;
/**
* 請求類型
*/
private String method;
/**
* IP地址
*/
private String ip;
/**
* 請求參數(shù)
*/
private Object parameter;
/**
* 請求返回的結(jié)果
*/
private Object result;
}
3.3、創(chuàng)建切面類WebLogAspect
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 統(tǒng)一日志處理切面
*/
@Aspect
@Component
@Order(1)
@Slf4j
public class WebLogAspect {
//定義切點表達式,指定通知功能被應(yīng)用的范圍
@Pointcut("execution(public * com.hs.notice.controller.*.*(..))")
public void webLog() {
}
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
}
/**value切入點位置
* returning 自定義的變量,標(biāo)識目標(biāo)方法的返回值,自定義變量名必須和通知方法的形參一樣
* 特點:在目標(biāo)方法之后執(zhí)行的,能夠獲取到目標(biāo)方法的返回值,可以根據(jù)這個返回值做不同的處理
*/
@AfterReturning(value = "webLog()", returning = "ret")
public void doAfterReturning(Object ret) throws Throwable {
}
//通知包裹了目標(biāo)方法,在目標(biāo)方法調(diào)用之前和之后執(zhí)行自定義的行為
//ProceedingJoinPoint切入點可以獲取切入點方法上的名字、參數(shù)、注解和對象
@Around("webLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable
{
long startTime = System.currentTimeMillis();
//獲取當(dāng)前請求對象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//記錄請求信息
WebLog webLog = new WebLog();
//前面是前置通知,后面是后置通知
Object result = joinPoint.proceed();
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
long endTime = System.currentTimeMillis();
String urlStr = request.getRequestURL().toString();
webLog.setBasePath(StrUtil.removeSuffix(urlStr, URLUtil.url(urlStr).getPath()));
webLog.setIp(request.getRemoteUser());
webLog.setMethod(request.getMethod());
webLog.setParameter(getParameter(method, joinPoint.getArgs()));
webLog.setResult(result);
webLog.setSpendTime((int) (endTime - startTime));
webLog.setStartTime(startTime);
webLog.setUri(request.getRequestURI());
webLog.setUrl(request.getRequestURL().toString());
log.info("{}", JSONUtil.parse(webLog));
return result;
}
/**
* 根據(jù)方法和傳入的參數(shù)獲取請求參數(shù)
*/
private Object getParameter(Method method, Object[] args)
{
List<Object> argList = new ArrayList<>();
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
//將RequestBody注解修飾的參數(shù)作為請求參數(shù)
RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
if (requestBody != null) {
argList.add(args[i]);
}
//將RequestParam注解修飾的參數(shù)作為請求參數(shù)
RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
if (requestParam != null) {
Map<String, Object> map = new HashMap<>();
String key = parameters[i].getName();
if (!StringUtils.isEmpty(requestParam.value())) {
key = requestParam.value();
}
map.put(key, args[i]);
argList.add(map);
}
}
if (argList.size() == 0) {
return null;
} else if (argList.size() == 1) {
return argList.get(0);
} else {
return argList;
}
}
}
3.4、調(diào)用接口進行測試
隨便訪問一個com.hs.notice.controller包下的接口,可以看到WebLogAspect輸出的日志信息:
2022-01-07 10:43:33.732 INFO 11400 --- [nio-8086-exec-7] com.cernet.notice.util.WebLogAspect:
{"result":{"code":200,"data":{"appName":"Tiktok","isSend":1,"id":1,"sendAT":1640596730378,
"tilte":"【測試數(shù)據(jù)】","receiverEmail":"110@qq.com","content":"測試"},"message":"請求成功"},
"basePath":"http://localhost:8086","method":"GET","startTime":1641523413722,
"uri":"/api/email-notices/1","url":"http://localhost:8086/api/email-notices/1","spendTime":6}
四、AOP切面+自定義注解實現(xiàn)接口日志記錄
4.1、自定義日志注解
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
?* 定義操作日志注解
?*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperatorLog
{
// 操作
String operate();
// 模塊
String module();
}4.2、定義攔截操作日志的切面
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import com.elon.springbootdemo.manager.OperatorLog;
import com.elon.springbootdemo.model.ResponseModel;
/**
* 操作日志切面定義
*/
@Aspect
@Component
public class OperatorLogApsect {
private static Logger logger = LogManager.getLogger(OperatorLogApsect.class);
@Pointcut("@annotation(com.hs.springbootdemo.aop.OperatorLog)")
public void operatorLog() {
}
@SuppressWarnings("rawtypes")
@AfterReturning(returning="result", pointcut="operatorLog()&&@annotation(log)")
public void afterReturn(JoinPoint joinPoint, ResponseModel result, OperatorLog log) {
/**
* 接口調(diào)用信息可以記日志,也可以寫到數(shù)據(jù)庫
*/
StringBuilder sb = new StringBuilder();
sb.append("模塊:").append(log.module());
sb.append("|操作:").append(log.operate());
sb.append("|接口名稱:").append(joinPoint.getSignature().getName());
sb.append("|錯誤碼:").append(result.getRetCode());
sb.append("|錯誤信息:").append(result.getErrorMsg());
logger.info(sb.toString());
}
}
4.3、在接口上增加操作日志注解
@RequestMapping(value="/v1/query-user", method=RequestMethod.GET)
@OperatorLog(operate="查詢用戶", module="用戶管理")
public ResponseModel queryUser(@RequestParam(value="user", required=true) String user)?
{
log.info("[INFO]user info:" + user);
log.warn("[WARN]user info:" + user);
log.error("[ERROR]user info:" + user);
return ResponseModel.success(null);
}4.4、調(diào)用接口,可以看到輸出的日志信息

參考鏈接:
SpringAOP中的ProceedingJoinPoint使用,配合注解的方式
使用攔截器(intercept)和AOP寫操作日志-springboot
到此這篇關(guān)于SpringBoot使用AOP記錄接口操作日志的文章就介紹到這了,更多相關(guān)SpringBoot AOP記錄接口內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot中MyBatis使用自定義TypeHandler的實現(xiàn)
本文主要介紹了SpringBoot中MyBatis使用自定義TypeHandler,當(dāng)默認(rèn)的類型映射不能滿足需求時,自定義?TypeHandler?就非常有用,具有一定的參考價值,感興趣的可以了解一下2024-08-08
SpringBoot整合spring-data-jpa的方法
這篇文章主要介紹了SpringBoot整合spring-data-jpa的方法,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-06-06
使用SpringBoot+EasyExcel+Vue實現(xiàn)excel表格的導(dǎo)入和導(dǎo)出詳解
這篇文章主要介紹了使用SpringBoot+VUE+EasyExcel?整合導(dǎo)入導(dǎo)出數(shù)據(jù)的過程詳解,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-08-08

