[Spring] Spring AOP

🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343

🏵️热门专栏:

🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm=1001.2014.3001.5482

🍕 Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482

🧀线程与网络(96平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482

🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482

🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482

🍃 Spring(97平均质量分)https://blog.csdn.net/2301_80050796/category_12724152.html?spm=1001.2014.3001.5482

感谢点赞与关注~~~

目录

  • [1. AOP概述](#1. AOP概述)
  • [2. Spring AOP快速入门](#2. Spring AOP快速入门)
    • [2.1 引入AOP依赖](#2.1 引入AOP依赖)
    • [2.2 编写AOP程序](#2.2 编写AOP程序)
  • [3. Spring AOP详解](#3. Spring AOP详解)
    • [3.1 核心概念](#3.1 核心概念)
      • [3.1.1 切点(PointCut)](#3.1.1 切点(PointCut))
      • [3.1.2 连接点(Join Point)](#3.1.2 连接点(Join Point))
      • [3.1.3 通知(Advice)](#3.1.3 通知(Advice))
      • [3.1.4 切面(Aspect)](#3.1.4 切面(Aspect))
    • [3.2 通知类型](#3.2 通知类型)
    • [3.3 @Pointcut](#3.3 @Pointcut)
    • [3.4 切面优先级@Order](#3.4 切面优先级@Order)
    • [3.5 切点表达式](#3.5 切点表达式)
      • [3.5.1 execution表达式](#3.5.1 execution表达式)
      • [3.5.2 @annotation(翻译:注解)](#3.5.2 @annotation(翻译:注解))
  • [4. Spring AOP原理](#4. Spring AOP原理)
    • [4.1 代理模式](#4.1 代理模式)
      • [4.1.1 静态代理](#4.1.1 静态代理)
      • [4.1.2 动态代理](#4.1.2 动态代理)

1. AOP概述

  • 什么是AOP?
    所谓AOP,就是面相切面的编程.什么是面向切面的编程呢,切面就是指定某一类特定的问题,所以,面向切面的编程就是正对于同一类问题进行编程.
    简单来说,就是针对某一类事情的集中处理.
  • 什么是Spring AOP?
    AOP是一种思想,实现AOP的方法有很多,有Spring AOP,有AspectJ,有CGLIB等.Spring AOP是其中的一种实现方式.

某种程度上,他和我们前面提到的统一功能处理的效果差不多,但是,统一功能处理并不等同于SpringAOP,拦截器的作用维度是URL,即一次请求响应,但是AOP的作用维度更加细致,可以对包括包,类,方法,参数等进行统一功能的处理,可以实现更加复杂的业务逻辑.

举例:

现在有一些业务执行效率比较低,我们需要对接口进行优化,第一步需要定位出耗时较长的业务方法,在针对业务额来进行优化.我们就可以在每一个方法的结束和开始的地方加上时间戳,之后作差展示出来.但是在一个项目中,我们有好多接口,此时在接口中一个个低添加时间戳又不是很现实,此时我们就需要用到AOP.

接下来,我们就来看看AOP如何使用.

2. Spring AOP快速入门

需求: 统计图书管理系统各个接口和方法的执行时间.

2.1 引入AOP依赖

在pom文件中添加依赖:

xml 复制代码
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.2 编写AOP程序

记录Controller中每个方法的执行时间.

java 复制代码
package com.jrj.books.component;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect//表示切面
@Component
@Slf4j
public class TimeAspect {
    @Around("execution(* com.jrj.books.controller.*.*(..)))")//切点表达式
    public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {//参数是连接点
        long begin = System.currentTimeMillis();
        Object ret = proceedingJoinPoint.proceed();//执行连接点方法
        long end = System.currentTimeMillis();
        log.info("执行耗时:" + (end-begin));
        return ret;
    }
}

上述代码的测试结果如下:

我们看到了接口返回了执行耗时.

下面我们对上面的代码进行详细的解释:

  1. @Aspect : 表示的是切面.表示这个类是一个切面类.

  2. @Around: 环绕通知,在目标方法前后都会有代码执行 .

  3. proceedingJoinPoint.proceed(): 执行加入切面的连接点.

  4. execution(* com.jrj.books.controller.*.*(..))):切点表达式,表示这个切面对该项目下的那个方法生效.

  5. ProceedingJoinPoint proceedingJoinPoint: 加入切面的连接点.

3. Spring AOP详解

3.1 核心概念

3.1.1 切点(PointCut)

切点,也就是切入点,我们一般在通知类型注解的后面使用execution切点表达式 来描述切点.切点就是告诉程序对那些方法的功能进行加强.

3.1.2 连接点(Join Point)

满足切点表达式规则的方法,就是连接点,也就是可以被该切面所作用的方法.连接点的数据类型是:ProceedingJoinPoint.也就是com.jrj.books.controller.包下的所有方法都是该切面的连接点.

切点与连接点之间的关系

连接点事满足切点表达式的元素,切点可以看做是一个保存了众多连接点的集合.

3.1.3 通知(Advice)

通知就是具体要做的工作,指在指定的方法中重复那些逻辑 ,也就是共性功能.

比如上面实现计算运行前后的时间差的业务逻辑就叫做通知.

3.1.4 切面(Aspect)

切面=切点+通知

切面既包含了逻辑的定义,也包含连接点的定义.

切面说在的类,我们一般称为切面类.

3.2 通知类型

SpringAOP中的通知类型有以下几种:

  • @Around:环绕通知,表示该方法在目标方法前后都会被执行.
  • @Before:前置通知,表示该方法只在目标方法之前被执行.
  • @After:后置通知,表示该方法只在目标方法之后被执行,无论是否有异常发生.
  • @AfterReturning:返回后通知,表示该方法只在目标方法之后被执行,方法返回了之后才会执行,有异常发生不会执行.
  • @AfterThrowing:异常后通知.表示方法在发生异常之后执行.

代码演示:

java 复制代码
@Slf4j
@Component
@Aspect
public class AspectDemo {
    @Before("execution(* com.jrj.aspect.*.*(..))")
    public void before(){
        log.info("before method");
    }
    @After("execution(* com.jrj.aspect.*.*(..))")
    public void after(){
        log.info("after method");
    }
    @Around("execution(* com.jrj.aspect.*.*(..))")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("around before");
        Object o = proceedingJoinPoint.proceed();
        log.info("around after");
        return o;
    }
    @AfterReturning("execution(* com.jrj.aspect.*.*(..))")
    public void afterReturning(){
        log.info("afterReturning method");
    }
    @AfterThrowing("execution(* com.jrj.aspect.*.*(..))")
    public void afterThrowing(){
        log.info("afterThrowing method");
    }
}

注意\] 被`@Around`标志的方法必须有形式和返回值.其他注解标志的方法可以加上`JoinPoint`类型的形式参数,\*\*用来获取原始方法的数据. 下面我们准备测试代码:其中一个正常执行,另外一个制造一些异常出来. ```java @RestController @RequestMapping("/test") public class TestController { @RequestMapping("/t1") public String t1(){ return "t1"; } @RequestMapping("t2") public String t2(){ int a = 10/0; return ""; } } ``` 下面我们来执行代码,观察后端日志: 1. 正常执行的代码 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/cdca206c983746eba0e5976734d0802c.png) 在程序运行正常的情况下,`@AfterThrowing`注解的方法不会执行. 从上面的执行结果中,我们可以看出:`@Around`有前置逻辑和后置逻辑两部分,这两个逻辑再内层就是`@Before`和`@After`标识的方法,再往内层就是`@AfterReturning`标注的方法. ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/37c21abcc1c94161971af32f4ef6b913.png) 2. 异常的情况 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/0c191a87ab7d42c18df9f363da416d23.png) 在异常发生的情况下,`@Around`标识的后置逻辑不会被执行.`@AfterReturning`标识的方法不会被执行.`@AfterThrowing`表示的方法会被执行. ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/aab218d324ec4b8f9020ae39af2eda31.png) \[注意事项

• @Around 环绕通知需要调用ProceedingJoinPoint.proceed() 来让原始方法执行 ,其他通知不需要考虑目标方法执行.
• @Around 环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的.

3.3 @Pointcut

上面的代码中存在一个问题,就是切点表达式大量重复,这时候,Spring提供了@Pointcut注解用来把公共的切点表达式提取出来.到时候直接使用方法名把它提取出来即可.

java 复制代码
@Slf4j
@Component
@Aspect
public class AspectDemo {
    @Pointcut("execution(* com.jrj.aspect.*.*(..))")
    public void pointCut(){}
    @Before("pointCut()")
    public void before(){
        log.info("before method");
    }
    @After("pointCut()")
    public void after(){
        log.info("after method");
    }
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("around before");
        Object o = proceedingJoinPoint.proceed();
        log.info("around after");
        return o;
    }
    @AfterReturning("pointCut()")
    public void afterReturning(){
        log.info("afterReturning method");
    }
    @AfterThrowing("pointCut()")
    public void afterThrowing(){
        log.info("afterThrowing method");
    }
}

注意\] 当切点使用private修饰的时候,仅可以在该类中使用.当其他切面类也需要使用到该切点的时候,就需要把private改成public,并使用**全限定方法名**. ### 3.4 切面优先级@Order 当我们在一个项目中,定义了多个切面类,**并且这些切面类的多个切点都匹配到了同一个目标方法**,当目标方法运行的时候,这些切面类中的方法都会执行,那么这些方法执行的顺序是什么样的呢?我们通过代码来验证. ```java @RequestMapping("/test2") public class TestController { @RequestMapping("/t1") public String t1(){ return "t1"; } } @Slf4j @Component @Aspect public class AspectDemo1 { @Pointcut("execution(* com.jrj.aspect.order.*.*(..))") public void pt(){} @Before("pt()") public void before(){ log.info("AspectDemo1 before"); } @After("pt()") public void after(){ log.info("AspectDemo1 after"); } } @Slf4j @Component @Aspect public class AspectDemo2 { @Before("com.jrj.aspect.order.AspectDemo1.pt()") public void before(){ log.info("AspectDemo2 before"); } @After("com.jrj.aspect.order.AspectDemo1.pt()") public void after(){ log.info("AspectDemo2 after"); } } @Slf4j @Component @Aspect public class AspectDemo3 { @Before("com.jrj.aspect.order.AspectDemo1.pt()") public void before(){ log.info("AspectDemo3 before"); } @After("com.jrj.aspect.order.AspectDemo1.pt()") public void after(){ log.info("AspectDemo3 after"); } } ``` 观察日志: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/b85c742371f943edbeb026f42d2af42d.png) 通过上述程序的运行结果,可以看出: 存在多个切面类对应一个方法的时候,默认按照切面类的字母序排序.**Before和After成对称式分布**. * @Before:字母序靠前的先通知. * @After:字母序靠后的先通知. 此外,我们还可以通过Spring的注解,来控制切面的执行顺序:`@Order`. 使用方式如下: ```java @Slf4j @Component @Aspect @Order(1) public class AspectDemo1 { ... } @Slf4j @Component @Aspect @Order(3) public class AspectDemo2 { ... } @Slf4j @Component @Aspect @Order(2) public class AspectDemo3 { ... } ``` 观察日志: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/64afda7d6e544165835bbfad85c3314a.png) 我们可以得出以下结论: * @Before:数字小的先执行 * @After:数字大的先执行 和上面一样,Before和After也是呈对称式分布. ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/8a1ecbedf8de46b8a92c14ac5bd46d78.png) ### 3.5 切点表达式 上面的代码中,我们一直在使用切点表达式来描述切点,下面我们来介绍一下切点表达式. 切点表达式最常见的有以下两种方式: 1. `execution(...)`: 根据**方法的签名来匹配**. 2. `@annotation`:根据**注解匹配**. #### 3.5.1 execution表达式 `execution(<访问限定符> <返回类型> <包名.类名.方法(方法参数)><异常>)` 其中访问**限定符可以省略** . ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/723f4674fd0a4c88a49fd036fe28044a.png) 切点表达式支持通配符: 1. `*`:匹配任何字符,只可以匹配一个元素,可以匹配返回类型,包名,类名,方法名,方法参数. a. 包名使用`* `表示任意包(⼀层包使用⼀个`*`) b. 类名使用`* `表示任意类 c. 返回值使用`*` 表示任意返回值类型 d. 方法名使用`*` 表示任意方法 e. 参数使用`*` 表示⼀个任意类型的参数 2. `..`:匹配多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数. a. 使用 `..` 配置包名,标识此包以及此包下的所有子包 b. 可以使用 `..` 配置参数,任意个任意类型的参数 \[注意\] `*`只可以匹配一个元素,`..`可以匹配多个元素. 举例: * 匹配TestController下的所有无参方法. execution(* com.example.demo.controller.TestController.*()) * 匹配TestController下的所有方法 execution(* com.example.demo.controller.TestController.*(..)) * 匹配com.example.demo包下,子孙包下的所有类的所有方法 execution(* com.example.demo..*(..)) * 匹配所有包下面的TestController类的所有方法. execution(* com..TestController.*(..)) #### 3.5.2 @annotation(翻译:注解) 准备接口测试代码: ```java @RestController @RequestMapping("/user") public class UserController { @RequestMapping("/u1") public String user1(){ return "u1"; } @RequestMapping("/u2") public String user2(){ return "u2"; } } @RestController @RequestMapping("/test") public class TestController { @RequestMapping("/t1") public String t1(){ return "t1"; } @RequestMapping("t2") public String t2(){ int a = 10/0; return ""; } } ``` 如果我们要匹配多个无规则的方法呢?比如UserController中的user1方法和TestController中的t1方法.这时候使用execution显得有些麻烦.这时候我们就可以借助切点表达式的另一种方式.`@annotation`注解来描述一类切点.具体实现步骤如下: 1. 编写自定义注解 2. 使用`@annotation`表达式来描述切点. 3. 在连接点的方法上**添加自定义注解**. * 自定义注解 创建一个自定义注解类,创建的方式和创建类是一样的. ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/223eec0dbfbc46b7a33dd76150e6b501.png) ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyAspect {} ``` 1. `@Target`注解,代表的是注解可以修饰的对象,常用的有以下四种取值: `ElementType.TYPE`:用于描述类、接口(包括注解类型)或enum声明 `ElementType.METHOD`:描述方法 `ElementType.PARAMETER`:描述参数 `ElementType.TYPE_USE`:可以标注任意类型 2. `@Retention`注解,代表的是`Annotation`被保留的时间.表示的是该注解的生命周期. `RetentionPolicy.SOURCE`:表示注解仅存在于源代码中,编译成字节码后会被丢弃. `RetentionPolicy.CLASS`:编译时注解.表示注解存在于源代码和字节码中,但在运行时会被丢弃. `RetentionPolicy.RUNTIME`:运行时注解,表示注解存在于源代码,字节码和运行时中. * 切面类 使用`@annotation`切点表达式定义切点,只对@MyAspect生效. ```java @Aspect @Slf4j @Component public class AspectDemo4 { @Before("@annotation(com.jrj.aspect.MyAspect)") public void before(){ log.info("before method"); } @After("@annotation(com.jrj.aspect.MyAspect))") public void after(){ log.info("after method"); } } ``` * 为指定方法添加自定义注解,在加上自定义注解之后,在调用接口的时候就会执行切面中的方法. 在测试代码的方法下面加上自定义注解: ```java @RestController @RequestMapping("/test") public class TestController { @MyAspect @RequestMapping("/t1") public String t1(){ ... } @MyAspect @RequestMapping("t2") public String t2(){ ... } } @RestController @RequestMapping("/user") public class UserController { @MyAspect @RequestMapping("/u1") public String user1(){ ... } @MyAspect @RequestMapping("/u2") public String user2(){ ... } } ``` 观察日志: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/fa6c44e0fde04b058560910b2fc774ce.png) 我们发现测试的接口打印出了制定的日志. ## 4. Spring AOP原理 SpringAOP使用的是**动态代理**模式来实现的.他所涉及的设计模式是代理模式. ### 4.1 代理模式 代理模式,也叫委托模式 为其他对象提供⼀种代理以控制对这个对象的访问.它的作用就是通过提供⼀个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用. ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/08ae4f9f277d4ee1a3592a9d8aea5ab4.png)![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/5ec6a31c9f3d4cf2861de8ab0640e8cf.png) > 举例说明代理模式: > > 房屋中介与房东,艺人与经纪人,老板与秘书. 代理模式的主要角色: 1. Subject:业务接口类.这里定义的是代理和被代理对象要做的事情.可以抽象类或者接口. 2. RealSubject:业务实现类.具体的业务执行,也就是被代理的对象. 3. Proxy:代理类.RealSubject的代理. 根据代理创建的时期,可以把代理模式分为**动态代理** 和**静态代理**. #### 4.1.1 静态代理 静态代理:在程序运行前,代理类的.class文件就已经存在了.(在出租房子之前,中介已经做好了相关的工作就等租户来租房子了) 1. 接口定义(定义房东和中介要做的事情) ```java public interface HouseSubject { void rentHouse(); } ``` 2. 实现接口(房东出租房子) ```java public class RealHouseProxy implements HouseSubject{ @Override public void rentHouse() { System.out.println("我要出租房子"); } } ``` 3. 代理(中介,帮房东出租房子) ```java public class Proxy implements HouseSubject{ private RealHouseProxy realHouseProxy; public Proxy(RealHouseProxy realHouseProxy) { this.realHouseProxy = realHouseProxy; } @Override public void rentHouse() { System.out.println("我要代理出租房子"); realHouseProxy.rentHouse(); System.out.println("代理完成"); } } ``` 4. 使用 ```java public static void main(String[] args) { RealHouseProxy realHouseProxy1 = new RealHouseProxy(); Proxy proxy = new Proxy(realHouseProxy1); proxy.rentHouse(); } ``` 运行结果: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/fbc66206eddf42faa29f0d4009156128.png) 虽然静态代理也完成了代理,但是由于代码是写死的,对目标方法的增强都是手动来完成的,非常不灵活,所以我们有了动态代理. #### 4.1.2 动态代理 我们不需要针对每个目标对象都单独创建一个代理对象,而是把这个代理对象的工作推迟到程序运行的时候由JVM来实现,也就是在程序运行的时候,根据需要动态创建代理. 常见的动态代理实现模式有两种方式: 1. jdk动态代理 实现步骤如下: * 首先定义一个接口及其实现类(相当于房东). * 之后自定义一个类,实现`InvocationHandler`接口并重写`invoke`方法,在invoke方法中**调用目标方法(被代理的方法)并自定义一些处理逻辑**. * 这里调用方法的时候使用的是反射的方式,使用invoke()方法来调用方法(**反射方式**),并传入被代理对象和args(数组中存放的是方法的参数)参数. * 通过`Proxy.newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h`方法创建代理对象. ```java public interface HouseSubject { void rentHouse(); } public class RealHouseProxy implements HouseSubject{ @Override public void rentHouse() { System.out.println("我要出租房子"); } } public class JDKInvocationHandler implements InvocationHandler { private Object object; public JDKInvocationHandler(Object o) { this.object = o; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("我是代理,开始代理"); Object object = method.invoke(this.object,args); System.out.println("代理结束"); return object; } public static void main(String[] args) { HouseSubject houseSubject = new RealHouseProxy(); HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(houseSubject.getClass().getClassLoader(), new Class[]{HouseSubject.class},new JDKInvocationHandler(houseSubject)); proxy.rentHouse(); } } ``` [注意事项] 1. 在动态代理类中,由于代理的对象可以是任何类型的,所以被代理对象应该是Object类型的. 2. 在使用newInstance方法创建代理对象的时候,第一个传入的是被代理对象的加载器,第二个传入的是一个class类的数组,数组中放着被代理对象接口的class,最后一个放的是我们提前设定好的代理类的对象. 2. CGLIB动态代理 jdk代理致命的问题就是他只可以代理接口(实现一个接口的类),但是CGLIB动态代理可以既可以实现接口,又可以实现类. CGLIB动态代理类实现步骤: * 引入CGLIB依赖 * 定义一个被代理类 * 自定义一个代理类,并实现`MethodInterceptor `接口,实现`intercept`方法,`Intercept`方法用于调用被代理类的方法,方法和jdk代理中的`invoke`类似. * 通过`Enhancer`类的`create()`方法创建代理对象.在其中传入接口的class和之前写好的自定义CGLIB代理类的对象. ```xml cglib cglib 3.3.0 ``` ```java public interface HouseSubject { void rentHouse(); } public class RealHouseSubject implements HouseSubject{ @Override public void rentHouse() { System.out.println("我要出租房子"); } } public class CGLIBInterceptor implements MethodInterceptor { private Object object; public CGLIBInterceptor(Object object) { this.object = object; } @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("我是代理,开始代理"); Object o1 = methodProxy.invoke(object,objects); System.out.println("代理结束"); return o1; } } public class Main { public static void main(String[] args) { HouseSubject houseSubject = new RealHouseSubject(); HouseSubject proxy = (HouseSubject) Enhancer.create(HouseSubject.class,new CGLIBInterceptor(houseSubject)); proxy.rentHouse(); } } ``` CGLB代理模式和jdk代理模式非常类似.

相关推荐
生擒小朵拉3 分钟前
STM32添加库函数
java·javascript·stm32
Z_z在努力9 分钟前
【杂类】Spring 自动装配原理
java·spring·mybatis
程序员爱钓鱼20 分钟前
Go语言实战案例-开发一个Markdown转HTML工具
前端·后端·go
小小菜鸡ing36 分钟前
pymysql
java·服务器·数据库
getapi39 分钟前
shareId 的产生与传递链路
java
桦说编程1 小时前
爆赞!完全认同!《软件设计的哲学》这本书深得我心
后端
thinktik1 小时前
还在手把手教AI写代码么? 让你的AWS Kiro AI IDE直接读飞书需求文档给你打工吧!
后端·serverless·aws
我没想到原来他们都是一堆坏人2 小时前
(未完待续...)如何编写一个用于构建python web项目镜像的dockerfile文件
java·前端·python
沙二原住民2 小时前
提升数据库性能的秘密武器:深入解析慢查询、连接池与Druid监控
java·数据库·oracle