@xmlelement 注解在属性 方法上的区别_Spring源码分析(二)依赖注入及方法注入...

本文深入探讨了Spring的依赖注入,包括构造函数注入和setter方法注入,并通过示例解释了@Autowired注解在字段与setter方法上的差异。接着讨论了方法注入,解决了在依赖注入下原型bean无法体现其原型意义的问题。最后总结了依赖注入和方法注入的适用场景,强调了Spring通过属性和方法注入全面管理bean协作关系。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上篇文章讲了官网中的 1.2,1.3 两小节,主要是涉及了容器,以及 Spring 实例化对象的一些知识。这篇文章继续 Spring 官网,主要是针对 1.4 小节,主要涉及到 Spring 的依赖注入。虽然只有一节,但是涉及的东西确不少。

依赖注入

根据官网的介绍,依赖注入主要分为两种方式:

  1. 构造函数注入
  2. setter方法注入

ab0b4a5dff867f03a625bd25a7dd9a2f.png

我们分别对以上两种方式进行测试,官网上用的XML的方式,我这里就采用注解的方式了

测试代码如下,我们通过Service中注入OrderService这个过程来

public class Test {

   public static void main(String[] args) {
      AnnotationConfigApplicationContext ac = new
            // config类主要完成对类的扫描
            AnnotationConfigApplicationContext(Appconfig.class);
      Service service = (Service) ac.getBean("service");
      service.test();
   }
}

@Component
public class OrderService {
   OrderService() {
      System.out.println("order create ");
   }
}

@Configuration
@ComponentScan("com.luban.service")
public class Appconfig {
   @Bean
   public Service service() {
      return new Service();
   }
}

@Configuration
@ComponentScan("com.luban.service")
public class Appconfig {

}

测试setter方法注入

@Component
public class Service {
   private OrderService orderService;

   public Service() {
      System.out.println("service create");
   }

   public void test(){
      System.out.println(orderService);
   }

   //通过autowired指定使用set方法完成注入
   @Autowired
   public void setOrderService(OrderService orderService) {
      System.out.println("注入orderService by setter");
      this.orderService = orderService;
   }
}

输入如下:

order create
service create
注入orderService by setter //验证了确实是通过setter注入的
com.luban.service.OrderService@5a01ccaa

测试构造函数注入:

@Component
public class Service {
   private OrderService orderService;

   public Service() {
      System.out.println("service create by no args constructor");
   }

   // 通过Autowired指定使用这个构造函数,否则默认会使用无参
   @Autowired
   public Service(OrderService orderService) {
      System.out.println("注入orderService by constructor with arg");
      this.orderService = orderService;
      System.out.println("service create by constructor with arg");
   }

   public void test(){
      System.out.println(orderService);
   }
}

输出如下:

order create
注入orderService by constructor with arg
service create by constructor with arg
com.luban.service.OrderService@61443d8f

疑问:

  1. @Autowired直接加在字段上和加在set方法上有什么区别?为什么问验证的时候需要将其添加到setter方法上?
  • 首先我们明确一点,直接添加@Autowired注解在字段上,不需要提供Setter方法也能完成注入。以上面的例子来说,Spring会通过反射获取到Service中orderService这个字段,然后通过反射包的方法,Filed.set(Service,orderService)这种方式来完成注入
  • 我们将@Autowired添加到setter方法时,我们可以通过断点看一下方法的调用栈,如下:

3c65c572bd70520524725eb9d67ab4e3.png

对于这种方式来说,最终是通过Method.invoke(object, args)的方式来完成注入的,这里的method对象就是我们的setter方法

  1. @Autowiredweis为什么加在构造函数上可以指定使用这个构造函数?
  • 我们先测试下,如果我们在加上这个注解会怎么样。我把前文中的@Autowired注解注释掉,然后运行发现:
order create 
service create by no args constructor// 可以看到执行的是空参构造
null

先不着急下结论,我们再把两个函数都添加@Autowired注解测试

  @Autowired
  public Service() {
      System.out.println("service create by no args constructor");
   }


   // 通过Autowired指定使用这个构造函数,否则默认会使用无参
   @Autowired
   public Service(OrderService orderService) {
      System.out.println("注入orderService by constructor with arg");
      this.orderService = orderService;
      System.out.println("service create by constructor with arg");
   }

结果报错:

077527a2bad25610acf871a31a2c172a.png

报错的大概意思是已经找到了一个被@Autowired注解标记的构造函数,同时这个注解中的required属性为true。后来我测试了将其中一个注解中的required属性该为false,发现还是报同样的错,最终将两个注解中的属性都改为false测试才通过,并且测试结果和上面的一样,都是执行的无参构造。

