错误姿势: Spring注入prototype bean固定成单例!

前言

最近看到个面试题: Spring中注入的bean,如何每次获取新的实例?

相信稍微了解点Spring的小伙伴都能够马上回答出: spring bean默认scopesingleton,将其修改为prototype就可以了!

但事实真的是这样吗?我们来看下面这个案例~


案例

普通案例

java 复制代码
public interface UserService {
}

@Service
public class UserServiceImpl implements UserService {
}

public interface OrderService {

	/**
	 * 测试 bean
	 */
	void testBean();

}

@Service
public class OrderServiceImpl implements OrderService {

	@Autowired
	private UserService userService;

	@Override
	public void testBean() {
		System.out.println(userService);
		System.out.println(userService.hashCode());
	}

}

再写个测试类

java 复制代码
@SpringBootTest
public class TestBean {

   @Autowired
   private OrderService orderService;

   @Test
   public void testBean() {
      for (int i = 0; i < 5; i++) {
         orderService.testBean();
      }
   }

}

运行并查看执行结果

我们可以发现,打印五次的userService bean的地址和hashCode都是一样的。

也就是说,我们在5次中获取的注入的userService bean都是同一个对象

这个是很符合预期的,因为spring bean默认的scope就是singleton ,接下来我们将userService beanscope修改为prototype再看看情况~


使用prototype案例

spring中,我们如果想要修改beanscope,可以使用@Scope注解,将value设置为你想要的scope即可~

java 复制代码
@Scope(value = "prototype")
@Service
public class UserServiceImpl implements UserService {
}

如上,我已经们将userService beanscope修改为prototype,接下来再运行下测试类查看执行结果~

通过执行结果,我们可以惊奇的发现,即使我们已经将beanscope修改为了prototype,但是我们仍然还是获取的同一个对象~

这时候不要着急,让我们回顾下案例代码~

虽然我们已经将userService beanscope修改为了prototype,但是我们本身是在OrderService中去获取的userService,

OrderService bean仍然是singleton,其只会被创建一次,创建时注入的userService bean已经固定

嘿嘿,这里就呼应文章标题了!Spring注入prototype bean被固定


破局

通过以上两个案例,现状我们已经了解了,那么该怎么破局呢?

两个方法

  1. 提供beanget方法,get时,利用ApplicationContext重新从容器中获取
  2. 提供beanget方法,再利用@Lookup注解

方法一:ApplicationContext

第一步: 先简单封装一个ApplicationContextUtil工具类,方便从Spring容器中获取bean

java 复制代码
@Component
public class ApplicationContextUtil implements ApplicationContextAware {

   private static ApplicationContext applicationContext;

   @Override
   public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
      ApplicationContextUtil.applicationContext = applicationContext;
   }

   public static <T> T getBeanByClass(Class<T> targetClass) {
      return applicationContext.getBean(targetClass);
   }

}

