【SpringFramework】面向切面编程-SpringAOP

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 本文简要记录了Spring AOP相关知识点,及基本的使用方法。

AOP 面向切面编程

在不改变原有方法的前提下,添加新的功能,一般是日志、事务或其他非业务的逻辑。
代理模式
   属于经典的二十三种设计模式之一,为结构型模式。
   作用为不直接调用目标类方法,而是通过代理类调用目标类方法。

一、引入的例子

  1. 起初,我们有一个计算器的模拟类,实现了整数的加减乘除;
public interface Calculator {
    int add(int a, int b);
    int sub(int a, int b);
    int mul(int a, int b);
    int div(int a, int b);
}
public class CalculatorImpl implements Calculator{
    @Override
    public int add(int a, int b) {
        int result = a + b;
        System.out.println("a = " + a + ", b = " + b +", a + b = " + result);
        return result;
    }
    @Override
    public int sub(int a, int b) {
        int result = a - b;
        System.out.println("a = " + a + ", b = " + b +", a - b = " + result);
        return result;
    }
    @Override
    public int mul(int a, int b) {
        int result = a * b;
        System.out.println("a = " + a + ", b = " + b +", a × b = " + result);
        return result;
    }
    @Override
    public int div(int a, int b) {
        int result = a / b;
        System.out.println("a = " + a + ", b = " + b +", a ÷ b = " + result);
        return result;
    }
}
  1. 主要的功能我们已经实现。但是现在,我们想在所有计算操作的前后加上操作日志,此时我们需要修改每个方法,如下以add为例,其他类似:
public int add(int a, int b) {
    System.out.println("[日志] add 方法开始执行...")
    int result = a + b;
    System.out.println("a = " + a + ", b = " + b +", a + b = " + result);
    System.out.println("[日志] add 方法结束...")
    return result;
}
  1. 回顾一下,我们发现如下几个问题:
  1. 我们为了增加日志,侵入了核心代码,破坏了原有方法的稳定性;
  2. 所有的方法都需要加类似的代码,重复劳动,代码冗余;
  3. 如果后面要修改一个日志,就要动核心代码,耦合性太高。
  1. 为解决上述问题,我们能想到的方法就是解耦:将附加功能,即此处的日志,从业务代码中抽离出去。
  2. 但是如何将这些附加功能抽离呢?如果只是单纯的提取积累并不能解决这个问题,所以我们需要引入新的技术——代理模式。

二、代理模式

代理模式,也叫 虚拟代理,远程代理, Protection Proxy 等,是一种结构型设计模式,用于提供对象的替代品,以便在代理对象中提供一些附加能力。
术语解释:

  • 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象和方法。
  • 目标:被代理“套用”了非核心逻辑代码的类、对象和方法。

不使用代理模式

使用代理模式后

(1) 静态代理

静态代理是指在编译期间就确定好代理类,代理类和目标类是强耦合的。这就使得代理类不具备通用性和灵活性。

package com.sheeprunner.aop.aband;
/**
 * @description:
 * @author: RunningSheep
 * @date: 2024-12-05 23:48
 * @version: V1.0
 */
public class CalculatorStaticProxy implements Calculator{
    // 将被代理的目标对象声明为成员变量
    private Calculator target;
    public CalculatorStaticProxy(Calculator target) {
        this.target = target;
    }
    @Override
    public int add(int a, int b) {
        // 附加功能由代理类中的代理方法来实现
        System.out.println("[日志] add 方法开始了,参数是:" + a + "," + b);
        // 通过目标对象来实现核心业务逻辑
        int addResult = target.add(a, b);
        System.out.println("[日志] add 方法结束了,结果是:" + addResult);
        return addResult;
    }
    @Override
    public int sub(int a, int b) {
        // 附加功能由代理类中的代理方法来实现
        System.out.println("[日志] sub 方法开始了,参数是:" + a + "," + b);
        // 通过目标对象来实现核心业务逻辑
        int subResult = target.add(a, b);
        System.out.println("[日志] sub 方法结束了,结果是:" + subResult);
        return subResult;
    }
    @Override
    public int mul(int a, int b) {
        // 附加功能由代理类中的代理方法来实现
        System.out.println("[日志] mul 方法开始了,参数是:" + a + "," + b);
        int mulResult = target.mul(a, b);
        System.out.println("[日志] mul 方法结束了,结果是:" + mulResult);
        return mulResult;
    }
    @Override
    public int div(int a, int b) {
        // 附加功能由代理类中的代理方法来实现
        System.out.println("[日志] div 方法开始了,参数是:" + a + "," + b);
        int divResult = target.div(a, b);
        System.out.println("[日志] div 方法结束了,结果是:" + divResult);
        return divResult;
    }
}

