Spring Boot整合Spring Aop
什麼是Aop?
讓我們首先定義一些核心的AOP概念和術語。這些術語並非特定於Spring。不幸的是,AOP術語並不是特別直觀。然而,如果Spring使用自己的術語,則會更加令人困惑。
- 方面(Aspect)︰跨越多個類的模組化關注點。事務管理是企業Java應用程式中橫切關注點的一個很好的例子。在SpringAOP中,方面是通過使用常規類(基於模式的方法)或使用@Aspect註解(@Aspectj樣式)註解的常規類來實現的。
- 連線點(Join point)︰程式執行過程中的一點,如方法的執行或異常的處理。在SpringAOP中,連線點總是表示一個方法執行。
- 通知(Advice) : 一個方面在特定連線點採取的行動。不同型別的通知包括"環繞"、"前“和"後"通知。許多AOP框架(包括Spring)將通知建模為攔截器,並在連線點周圍維護攔截器鏈。
- 切點(Pointcut)︰與連線點匹配的謂詞。通知與切入點表示式關聯,並在與切入點匹配的任何連線點上執行(例如,使用特定名稱執行方法)。pointcut表示式匹配的連線點概念是AOP的核心,Spring預設使用AspectJ pointcut表示式語言。
- 說明(Introduction)∶代表型別宣告其他方法或欄位。SpringAOP允許你向任何advised物件引入新的介面(和相應的實現)。例如,你可以使用一個Introduction使bean實現一個lsModified介面,以簡化快取。(introduction在AspectJ社群中稱為型別間宣告。)
- 目標物件(Target object):由一個或多個方面advised的物件。也稱為"advised物件"。因為SpringAOP是通過使用執行時代理實現的,所以這個物件始終是一個代理物件。
- AOP代理∶由AOP框架建立的用於實現aspect contracts(通知方法執行等)的物件。在Spring框架中,AOP代理是JDK動態代理或CGLIB代理。
- 編織(Weaving):將aspects與其他應用程式型別或物件連結,以建立advised的物件。這可以在編譯時(例如,使用AspectJ編譯器)、載入時或執行時完成。Spring AOP和其他純Java AOP框架一樣,在執行時進行編織。
Spring AOP包含以下幾種通知型別:
- Before advice:在連線點之前執行但不能阻止執行到連線點的通知(除非它丟擲異常)。
- After returning advice:在連線點正常完成後要執行的通知(例如,如果方法返回並且不引發異常)
- After throwing advice:如果方法通過引發異常而退出,則要執行的通知。
- After (finally) advice:無論連線點退出的方式如何(正常或異常返回),都要執行的通知。
- Around advice:環繞連線點(如方法呼叫)的通知。這是最有力的通知。around通知可以在方法呼叫前後執行自定義行為。它還負責通過返回自己的返回值或引發異常來選擇是繼續到連線點還是快捷地執行通知的方法。Around advice是最普遍的advice。由於Spring AOP和AspectJ一樣提供了一系列完整的通知型別,我們建議你使用功能最差的通知型別來實現所需的行為。例如,如果只需要使用方法的返回值更新快取,那麼最好實現after retuming advice,而不是around advice,儘管around advice可以完成相同的事情。使用最具體的通知型別提供了一個更簡單的程式設計模型,並且錯誤的可能性更小。例如,你不需要在用於around通知的joinpoint上呼叫proceed()方法,因此,你不會呼叫失敗。
Spring AOP是純Java實現的。不需要特殊的編譯過程。SpringAOP不需要控制類載入器層次結構,因此適合在Servlet容器或應用程式伺服器中使用。
AOP代理
SpringAOP預設為對AOP代理使用標準的JDK動態代理。這樣就可以代理任何介面(或一組介面)。
SpringAOP也可以使用CGLIB代理。如果要代理類而不是介面,則必須使用CGLIB。預設情況下,如果業務物件不實現介面,則使用cglib。由於程式設計到介面而不是類是很好的實踐,業務類通常實現一個或多個業務介面。在某些情況下(可能很少),需要通知一個介面上沒有宣告的方法,或者需要將代理物件作為具體型別傳遞給方法,則可以強制使用cglib。
定義一個切入點(Pointcut)
切入點決定連線點,從而使我們能夠控制何時執行通知。SpringAOP只支援SpringBean的方法執行連線點,因此你可以將切入點視為匹配SpringBean上方法的執行。一個切入點宣告有兩部分:一個包含一個名稱和任何引數的簽名,一個能精確地確定我們感興趣的執行方法的切入點表示式。在aop的@Aspectj註解樣式中,通過常規方法定義提供切入點簽名,並使用@Pointcut註解指示切入點表示式(作為切入點簽名的方法必須具有void返回型別)。
支援的切入點指示符
SpringAOP支援在切入點表示式中使用的以下AspectJ切入點指示符(PCD):
- execution:用於匹配方法執行連線點。這是使用SpringAOP時要使用的主要切入點指示符。within:限制匹配到特定型別中的連線點(使用SpringAOP時,在匹配型別中宣告的方法的執行)
- this:將匹配限制為連線點(使用SpringAOP時方法的執行),其中bean引用(SpringAOP代理)是給定型別的例項。
- target:限制匹配到連線點(使用SpringAOP時方法的執行),其中目標物件(要代理的應用程式物件)是給定型別的例項。
- args:限制匹配到聯接點(使用SpringAOP時方法的執行),其中引數是給定型別的例項。
- @target:限制匹配到連線點(使用SpringAOP時方法的執行),其中執行物件的類具有給定型別的註解。
- @args:限制匹配到聯接點(使用SpringAOP時方法的執行),其中傳遞的實際引數的執行時型別具有給定型別的註解。
- @within:限制與具有給定註解的型別中的聯接點匹配(使用SpringAOP時,在具有給定註解的型別中宣告的方法的執行)。
- @annotation:限制匹配到連線點的主題(在SpringAOP中執行的方法)具有給定註解的連線點。
一個例子可能有助於清晰地區分切入點簽名和切入點表示式。下面的示例定義一個名為anyOldTransfer的切入點,該切入點與名為Transfer的任何方法的執行相匹配︰
@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature
組合切入點表示式
你可以使用&&、|和!組合切入點表示式。也可以按名稱引用切入點表示式。下面的示例顯示三個切點表示式:
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
共享公共切入點定義
在處理企業應用程式時,開發人員通常希望從多個方面引用應用程式的模組和特定的操作集。我們建議定義一個"SystemArchitecture"方面,該方面為此目的捕獲公共切入點表示式。此類方面通常類似於以下示例:
package com.xyz.someapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SystemArchitecture {
/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.someapp.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.web..*)")
public void inWebLayer() {
}
/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.someapp.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.service..*)")
public void inServiceLayer() {
}
/**
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.someapp.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.dao..*)")
public void inDataAccessLayer() {
}
/**
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
* <p>
* If you group service interfaces by functional area (for example,
* in packages com.xyz.someapp.abc.service and com.xyz.someapp.def.service) then
* the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
* could be used instead.
* <p>
* Alternatively, you can write the expression using the 'bean'
* PCD, like so "bean(*Service)". (This assumes that you have
* named your Spring service beans in a consistent fashion.)
*/
@Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
public void businessService() {
}
/**
* A data access operation is the execution of any method defined on a
* dao interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
public void dataAccessOperation() {
}
}
除返回型別模式(前面程式碼段中的ret-type-pattern)、名稱模式和引數模式之外的所有部分都是可選的。確定方法的返回型別,以便匹配連線點。*最常用作返回型別模式。它匹配任何返回型別。只有當方法返時,完全限定的型別名才匹配。名稱模式與方法名匹配。你可以使用*萬用字元作為名稱模式的全部或部分。如果指定宣告型別模式,請包含字尾.將其連線到名稱模式元件。引數模式稍微複雜一點:()匹配不帶引數的方法,而(..)匹配任何數量(零個或多個)的引數。(*)模式與採用任何型別引數的方法匹配。(*,string)匹配接受兩個引數的方法。第一個可以是任何型別,而第二個必須是字串。有關更多資訊,請參閱AspectJ程式設計指南的語言語義部分。
以下示例顯示了一些常見的切入點表示式:
- 執行任何公共方法:
execution(public **(..))
- 執行任何以set開頭的方法︰
execution( * set*(..))
- 執行任何定義在AccountService類的方法:
execution( * com.xyz.service.AccountService.*(..))
- 執行任何定義在service包中的方法:
execution(* com.xyz.service.*.*(..))
- 執行任何定義在service包或者他的一個子包中的方法:
execution( * com.xyz.service..*.*(..))
- 任何在service包中的連線點(僅僅是Spring AOP中執行的方法)
within(com.xyz.service.*)
- service包或其子包中的任何連線點(僅在SpringAOP中執行的方法)︰
within(com.xyz.service..*)
- 任何實現了AccountService介面的代理連線點(僅在SpringAOP中執行的方法)∶
this(com.xyz.service.AccountService)
- 任何目標物件有@Transactional註解的連線點(僅在SpringAOP中執行的方法)︰
@target(org.springframework.transaction.annotation.Transactional)
- 目標物件的宣告型別具有@transactional註解的任何連線點(僅在Spring AOP中執行的方法)∶
@within(org.springframework.transaction.annotation.Transactional)
- 執行方法具有@transactional註解的任何連線點(僅在Spring AOP中執行方法)∶
@annotation(org.springframework.transaction.annotation.Transactional)
宣告通知
通知與切入點表示式關聯,並在切入點匹配的方法執行before, after,或 around執行。切入點表示式可以是對命名切入點的簡單引用,也可以是就地宣告的切入點表示式。如下所示:
Before Advice
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
//...
}
}
After Returning Advice
當一個匹配的方法正常返回時執行After returming advice。你可以通過@AfterReturning註解來宣告,你可以在同一方面內擁有多個通知宣告(以及其他成員)。在這些示例中,我們只顯示一個通知宣告,以集中顯示每個通知宣告的效果。
有時,你需要訪問通知主體中返回的實際值。你可以使用@AfterReturn的形式繫結返回值以獲得該訪問,如下示例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
//...
}
@AfterReturning(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
returning屬性中使用的名稱必須與advice方法中引數的名稱相對應。當方法執行返回時,返回值作為相應的引數值傳遞給通知方法。返回子句還限制只匹配那些返回指定型別值的方法執行(在本例中是Object,它匹配任何返回值)。
After Throwing Advice
當匹配的方法丟擲異常退出時,After throwing advice執行,你可以使用@AfterThrowing來宣告它:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
//當匹配的方法丟擲異常退出時,After throwihg advice執行
@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}
/**
* 丟擲給定型別的異常時才執行通知,並且通常還需要訪問通知正文中丟擲的異常。
* 可以使用throwing屬性來限制匹配(如果需要-使用throwable作為異常型別),
* 並將引發的異常繫結到advice引數。以下示例顯示瞭如何執行此操作:
*/
@AfterThrowing(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
throwing屬性中使用的名稱必須與advice方法中引數的名稱相對應。當通過引發異常退出方法執行時,異常作為相應的引數值傳遞給通知方法。throwing子句還限制只匹配那些引發指定型別的異常(在本例中是DataAccessException)的方法執行。
After (Finally) Advice
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
Around Advice
最後一種advice是around advice的。around advice執行"around"匹配方法的執行。它有機會在方法執行之前和之後都進行工作,並確定何時、如何以及即使該方法真正開始執行。如果需要以執行緒安全的方式(例如,啟動和停止計時器)共享方法執行前後的狀態,則通常使用around建議。始終使用滿足你要求的最不強大的advice形式(也就是說,如果在around之前不使用around)。使用@Around註解宣告around通知。advice方法的第一個引數必須是ProceedingJoinPoint型別。在通知正文中,對ProceedingJoinPoint呼叫proceed()會導致執行基礎方法。proceed方法也可以傳入Object]。陣列中的值在方法執行過程中用作引數。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
around通知返回的值是方法呼叫方看到的返回值。例如,一個簡單的快取方面可以從快取中返回一個值(如果有),如果沒有,則呼叫proceed ()。請注意,可以在around建議的主體中呼叫proceed一次、多次或根本不呼叫proceed。所有這些都是合法的。
訪問當前JoinPoint
任何advice方法都可以將org.aspectilang.joinpoint型別的引數宣告為其第一個引數(請注意,需要使用around advice從而宣告ProceedingJoinPoint型別作為第一個引數,該引數是JoinPoint的子類)。JoinPoint介面提供了許多有用的方
法:
- getArgs():返回方法引數
- getThis():返回代理物件
- getTarget():返回目標物件
- getSignature():返回被advice的方法描述.
- toString():列印被advice方法的有用描述
在SpringBoot中使用
pom
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.3.6.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.11.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
通知
package com.dev.aop.aspect;
import com.dev.aop.annotation.Log;
import com.dev.aop.util.ServletUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* @Description : spring aop //描述
*/
@Aspect
@Component
@Slf4j
public class LogAspect {
/* 常量 */
private static final String HTTP_POST = "POST";
private static final String HTTP_GET = "GET";
/**
* 切入點
*/
@Pointcut(value = "@annotation(com.dev.aop.annotation.Log)")
public void logPointcut(){
}
/**
* 後置處理(方法返回時觸發)
* @param joinPoint
* @param object
*/
@AfterReturning(value = "logPointcut()",returning = "object")
public void logAfterReturning(JoinPoint joinPoint, Object object) {
resolver(joinPoint, object, null);
}
/**
* 異常通知
* @param joinPoint
* @param e
*/
@AfterThrowing(value = "logPointcut()",throwing = "e")
public void logAfterThrowing(JoinPoint joinPoint,Exception e) {
resolver(joinPoint, null, e);
}
/**
* 解析引數,儲存日誌
* @param joinPoint
* @param object
* @param e
*/
protected void resolver(final JoinPoint joinPoint,final Object object, Exception e) {
try {
//獲取註解
Log log = (Log) getAnnotationLog(joinPoint);
if (Objects.isNull(log)) {
return;
}
/**
* 獲取登入使用者session
* 如果整合shiro使用: User user = (User) SecurityUtils.getSubject().getPrincipal()
*/
User user = (User) ServletUtil.getSession().getAttribute("user");
//假設map為log類
Map<String,Object> map = new HashMap<String, Object>(16);
if (Objects.nonNull(user)) {
//操作人
map.put("oper_user", user.getUsername());
//部門
map.put("oper_dep", user.getDep());
}
//請求主機ip:127.0.0.1
//如果整合shiro使用:getSubject().getSession().getHost()
map.put("ip", getHostIp());
//狀態:成功/失敗
map.put("status", 1);
//請求URL
map.put("req_url", ServletUtil.getRequest().getRequestURI());
//請求方式:GET/POST
map.put("req_method", ServletUtil.getRequest().getMethod());
//類名
String className = joinPoint.getTarget().getClass().getName();
//方法名稱
String methodName = joinPoint.getSignature().getName();
//設定方法名稱
map.put("oper_method", className + "." + methodName + "()");
//返回結果
map.put("json_result", marshal(object));
//處理註解引數
getAnnotationParameter(log,map,joinPoint);
if (Objects.nonNull(e)) {
//狀態:成功/失敗
map.put("status", 2);
//異常資訊
map.put("err_msg", substring(e.toString(), 0, 3000));
}
//------------------------非同步插入資料庫-----------------------------
System.out.println(map.toString());
} catch (Exception exception) {
exception.printStackTrace();
System.out.println(exception.getMessage());
}
}
/**
* 註解引數處理
*/
public void getAnnotationParameter(Log log, Map<String, Object> m, JoinPoint joinPoint) throws Exception {
//標題
m.put("title", log.title());
//操作類別:INSERT、UPDATE
m.put("business", log.type().ordinal());
//是否儲存請求引數
if (log.isParameter()){
getParameter(m,joinPoint);
}
}
/**
* 獲取註解
*/
private Log getAnnotationLog(JoinPoint joinPoint) throws Exception {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
return method.getAnnotation(Log.class);
}
return null;
}
/**
* 獲取並解析請求引數
*/
private void getParameter(Map<String, Object> map, JoinPoint joinPoint) throws Exception {
String method = ServletUtil.getRequest().getMethod();
//POST
if (HTTP_POST.equals(method)) {
Object[] args = joinPoint.getArgs();
if (args != null || args.length > 0){
map.put("req_parameter", substring(marshal(args), 0, 3000));
}
}
//GET
if (HTTP_GET.equals(method)) {
Map<String, String[]> parameterMap = ServletUtil.getRequest().getParameterMap();
if (!CollectionUtils.isEmpty(parameterMap)) {
map.put("req_parameter", substring(marshal(parameterMap), 0, 3000));
}
}
}
private String marshal(Object value) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
ObjectWriter objectWriter = objectMapper.writerWithDefaultPrettyPrinter();
return objectWriter.writeValueAsString(value);
}
/**
* 擷取字串
*/
public static String substring(final String str, int start, int end) {
if (str == null) {
return "";
}
if (end < 0) {
end = str.length() + 1 + end;
}
if (start < 0) {
start = str.length() + start;
}
if (end > str.length()) {
end = str.length();
}
if (start > end) {
return "";
}
if (start < 0) {
start = 0;
}
if (end < 0) {
end = 0;
}
return str.substring(start, end);
}
//可以忽略不計
@Data
public class User implements Serializable {
private int id;
private String username;
private String dep;
public User(int id, String username, String dep) {
this.id = id;
this.username = username;
this.dep = dep;
}
}
/**
* 獲取ip
*/
public static String getHostIp() {
try {
return InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
}
return "127.0.0.1";
}
}
測試
package com.dev.aop.controller;
import com.dev.aop.annotation.Log;
import com.dev.aop.aspect.LogAspect;
import com.dev.aop.util.ServletUtil;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* @Description : 測試 //描述
*/
@RestController
public class IndexController {
@Log(title = "測試Post", type = Log.BusinessType.INSERT, isParameter = true)
@PostMapping("/Post")
public String indexPost(@RequestBody Map<String, Object> map) {
System.out.println(map.toString());
ServletUtil.getSession().setAttribute("user",new LogAspect().new User(1,"admin","技術部"));
return "success";
}
@Log(title = "測試Get", type = Log.BusinessType.OTHER, isParameter = true)
@GetMapping("/Get")
public String indexGet(Map<String, Object> map) {
System.out.println(map.toString());
ServletUtil.getSession().setAttribute("user",new LogAspect().new User(1,"admin","技術部"));
return "success";
}
@Log(title = "測試err", type = Log.BusinessType.OTHER, isParameter = true)
@PostMapping("/err")
public String indexErr(@RequestBody Map<String, Object> map) {
System.out.println(map.toString());
ServletUtil.getSession().setAttribute("user",new LogAspect().new User(1,"admin","技術部"));
throw new RuntimeException("NullPointException");
}
}
測試還是老方式
generated-requests.http
###
POST http://localhost/Post
Content-Type: application/json
{
"admin": 5,
"name": "張三"
}
1列印的是請求引數,2處理過後封裝好引數
###
POST http://localhost/err
Content-Type: application/json
{
"admin": 2,
"name": "kangkang"
}
可以從2處看到引數"err_msg"中已經獲取到控制檯列印的異常資訊
參考資料:Spring5中文參考指南
GitHub:傳送門
相關文章
- Spring-boot整合AOP及AOP相關學習Springboot
- Spring Boot整合Spring BatchSpring BootBAT
- Spring Boot整合Spring SecuritySpring Boot
- spring boot AOP筆記Spring Boot筆記
- spring boot使用Jedis整合Redis實現快取(AOP)Spring BootRedis快取
- Spring Boot系列十九 Spring boot整合 swaggerSpring BootSwagger
- Spring Boot 2.0(八):Spring Boot 整合 MemcachedSpring Boot
- Spring Boot:整合Spring Data JPASpring Boot
- Spring boot學習(三) Spring boot整合mybatisSpring BootMyBatis
- Spring boot學習(四)Spring boot整合DruidSpring BootUI
- Spring Boot 入門(五):整合 AOP 進行日誌管理Spring Boot
- Spring Boot之IOC&AOPSpring Boot
- Spring Boot 整合 KafkaSpring BootKafka
- Spring Boot 整合 MyBatisSpring BootMyBatis
- Spring Boot整合SwaggerSpring BootSwagger
- Spring Boot整合rabbitmqSpring BootMQ
- Spring Boot整合RedisSpring BootRedis
- Spring Boot 整合redisSpring BootRedis
- Spring Boot 整合 rabbitmqSpring BootMQ
- Spring Boot 整合 elasticsearchSpring BootElasticsearch
- Spring Boot 整合 dockerSpring BootDocker
- Spring Boot 整合 elkSpring Boot
- Spring Boot 整合 ApolloSpring Boot
- spring boot整合shiroSpring Boot
- spring boot整合jooqSpring Boot
- spring boot整合HadoopSpring BootHadoop
- Spring Boot整合SocketSpring Boot
- Spring Boot整合Spring Cloud Netflix元件Spring BootCloud元件
- spring-boot 整合 spring-securitySpringboot
- spring-boot 整合 spring-sessionSpringbootSession
- 在Spring Boot框架中使用AOPSpring Boot框架
- Spring Boot系列(三):Spring Boot整合Mybatis原始碼解析Spring BootMyBatis原始碼
- 使用Spring Boot整合ConsulSpring Boot
- Spring-Boot整合RedisSpringbootRedis
- 【Spring Boot】快速整合SwaggerSpring BootSwagger
- Spring Boot 快速整合SwaggerSpring BootSwagger
- spring boot(三)整合 redisSpring BootRedis
- Spring Boot Actuator 整合 PrometheusSpring BootPrometheus