第二步: 提供UserServiceget方法,在get方法中利用封装好的ApplicationContextUtil重新从容器中获取UserService bean(前提还是要把scope设置为prototype

java 复制代码
@Service
public class OrderServiceImpl implements OrderService {

   @Autowired
   private UserService userService;

   public UserService getUserService() {
			return ApplicationContextUtil.getBeanByClass(userService.getClass());
   }

   @Override
   public void testBean() {
      System.out.println(getUserService());
      System.out.println(getUserService().hashCode());
   }

}

第三步: 做完前两步,可再运行测试类查看执行结果~

此时我们通过执行结果就可发现,每次获取到的userService bean是不同的对象~


原理

跟踪代码流程太长了,我就直接贴出核心代码了,在ApplicationContext getBean时,会检查beanscope

结合下面源码我们可见

  1. 如果是singleton,那么会先从容器里获取,获取不到再createBean
  2. prptotype则是直接createBean

这样一来,在外层,我们每次获取到的就是新的一个bean了,所以地址和hashCode都不同

java 复制代码
// org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean
protected <T> T doGetBean(
  String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
  throws BeansException {
  
  // ..... 

  // Create bean instance.
  if (mbd.isSingleton()) {
    // todo scope为singleton
    
    // 先从容器中获取单例bean,拿不到再createBean
    sharedInstance = getSingleton(beanName, () -> {
      try {
        return createBean(beanName, mbd, args);
      }
      catch (BeansException ex) {
        // Explicitly remove instance from singleton cache: It might have been put there
        // eagerly by the creation process, to allow for circular reference resolution.
        // Also remove any beans that received a temporary reference to the bean.
        destroySingleton(beanName);
        throw ex;
      }
    });
    beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
  }

  else if (mbd.isPrototype()) {
    // todo scope为prototype
    Object prototypeInstance = null;
    try {
      beforePrototypeCreation(beanName);
      // todo 直接createBean
      prototypeInstance = createBean(beanName, mbd, args);
    }
    finally {
      afterPrototypeCreation(beanName);
    }
    beanInstance = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
  }

  // ..... 

}

方法二:@Lookup注解

我们仍然需要提供UserService beanget方法,并在get方法上加上@Lookup即可~

java 复制代码
@Service
public class OrderServiceImpl implements OrderService {

   @Autowired
   private UserService userService;

   @Lookup
   public UserService getUserService() {
      return userService;
   }

   @Override
   public void testBean() {
      System.out.println(getUserService());
      System.out.println(getUserService().hashCode());
   }

}

再次运行测试类查看执行结果~

结果符合预期,每次拿到的都是不同的userService bean~


原理

实例化bean时,如果bean存在lookup-method replaced-method,则通过cglib生成代理类

java 复制代码
// SimpleInstantiationStrategy#instantiate
@Override
public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {
   // Don't override the class with CGLIB if no overrides.
   if (!bd.hasMethodOverrides()) {
      Constructor<?> constructorToUse;
      synchronized (bd.constructorArgumentLock) {
         constructorToUse = (Constructor<?>) bd.resolvedConstructorOrFactoryMethod;
         if (constructorToUse == null) {
            final Class<?> clazz = bd.getBeanClass();
            if (clazz.isInterface()) {
               throw new BeanInstantiationException(clazz, "Specified class is an interface");
            }
            try {
               if (System.getSecurityManager() != null) {
                  constructorToUse = AccessController.doPrivileged(
                        (PrivilegedExceptionAction<Constructor<?>>) clazz::getDeclaredConstructor);
               }
               else {
                  constructorToUse = clazz.getDeclaredConstructor();
               }
               bd.resolvedConstructorOrFactoryMethod = constructorToUse;
            }
            catch (Throwable ex) {
               throw new BeanInstantiationException(clazz, "No default constructor found", ex);
            }
         }
      }
      return BeanUtils.instantiateClass(constructorToUse);
   }
   else {
      // todo 如果存在 lookup-method 和 replaced-method,则通过cglib生成代理类
      return instantiateWithMethodInjection(bd, beanName, owner);
   }
}
java 复制代码
// CglibSubclassingInstantiationStrategy#instantiateWithMethodInjection
@Override
protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {
   return instantiateWithMethodInjection(bd, beanName, owner, null);
}

@Override
protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner,
      @Nullable Constructor<?> ctor, Object... args) {

   // Must generate CGLIB subclass...
   return new CglibSubclassCreator(bd, owner).instantiate(ctor, args);
}

生成代理类后,会设置方法拦截器,拦截lookup-method replaced-method

java 复制代码
public Object instantiate(@Nullable Constructor<?> ctor, Object... args) {
   Class<?> subclass = createEnhancedSubclass(this.beanDefinition);
   Object instance;
   if (ctor == null) {
      instance = BeanUtils.instantiateClass(subclass);
   }
   else {
      try {
         Constructor<?> enhancedSubclassConstructor = subclass.getConstructor(ctor.getParameterTypes());
         instance = enhancedSubclassConstructor.newInstance(args);
      }
      catch (Exception ex) {
         throw new BeanInstantiationException(this.beanDefinition.getBeanClass(),
               "Failed to invoke constructor for CGLIB enhanced subclass [" + subclass.getName() + "]", ex);
      }
   }
   
   Factory factory = (Factory) instance;
   
   // todo 设置拦截器
   factory.setCallbacks(new Callback[] {NoOp.INSTANCE,
         new LookupOverrideMethodInterceptor(this.beanDefinition, this.owner),
         new ReplaceOverrideMethodInterceptor(this.beanDefinition, this.owner)});
   return instance;
}

LookupOverrideMethodInterceptor: 拦截被@LookUp修饰的方法

java 复制代码
private static class LookupOverrideMethodInterceptor extends CglibIdentitySupport implements MethodInterceptor {

   private final BeanFactory owner;

   public LookupOverrideMethodInterceptor(RootBeanDefinition beanDefinition, BeanFactory owner) {
      super(beanDefinition);
      this.owner = owner;
   }

   @Override
   public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable {
      // Cast is safe, as CallbackFilter filters are used selectively.
      LookupOverride lo = (LookupOverride) getBeanDefinition().getMethodOverrides().getOverride(method);
      Assert.state(lo != null, "LookupOverride not found");
      Object[] argsToUse = (args.length > 0 ? args : null);  // if no-arg, don't insist on args at all
      if (StringUtils.hasText(lo.getBeanName())) {
         Object bean = (argsToUse != null ? this.owner.getBean(lo.getBeanName(), argsToUse) :
               // todo 重新从spring容器里获取bean
               this.owner.getBean(lo.getBeanName()));
         // Detect package-protected NullBean instance through equals(null) check
         return (bean.equals(null) ? null : bean);
      }
      else {
         // Find target bean matching the (potentially generic) method return type
         ResolvableType genericReturnType = ResolvableType.forMethodReturnType(method);
         return (argsToUse != null ? this.owner.getBeanProvider(genericReturnType).getObject(argsToUse) :
               this.owner.getBeanProvider(genericReturnType).getObject());
      }
   }
}

可见,@Lookup最终本质上也是通过重新从容器中获取bean,如果beanprototype则重新创建并返回。


总结

之所以注入的prototype bean会被固定,是因为其所属的bean属于singleton,只会实例化一次,所以prototype bean只会被注入一次,是同一个对象。

破局的方法也很简单,提供prototype beanget方法,每次从容器中重新获取即可,容器检测到beanscopeprototype时,会重新创建一个bean

我是 Code皮皮虾 ,会在以后的日子里跟大家一起学习,一起进步! 觉得文章不错的话,可以在 掘金 关注我,这样就不会错过很多技术干货啦~

相关推荐
救救孩子把3 分钟前
Java基础之IO流
java·开发语言
小菜yh4 分钟前
关于Redis
java·数据库·spring boot·redis·spring·缓存
宇卿.10 分钟前
Java键盘输入语句
java·开发语言
浅念同学11 分钟前
算法.图论-并查集上
java·算法·图论
希冀12311 分钟前
【操作系统】1.2操作系统的发展与分类
后端
立志成为coding大牛的菜鸟.24 分钟前
力扣1143-最长公共子序列(Java详细题解)
java·算法·leetcode
鱼跃鹰飞24 分钟前
Leetcode面试经典150题-130.被围绕的区域
java·算法·leetcode·面试·职场和发展·深度优先
GoppViper41 分钟前
golang学习笔记29——golang 中如何将 GitHub 最新提交的版本设置为 v1.0.0
笔记·git·后端·学习·golang·github·源代码管理
爱上语文2 小时前
Springboot的三层架构
java·开发语言·spring boot·后端·spring
荆州克莱2 小时前
springcloud整合nacos、sentinal、springcloud-gateway,springboot security、oauth2总结
spring boot·spring·spring cloud·css3·技术