(2)动态代理


动态代理是指在运行期间,根据需要创建代理类,代理类和目标类是弱耦合的。

public class DynamicProxyFactory {
    private Object target;
    public DynamicProxyFactory(Object target) {
        this.target = target;
    }
    public Object getProxyInstance() {
        /**
         * newProxyInstance():创建一个代理实例
         * 其中有三个参数:
         * 1、classLoader:加载动态生成的代理类的类加载器
         * 2、interfaces:目标对象实现的所有接口的class对象所组成的数组
         * 3、invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重写接口中的抽象方法
         */
        ClassLoader classLoader = target.getClass().getClassLoader();
        Class<?>[] interfaces = target.getClass().getInterfaces();
        InvocationHandler invocationHandler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                /**
                 * proxy:代理对象
                 * method:代理对象需要实现的方法,即其中需要重写的方法
                 * args:method所对应方法的参数
                 */
                Object result = null;
                try {
                    System.out.println("[DynamicProxy][log]" + method.getName() + ",参数:" + Arrays.toString(args));
                    result = method.invoke(target, args);
                    System.out.println("[DynamicProxy][log]" + method.getName() + ",结果:" + result);
                } catch (Exception e) {
                    e.printStackTrace();
                    System.out.println("[DynamicProxy][log]" + method.getName() + ",异常:" + e.getMessage());
                } finally {
                    System.out.println("[DynamicProxy][log]" + method.getName() + ",执行完毕!");
                }
                return result;
            }
        };
        return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
    }
}

三、AOP

AOP(Aspect Oriented Programming),即面向切面编程,是一种编程思想,它把应用程序切分为模块,这些模块之间通过接口或公共的编程语言元素进行通信,例如方法调用。
AOP 的主要思想是把应用程序的各个模块进行解耦,把公共的功能抽取出来,形成新的模块,然后把各个模块通过接口或公共的编程语言元素进行通信,从而实现复用。
AOP是对面向对象的补充和完善,它通过引入新的概念和模式,为对象行为添加额外的功能,而无需修改源代码。
降低耦合,提供复用性,提高开发效率。

基本概念

(1)横切关注点
分散在各个模块中解决同一问题的代码,例如日志记录、事务管理、权限控制、数据缓存、用户校验等。
(2)通知(增强)
增强,就是我们要增强的功能,比如安全、事务、日志等。
每个横切关注点上要做的事情都要写一个方法来实现,这写方法叫通知方法。

  • 前置通知:在被代理目标执行前执行
  • 返回通知:在被代理目标执行结束后执行(“寿终正寝”)
  • 异常通知:在被代理目标执行异常后执行(“死于非命”)
  • 后置通知:在被代理目标执行最后结束后执行(“盖棺定论”)
  • 环绕通知:使用try……catch……finally……结构围绕整改被代理的目标方法,包括上面四种通知所对应的位置

    (3)切面
    封装通知方法的类。

    (4)目标
    被代理的对象。
    (5)代理
    向目标对象应用通知之后创建的代理对象。
    (6)连接点
    逻辑概念:把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。
    通俗说,就是spring允许你使用通知的地方

    (7)切入点
    点位连接点的方式。每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。

如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。

Spring 的 AOP 技术可以通过切入点定位到特定的连接点。通俗说,要实际去增强的方法

切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

作用

  1. 简化代码:把方法中的重复代码抽取出来,让被抽取方法只关注自己的核心功能,提高内聚性。
  2. 代码增强:把特定的功能封装到切面类中,哪里有需要就往上套,被套用了切面逻辑的方法就被切面增强了。

四、基于注解的AOP


  1. 分为两种动态代理:JDK动态代理和CGLIB动态代理。
  2. 目标类有接口时JDK动态代理和cglib动态代理都适用,没有接口则只能使用cglib动态代理。
  3. JDK动态代理生成的代理类在com.sun.proxy包下,类名为$proxy1,和目标类实现相同的接口。
  4. cglib生成的代理类会在目标类的同一个包下,并继承目标类。
  5. 动态代理(InvocationHandler):JDK原生实现,需要被代理的目标类必须实现接口,因为这个技术要求代理对象和目标对象实现同样接口(兄弟拜把子模式)。
  6. cglib通过继承目标类实现代理,故无需接口(任干爹模式)。
  7. AspectJ:是AOP思想的一种实现。本质上是静态代理,将代理逻辑“织入”被代理的目标类的编译后字节码文件中,所以最终效果是动态的。weaver就是织入器。Spring借用了AspectJ的注解,未使用这种实现。