要说清楚这一点,涉及到两个知识

  • Spring中的注入模型,下篇文章再讲
  • Spring对构造函数的推断。我们现在只需要记住:在默认的注入模型下,Spring如果同时找到两个符合要求的构造函数,那么Spring会采用默认无参构造进行实例化,如果这个时候没有无参构造,会报错。java.lang.NoSuchMethodException。什么叫符合要求的构造函数呢,就是构造函数中的参数Spring会找到,参数被Spring所管理。这里需要着重记住两点:一,默认注入模型;二,符合要求的构造函数
  1. 如果我们同时采用构造注入加属性注入会发生什么呢?

在没有进行测试前,我们可以大胆猜测下,Spring虽然能在构造函数里完成属性注入,但是这属于实例化对象阶段做的事情,那么在后面真正进行属性注入的时候,肯定会被覆盖掉。现在我们来验证我们的结论:

@Component
public class Service {

   private OrderService orderService;

   @Autowired(required = false)
   public Service() {
      System.out.println("service create by no args constructor");
   }

   public Service(OrderService orderService) {
      System.out.println("注入orderService by constructor with arg");
      this.orderService = orderService;
      System.out.println("service create by constructor with arg");
   }

   public void test(){
      System.out.println(orderService);
   }

   @Autowired
   public void setOrderService(OrderService orderService) {
      System.out.println("注入luBanService by setter");
      this.orderService = null;
   }
}

运行结果:

注入orderService by constructor with arg//实例化时进行了一次注入
service create by constructor with arg//完成了实例化
注入orderService by setter//属性注入时将实例化注入的属性进行了覆盖
null

区别:

86023c7c976c5b1111f983451f0f59ee.png

根据官网所说,我们可以得出如下结论:

  1. 构造函数注入和setter注入可以混用
  • 对于一些强制的依赖,我们最好使用构造函数注入,对于一些可选依赖,我们可以采用setter方法注入
  1. Spring团队推荐使用构造函数的方式完成注入,但是对于一些参数过长的构造函数,Spring不推荐

方法注入

我们不完全按照官网顺序进行学习,先看这一小节,对于官网上的位置入下图:

efdf10ede9bd74202989bd4e490cd3e9.png

为什么需要方法注入:

首先我们思考一个问题,在有了依赖注入的情况下,为什么还需要方法注入这种方式呢?换而言之,方法注入解决了什么问题?

我们来看下面的测试代码:

public class Test {
   public static void main(String[] args) {
      AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Appconfig.class);
      MyService service = (MyService) ac.getBean("myService");
      service.test(1);
      service.test(2);
      service.test(3);
   }
}

@Configuration
@ComponentScan("com.luban.service")
public class Appconfig {
}


@Component
public class MyService {
   @Autowired
   private OrderService orderService;

   public void test(int a){
      orderService.addAndPrint(a);
   }
}


@Component
// 原型对象
@Scope("prototype")
public class OrderService {
   int i;

   OrderService() {
      System.out.println("order create ");
   }

   // 每次将当前对象的属性i+a然后打印
   public void addAndPrint(int a) {
      i += a;
      System.out.println(i);
   }
}

在上面的代码中,我们有两个bean,MyService为单例的Bean,OrderService为原型的bean,我们的本意可能是希望每次都能获取到不同的OrderService

预期的结果应该打印出:1,2,3

实例输出:1,3,6

这个结果说明我们每次调用到的OrderService是同一个对象。当然这也很好理解,因为在依赖注入阶段我们就完成了OrderService的注入,之后我们在调用测试方法时,不会再去进行注入,所以我们使用的一直是同一个对象

我们可以这么说,原型对象在这种情况下,失去了原型的意义,因为每次都使用的是同一个对象。那么如何解决这个问题呢,只要我每次在使用这个Bean的时候都去重新获取就可以了,这个时候我们可以通过方法注入来解决

通过注入上下文(applicationContext对象)

又分为以下两种方式:

  • 实现org.springframework.context.ApplicationContextAware接口
@Component
public class MyService implements ApplicationContextAware {

   private ApplicationContext applicationContext;

   public void test(int a){
      OrderService orderService = ((OrderService) applicationContext.getBean("orderService"));
      orderService.addAndPrint(a);
   }

   @Override
   public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
      this.applicationContext = applicationContext;
   }
}
  • 直接注入上下文
@Component
public class MyService {

   @Autowired
   private ApplicationContext applicationContext;

   public void test(int a) {
      OrderService orderService = ((OrderService) applicationContext.getBean("orderService"));
      orderService.addAndPrint(a);
   }
}