(1)实现步骤

1)引入相关依赖:

<!--spring aop依赖-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.3.39</version>
</dependency>
<!--spring aspects依赖-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.39</version>
</dependency>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd">
    <context:component-scan base-package="com.sheeprunner.aop.bannotation" />
    <!--
        基于注解的AOP的实现:
        1、将目标对象和切面交给IOC容器管理(注解+扫描)
        2、开启AspectJ的自动代理,为目标对象自动生成代理
        3、将切面类通过注解@Aspect标识
    -->
    <aop:aspectj-autoproxy />
</beans>

2)创建目标接口、实现目标类:
同上CalculatorCalculatorImpl

3)创建切面类

package com.sheeprunner.aop.bannotation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
 * @description: 实现日志切面
 * @author: RunningSheep
 * @date: 2024-12-08 14:46
 * @version: V1.0
 */
// @Aspect表示这是一个切面类
@Aspect
// @Component注解保证这个切面类能够放到IOC容器中
@Component
public class LogAspect {
    public static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
    @Before("execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))")
    public void beforeMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        String argstr = Arrays.toString(args);
        logger.info("LogAspect-前置通知,方法名:{},参数:{}", methodName, argstr);
    }
    @After("execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))")
    public void afterMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        logger.info("LogAspect-后置通知,方法名:{}", methodName);
    }
    @AfterReturning(value = "execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))",returning = "result")
    public void afterReturnMethod(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        logger.info("LogAspect-返回通知,方法名:{},结果:{}", methodName, result);
    }
    @AfterThrowing(value = "execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))", throwing = "e")
    public void afterThrowingMethod(JoinPoint joinPoint, Throwable e) {
        String methodName = joinPoint.getSignature().getName();
        logger.info("LogAspect-异常通知,方法名:{},异常信息:{}", methodName, e);
    }
    @Around("execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))")
    public Object aroundMethod(ProceedingJoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        String args = Arrays.toString(joinPoint.getArgs());
        Object result = null;
        try {
            logger.info("环绕通知-->目标对象方法执行之前");
            //目标对象(连接点)方法的执行
            result = joinPoint.proceed();
            logger.info("环绕通知-->目标对象方法返回值之后");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            logger.info("环绕通知-->目标对象方法出现异常时");
        } finally {
            logger.info("环绕通知-->目标对象方法执行完毕");
        }
        return result;
    }
}

4)运行结果

@Test
public void test01() {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    Calculator calculator = context.getBean("calculatorImpl", Calculator.class);
    // Calculator calculator = context.getBean("intelligent", Calculator.class);
    // Calculator calculator = context.getBean(Calculator.class);
    calculator.add(2, 5);
    // calculator.div(1, 0);
}

5)解释说明