通过@LookUp的方式(也分为注解和XML两种方式,这里只演示注解的)

@Component
public class MyService{
   public void test(int a) {
      OrderService orderService = lookUp();
      orderService.addAndPrint(a);
   }
   // 
   @Lookup
   public OrderService lookUp(){
      return null;
   }
}

输出结果:

order create 
1
order create
2
order create
3

方法注入之 replace-method

方法注入还有一种方式,即通过replace-method,没有找到对应的注解,这里我们也就用XML的方式测试一下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
   <bean id="myService" class="com.luban.service.MyService">
      <replaced-method replacer="replacer" name="test"/>
   </bean>


   <bean id="replacer" class="com.luban.service.MyReplacer"/>
</beans>
public class MyReplacer implements MethodReplacer {

   @Override
   public Object reimplement(Object obj, Method method, Object[] args) throws Throwable {
      System.out.println("替代"+obj+"中的方法,方法名称:"+method.getName());
      System.out.println("执行新方法中的逻辑");
      return null;
   }
}

@Component
public class MyService{
   public void test(int a) {
      System.out.println(a);
   }
}

public class Test {

   public static void main(String[] args) {
      ClassPathXmlApplicationContext cc =
            new ClassPathXmlApplicationContext("application.xml");
      MyService myService = ((MyService) cc.getBean("myService"));
      myService.test(1);
   }
}
   

执行结果:

替代com.luban.service.MyService$$EnhancerBySpringCGLIB$$32161872@4141d797中的方法,方法名称:test
执行新方法中的逻辑

这里需要注意一点:

我在测试replace-method这种方法注入的方式时,受动态代理的影响,一直想将执行我们被替代的方法,用代码体系如下:

public class MyReplacer implements MethodReplacer {
   @Override
   public Object reimplement(Object obj, Method method, Object[] args) throws Throwable {
//    System.out.println("替代"+obj+"中的方法,方法名称:"+method.getName());
//    System.out.println("执行新方法中的逻辑");
      method.invoke(obj,args);
      return null;
   }
}

但是这段代码是无法执行的,会报内存溢出。因为obj是我们的代理对象,method.invoke(obj,args)执行时会进入方法调用的死循环。最终我也没有找到一种何时的方式来执行被替代的方法。目前看来也可能是Spring的设计,所以我们使用replace-method的场景应该是想完全替代某种方法的执行逻辑,而不是像AOP那样更多的用于在方法的执行前后灯时机完成某些逻辑。

依赖注入和方法注入的总结:

  • 首先我们要明确一点,什么是依赖(Dependencies)?来看官网的一段话

a3aac0adda6b5d92f32d3436fe4adc92.png

可以说,一个对象的依赖就是它自身的属性,Spring中的依赖注入就是属性注入

  • 我们知道一个对象由两部分组成:属性+行为,可以说Spring通过属性注入+方法注入的方式掌控了整个bean。
  • 属性注入和方法注入都是Spring提供给我们用来处理bea之间协作关系的手段。
  • 属性注入有两种方式:构造函数,setter方法。
  • 方法注入(LookUp Method和Replace Method)需要依赖动态代理完成
  • 方法注入对属性注入进行了一定程序的补充,因为属性注入的情况下,原型对象可能失去原型的意义。

画图如下:

63cd94c6472caf6af4ab2250ab806718.png

不吃竹子的滚滚:Spring源码分析(十四)Spring中的BeanWrapper及类型转换

不吃竹子的滚滚:Spring源码分析(十二)ApplicationContext详解(中)

不吃竹子的滚滚:Spring源码分析(十一)ApplicationContext详细介绍(上)

不吃竹子的滚滚:Spring源码分析(十)Spring中Bean的生命周期(下)

不吃竹子的滚滚:Spring源码分析(九)Spring中Bean的生命周期(上)

不吃竹子的滚滚:Spring源码分析(八)容器的扩展点(BeanPostProcessor)

不吃竹子的滚滚:Spring源码分析(七)容器的扩展点(FactoryBean)

不吃竹子的滚滚:Spring源码分析(六)容器的扩展点(BeanFactoryPostProcessor)

不吃竹子的滚滚:Spring源码分析(五)BeanDefinition(下)

不吃竹子的滚滚:Spring源码分析(四)BeanDefinition(上)

不吃竹子的滚滚:Spring源码分析(三)自动注入与精确注入

不吃竹子的滚滚:Spring源码分析(二)依赖注入及方法注入

不吃竹子的滚滚:Spring源码分析(一)Spring容器及Spring Bean

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值