各种通知

  • 前置通知:使用@Before注解标识,在被代理的目标方法执行
  • 返回通知:使用@AfterReturning注解标识,在被代理的目标方法成功结束后执行(寿终正寝
  • 异常通知:使用@AfterThrowing注解标识,在被代理的目标方法异常结束后执行(死于非命
  • 后置通知:使用@After注解标识,在被代理的目标方法最终结束后执行(盖棺定论
  • 环绕通知:使用@Around注解标识,使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

各种通知的执行顺序:

  • Spring版本5.3.x以前:
  • 前置通知
  • 目标操作
  • 后置通知
  • 返回通知或异常通知
  • Spring版本5.3.x以后:
  • 前置通知
  • 目标操作
  • 返回通知或异常通知
  • 后置通知

切入点表达式语法:

@Before("execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))")

  • 用*号代替“权限修饰符”和“返回值”部分 表示“权限修饰符”和“返回值”不限
  • 在包名的部分,一个“*”号只能代表包的层次结构中的一层,表示这一层是任意的。
  • 例如:*.Hello匹配com.Hello,不匹配com.atguigu.Hello
  • 在包名的部分,使用“*..”表示包名任意、包的层次深度任意
  • 在类名的部分,类名部分整体用*号代替,表示类名任意
  • 在类名的部分,可以使用*号代替类名的一部分
  • 例如:*Service匹配所有名称以Service结尾的类或接口
  • 在方法名部分,可以使用*号表示方法名任意
  • 在方法名部分,可以使用*号代替方法名的一部分
  • 例如:*Operation匹配所有方法名以Operation结尾的方法
  • 在方法参数列表部分,使用(..)表示参数列表任意
  • 在方法参数列表部分,使用(int,..)表示参数列表以一个int类型的参数开头
  • 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的
  • 切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
  • 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
  • 例如:execution(public int ..Service.(.., int)) 正确
    例如:execution( int *..Service.(.., int)) 错误

重用切入点表达式:

// 内部定义的切入点表达式
    @Pointcut("execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))")
    public void beforePointCutInner() {
    }
    // 三种使用方法
    // @Before("execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))")
    // @Before("beforePointCutInner()")
    @Before("com.sheeprunner.aop.bannotation.CalculatorBeforePointCut.beforePointCutOutter()")
    public void beforeMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        String argstr = Arrays.toString(args);
        logger.info("LogAspect-前置通知,方法名:{},参数:{}", methodName, argstr);
    }
// 外部定义的切入点表达式
public class CalculatorBeforePointCut {
    @Pointcut("execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))")
    public void beforePointCutOutter() {
    }
}

获取通知的相关信息

  1. 获取连接点信息
@Before("com.sheeprunner.aop.bannotation.CalculatorBeforePointCut.beforePointCutOutter()")
    public void beforeMethod(JoinPoint joinPoint) {
        // 获取连接点的签名信息,getName获取方法名
        String methodName = joinPoint.getSignature().getName();
        // 获取目标方法的实参信息
        Object[] args = joinPoint.getArgs();
        String argstr = Arrays.toString(args);
        logger.info("LogAspect-前置通知,方法名:{},参数:{}", methodName, argstr);
    }
  1. 获取目标方法返回值

returnning="result"

@AfterReturning(value = "execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))",returning = "result")
    public void afterReturnMethod(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        logger.info("LogAspect-返回通知,方法名:{},结果:{}", methodName, result);
    }
  1. 获取目标方法异常

throwing="e"

@AfterThrowing(value = "execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))", throwing = "e")
    public void afterThrowingMethod(JoinPoint joinPoint, Throwable e) {
        String methodName = joinPoint.getSignature().getName();
        logger.info("LogAspect-异常通知,方法名:{},异常信息:{}", methodName, e);
    }
  1. 环绕通知
@Around("execution(public int com.sheeprunner.aop.bannotation.Calculator.*(..))")
    public Object aroundMethod(ProceedingJoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        String args = Arrays.toString(joinPoint.getArgs());
        Object result = null;
        try {
            logger.info("环绕通知-->目标对象方法执行之前");
            //目标对象(连接点)方法的执行
            result = joinPoint.proceed();
            logger.info("环绕通知-->目标对象方法返回值之后");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            logger.info("环绕通知-->目标对象方法出现异常时");
        } finally {
            logger.info("环绕通知-->目标对象方法执行完毕");
        }
        return result;
    }

切面的优先级

@Order:有一个参数,value为int类型,数字越小,优先级越高,默认为2147483647

在未修改原来的LogAspect.java情况下,增加了LogAspect02.java,Order设为2,如下;

@Aspect
@Component
@Order(2)
public class LogAspect02 {}

结果如下:LogAspect02是包裹在LogAspect外侧的,既内外嵌套,如上图所示。

2024-12-09 23:12:53 020 [main] INFO com.sheeprunner.aop.bannotation.LogAspect02 - 环绕通知02-->目标对象方法执行之前
2024-12-09 23:12:53 041 [main] INFO com.sheeprunner.aop.bannotation.LogAspect02 - LogAspect02-前置通知,方法名:add,参数:[2, 5]
2024-12-09 23:12:53 042 [main] INFO com.sheeprunner.aop.bannotation.LogAspect - 环绕通知-->目标对象方法执行之前
2024-12-09 23:12:53 042 [main] INFO com.sheeprunner.aop.bannotation.LogAspect - LogAspect-前置通知,方法名:add,参数:[2, 5]
[inner] a = 2, b = 5, a + b = 7
2024-12-09 23:12:53 042 [main] INFO com.sheeprunner.aop.bannotation.LogAspect - LogAspect-返回通知,方法名:add,结果:7
2024-12-09 23:12:53 043 [main] INFO com.sheeprunner.aop.bannotation.LogAspect - LogAspect-后置通知,方法名:add
2024-12-09 23:12:53 043 [main] INFO com.sheeprunner.aop.bannotation.LogAspect - 环绕通知-->目标对象方法返回值之后
2024-12-09 23:12:53 043 [main] INFO com.sheeprunner.aop.bannotation.LogAspect - 环绕通知-->目标对象方法执行完毕
2024-12-09 23:12:53 043 [main] INFO com.sheeprunner.aop.bannotation.LogAspect02 - LogAspect02-返回通知,方法名:add,结果:7
2024-12-09 23:12:53 045 [main] INFO com.sheeprunner.aop.bannotation.LogAspect02 - LogAspect02-后置通知,方法名:add
2024-12-09 23:12:53 045 [main] INFO com.sheeprunner.aop.bannotation.LogAspect02 - 环绕通知02-->目标对象方法返回值之后
2024-12-09 23:12:53 045 [main] INFO com.sheeprunner.aop.bannotation.LogAspect02 - 环绕通知02-->目标对象方法执行完毕

五、基于XML实现AOP

<!--
        基于XML的AOP实现:
    -->
    <bean id="calculatorCustomImpl" class="com.sheeprunner.aop.cxml.CalculatorCustomImpl" />
    <bean id="logAspect03" class="com.sheeprunner.aop.cxml.LogAspect03" />
    <aop:config>
        <!-- 配置切面类 -->
        <aop:aspect ref="logAspect03">
            <aop:pointcut id="pointCut" expression="execution(* com.sheeprunner.aop.cxml.Calculator.*(..))"/>
            <aop:before method="beforeMethod" pointcut-ref="pointCut" />
            <aop:after method="afterMethod" pointcut-ref="pointCut" />
            <aop:after-returning method="afterReturnMethod" pointcut-ref="pointCut" returning="result" />
            <aop:after-throwing method="afterThrowingMethod" pointcut-ref="pointCut" throwing="e" />
            <aop:around method="aroundMethod" pointcut-ref="pointCut" />
        </aop:aspect>
    </aop:config>
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
4月前
|
前端开发 Java Spring
springbootWeb常用注解使用
本文概述了Spring Boot Web中处理HTTP请求的常用注解,包括`@PathVariable`、`@RequestHeader`、`@RequestParam`、`@RequestBody`、`@ModelAttribute`和`@CookieValue`的用法及其示例。
springbootWeb常用注解使用
|
7天前
|
Java Spring 容器
【SpringFramework】Spring IoC-基于注解的实现
本文主要记录基于Spring注解实现IoC容器和DI相关知识。
40 21
|
2天前
|
SQL Java 关系型数据库
【SpringFramework】Spring事务
本文简述Spring中数据库及事务相关衍伸知识点。
26 9
|
5月前
|
Java 程序员 数据库
SpringBootWeb AOP(一)
SpringBootWeb AOP(一)
51 0
|
5月前
|
Java 数据库 Spring
SpringBootWeb AOP(二)
SpringBootWeb AOP(二)
50 0
|
8月前
|
XML 监控 Java
Spring基础 SpringAOP
Spring基础 SpringAOP
53 0
|
Java Spring
享读SpringMVC源码4-感谢RequestMappingHandlerAdapter(下)
享读SpringMVC源码4-感谢RequestMappingHandlerAdapter(下)
享读SpringMVC源码4-感谢RequestMappingHandlerAdapter(下)
|
缓存 安全 Java
SpringAOP面向切面
SpringAOP面向切面
SpringAOP面向切面
|
Java Spring
Spring的@Autowired依赖注入原来这么多坑!(下)
像第一个案例,同种类型的实现,可能不是同时出现在自己的项目代码中,而是有部分实现出现在依赖的类库。看来研究源码的确能让我们少写几个 bug!
200 0
Spring的@Autowired依赖注入原来这么多坑!(下)
|
Oracle NoSQL Java
Spring的@Autowired依赖注入原来这么多坑!(中)
像第一个案例,同种类型的实现,可能不是同时出现在自己的项目代码中,而是有部分实现出现在依赖的类库。看来研究源码的确能让我们少写几个 bug!
445 0
Spring的@Autowired依赖注入原来这么多坑!(中)