面试官:Spring代理目标bean时为何通过TargetSource类型对目标bean封装?

Posted codingjav

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试官:Spring代理目标bean时为何通过TargetSource类型对目标bean封装?相关的知识,希望对你有一定的参考价值。

在Spring代理目标bean的时候,其并不是直接创建一个目标bean的对象实例的,而是通过一个TargetSource类型的对象将目标bean进行封装,Spring Aop获取目标对象始终是通过TargetSource.getTarget()方法进行的。本文首先会讲解Spring Aop是如何封装目标对象到TargetSource中的,然后会讲解TargetSource各个方法的使用原理,接着会对Spring提供的常见的TargetSource的实现类进行讲解,最后会讲解如何实现自定义的TargetSource

1. 封装TargetSource对象

        我们知道,Spring Aop标签解析的最终结果就是生成了一个AnnotationAwareAspectJAutoProxyCreatorBeanDefinition,我们查看这个类的继承结构可以发现其实现了InstantiationAwareBeanPostProcessorBeanPostProcessor两个接口,并且分别实现了下面两个方法:

public interface InstantiationAwareBeanPostProcessor extends BeanPostProcessor {
    @Nullable
    default Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) 
        throws BeansException {
        return null;
    }
}
public interface BeanPostProcessor {
	@Nullable
	default Object postProcessAfterInitialization(Object bean, String beanName) 
        throws BeansException {
		return bean;
	}
}

       这里省略了其余的不相关方法。上述第一个方法会在Spring实例化一个bean之前执行,如果这里第一个方法能够返回目标bean对象,那么这里就直接使用该对象,Spring不会继续生成目标bean对象,这种方式可以实现自定义的bean对象;

第二个方法会在Spring实例化一个bean之后执行,主要作用是对已经生成的bean进行一定的处理。这里AnnotationAwareAspectJAutoProxyCreator对这两个方法都进行了重写,对于重写的第一个方法,其主要目的在于如果用户使用了自定义的TargetSource对象,则直接使用该对象生成目标对象,而不会使用Spring的默认逻辑生成目标对象,并且这里会判断各个切面逻辑是否可以应用到当前bean上,如果可以,则直接应用,也就是说TargetSource为使用者在Aop中提供了一个自定义生成目标bean逻辑的方式,并且会应用相应的切面逻辑。对于第二个方法,其主要作用在于Spring生成某个bean之后,将相关的切面逻辑应用到该bean上,这个方法在后续将会详细讲解。

Advisor解析

CodingBug,公众号:CodingCodeSpring Aop 之 Advisor 解析

这里主要讲解第一方法的原理,如下是其实现源码:

@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) 
    throws BeansException {
    Object cacheKey = getCacheKey(beanClass, beanName);

    // 判断TargetSource缓存中是否包含当前bean,如果不包含,则判断当前bean是否是已经被代理的bean,
    // 如果代理过,则不对当前传入的bean进行处理,如果没代理过,则判断当前bean是否为系统bean,或者是
    // 切面逻辑不会包含的bean,如果是,则将当前bean缓存到advisedBeans中,否则继续往下执行。
    // 经过这一步的处理之后,只有在TargetSource中没有进行缓存,并且应该被切面逻辑环绕,但是目前还未
    // 生成代理对象的bean才会通过此方法。
    if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) {
        if (this.advisedBeans.containsKey(cacheKey)) {
            return null;
        }
        if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return null;
        }
    }

    // 获取封装当前bean的TargetSource对象,如果不存在,则直接退出当前方法,否则从TargetSource
    // 中获取当前bean对象,并且判断是否需要将切面逻辑应用在当前bean上。
    TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
    if (targetSource != null) {
        if (StringUtils.hasLength(beanName)) {
            this.targetSourcedBeans.add(beanName);
        }
        
        // 获取能够应用当前bean的切面逻辑
        Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, 
           beanName, targetSource);
        // 根据切面逻辑为当前bean生成代理对象
        Object proxy = createProxy(beanClass, beanName, specificInterceptors, 
           targetSource);
        // 对生成的代理对象进行缓存
        this.proxyTypes.put(cacheKey, proxy.getClass());
        // 直接返回生成的代理对象,从而使后续bean的创建工作短路
        return proxy;
    }

    return null;
}

2. TargetSource使用原理

       如下是TargetSource接口的声明:

public interface TargetSource extends TargetClassAware {

    // 本方法主要用于返回目标bean的Class类型
	@Override
	@Nullable
	Class<?> getTargetClass();

    // 这个方法用户返回当前bean是否为静态的,比如常见的单例bean就是静态的,而prototype就是动态的,
    // 这里这个方法的主要作用是,对于静态的bean,spring是会对其进行缓存的,在多次使用TargetSource
    // 获取目标bean对象的时候,其获取的总是同一个对象,通过这种方式提高效率
	boolean isStatic();

    // 获取目标bean对象,这里可以根据业务需要进行自行定制
	@Nullable
	Object getTarget() throws Exception;

    // Spring在完目标bean之后会调用这个方法释放目标bean对象,对于一些需要池化的对象,这个方法是必须
    // 要实现的,这个方法默认不进行任何处理
	void releaseTarget(Object target) throws Exception;
}

3. Spring提供的TargetSource对象

       通过第二节对TargetSource的声明和使用原理讲解,我们可以看到,TargetSource接口的设计几乎为我们使用该接口实现自定义的对象实现了各种可能性:单例,多例,池化对象等等。下面我们看看Spring为我们提供了哪些常见的TargetSource实现类:

3.1 SingletonTargetSource

       SingletonTargetSource,顾名思义,即为单例的TargetSource,其只是对目标bean进行了简单的封装。如下是其实现源码:

public class SingletonTargetSource implements TargetSource, Serializable {
	private static final long serialVersionUID = 9031246629662423738L;
	private final Object target;
    
	public SingletonTargetSource(Object target) {
		Assert.notNull(target, "Target object must not be null");
		this.target = target;
	}

	@Override
	public Class<?> getTargetClass() {
		return this.target.getClass();
	}

	@Override
	public Object getTarget() {
		return this.target;
	}

	@Override
	public void releaseTarget(Object target) {}

	@Override
	public boolean isStatic() {
		return true;
	}
}

       可以看到SingletonTargetSource通过构造方法传入一个目标bean对象,在使用getTarget()方法时,也只是将该对象直接返回;并且这里isStatic()方法返回的是true,也就是说,Spring是可以缓存SingletonTargetSource的。

3.2 PrototypeTargetSource

       与SingletonTargetSource类似,PrototypeTargetSource表示其将生成prototype类型的bean,即其生成的bean并不是单例的,因而使用这个类型的TargetSource时需要注意,封装的目标bean必须是prototype类型的。如下是其实现源码:

public class PrototypeTargetSource extends AbstractPrototypeBasedTargetSource {

	@Override
	public Object getTarget() throws BeansException {
		return newPrototypeInstance();
	}

	@Override
	public void releaseTarget(Object target) {
		destroyPrototypeInstance(target);
	}
}

       可以看到PrototypeTargetSource主要重写了getTarget()releaseTarget()方法,并且委托给newPrototypeInstance()destroyPrototypeInstance()执行。我们这里看看AbstractPrototypeBasedTargetSource的源码:

public abstract class AbstractPrototypeBasedTargetSource 
    extends AbstractBeanFactoryBasedTargetSource {

    // 继承自BeanFactoryAware接口,将当前Spring使用的BeanFactory传进来
	@Override
	public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
		super.setBeanFactory(beanFactory);
		if (!beanFactory.isPrototype(getTargetBeanName())) {
			throw new BeanDefinitionStoreException(
				"Cannot use prototype-based TargetSource 
                   + "against non-prototype bean with name '" 
                   + getTargetBeanName() + "': instances would not be independent");
		}
	}

    // 使用BeanFactory获取目标bean的对象,getTargetBeanName()方法将返回目标bean的名称,
    // 由于目标bean是prototype类型的,因而这里也就可以通过BeanFactory获取prototype类型的bean
    // 这也是PrototypeTargetSource能够生成prototype类型的bean的根本原因
	protected Object newPrototypeInstance() throws BeansException {
		if (logger.isDebugEnabled()) {
			logger.debug("Creating new instance of bean '" + getTargetBeanName() + "'");
		}
		return getBeanFactory().getBean(getTargetBeanName());
	}

    // 如果生成的bean使用完成,则会调用当前方法销毁目标bean,由于目标bean可能实现了DisposableBean
    // 接口,因而这里销毁bean的方式就是调用其实现的该接口的方法,从而销毁目标bean
	protected void destroyPrototypeInstance(Object target) {
		if (this.logger.isDebugEnabled()) {
			this.logger.debug("Destroying instance of bean '" 
               + getTargetBeanName() + "'");
		}
		if (getBeanFactory() instanceof ConfigurableBeanFactory) {
			((ConfigurableBeanFactory) getBeanFactory())
                .destroyBean(getTargetBeanName(), target);
		} else if (target instanceof DisposableBean) {
			try {
				((DisposableBean) target).destroy();
			} catch (Throwable ex) {
				logger.error("Couldn't invoke destroy method of bean with name '" 
                    + getTargetBeanName() + "'", ex);
			}
		}
	}
}

       可以看到,PrototypeTargetSource的生成prototype类型bean的方式主要是委托给BeanFactory进行的,因为BeanFactory自有一套生成prototype类型的bean的逻辑,因而PrototypeTargetSource也就具有生成prototype类型bean的能力,这也就是我们要生成的目标bean必须声明为prototype类型的原因。

3.3 CommonsPool2TargetSource

       这里CommonsPool2TargetSource也就是池化的TargetSource,其基本具有平常所使用的“池”的概念的所有属性,比如:最小空闲数,最大空闲数,最大等待时间等等。实际上,CommonsPool2TargetSource的实现是将其委托给了ObjectPool进行,具体的也就是GenericObjectPool,其实现了ObjectPool接口。如下是CommonsPool2TargetSource的主要实现:

public class CommonsPool2TargetSource extends AbstractPoolingTargetSource implements PooledObjectFactory<Object> {

    // 保存池化对象的池
	@Nullable
	private ObjectPool pool;
    
    public CommonsPool2TargetSource() {
		setMaxSize(GenericObjectPoolConfig.DEFAULT_MAX_TOTAL);
	}

	@Override
	protected final void createPool() {
		logger.debug("Creating Commons object pool");
        // 创建池化对象
		this.pool = createObjectPool();
	}

    // 设置池化对象的基本属性
	protected ObjectPool createObjectPool() {
		GenericObjectPoolConfig config = new GenericObjectPoolConfig();
		config.setMaxTotal(getMaxSize());
		config.setMaxIdle(getMaxIdle());
		config.setMinIdle(getMinIdle());
		config.setMaxWaitMillis(getMaxWait());
		config.setTimeBetweenEvictionRunsMillis(getTimeBetweenEvictionRunsMillis());
		config.setMinEvictableIdleTimeMillis(getMinEvictableIdleTimeMillis());
		config.setBlockWhenExhausted(isBlockWhenExhausted());
		return new GenericObjectPool(this, config);
	}

    // 从池中请求目标对象
	@Override
	public Object getTarget() throws Exception {
		Assert.state(this.pool != null, "No Commons ObjectPool available");
		return this.pool.borrowObject();
	}

    // 将目标对象归还到池中
	@Override
	public void releaseTarget(Object target) throws Exception {
		if (this.pool != null) {
			this.pool.returnObject(target);
		}
	}
}

       可以看到CommonsPool2TargetSource实现是非常简单的,其将主要功能都委托给了对象池进行,这里的对象池实现也比较简单,其主要使用LinkedBlockingDeque,也就是可阻塞的双端队列实现对象池的功能。

3.4 ThreadLocalTargetSource

       ThreadLocalTargetSource也就是和线程绑定的TargetSource,可以理解,其底层实现必然使用的是ThreadLocal。既然使用了ThreadLocal,也就是说我们需要注意两个问题:

  • 目标对象必须声明为prototype类型,因为每个线程都会持有一个不一样的对象;

  • 目标对象必须是无状态的,因为目标对象是和当前线程绑定的,而Spring是使用的线程池处理的请求,因而每个线程可能处理不同的请求,因而为了避免造成问题,目标对象必须是无状态的。

       如下是ThreadLocalTargetSource的源码:

public class ThreadLocalTargetSource extends AbstractPrototypeBasedTargetSource
		implements ThreadLocalTargetSourceStats, DisposableBean {

    // 保存目标对象的ThreadLocal对象
	private final ThreadLocal<Object> targetInThread =
		new NamedThreadLocal<>("Thread-local instance of bean '" 
			+ getTargetBeanName() + "'");

    // 将生成过的目标对象保存起来,以便于后续进行统一销毁
	private final Set<Object> targetSet = new HashSet<>();
	
	// 生成目标对象,这里的生成方式是ThreadLocal很典型的一种使用策略,即首先从ThreadLocal中取,
	// 如果取到了,则直接返回,如果没取到,则使用“消耗“大一些的方式获取,并缓存到ThreadLocal中
	@Override
	public Object getTarget() throws BeansException {
	    // 记录目标对象的获取次数
		++this.invocationCount;
		// 从ThreadLocal中获取
		Object target = this.targetInThread.get();
		if (target == null) {
			if (logger.isDebugEnabled()) {
				logger.debug("No target for prototype '" + getTargetBeanName()
                + "' bound to thread: " + "creating one and binding it to thread '" 
                + Thread.currentThread().getName() + "'");
			}
			// 如果ThreadLocal中不存在,则通过最基本的方式获取目标对象,
			// 并将生成的对象保存到ThreadLocal中
			target = newPrototypeInstance();
			this.targetInThread.set(target);
			// 将生成的对象进行缓存
			synchronized (this.targetSet) {
				this.targetSet.add(target);
			}
		}
		else {
			++this.hitCount;
		}
		return target;
	}

    // 销毁当前TargetSource对象和生成的目标对象
	@Override
	public void destroy() {
		logger.debug("Destroying ThreadLocalTargetSource bindings");
		synchronized (this.targetSet) {
			for (Object target : this.targetSet) {
			    // 销毁生成的目标对象
				destroyPrototypeInstance(target);
			}
			this.targetSet.clear();
		}
		// 清除ThreadLocal中的缓存
		this.targetInThread.remove();
	}
}

       这里ThreadLocalTargetSource主要集成了AbstractPrototypeBasedTargetSourceDisposableBean。关于AbstractPrototypeBasedTargetSource前面已经讲过了,读者可以到前面翻看;而DisposableBean的作用主要是提供一个方法,以供给Spring在销毁当前对象的时候调用。也就是说Spring在销毁当前TargetSource对象的时候会首先销毁其生成的各个目标对象。这里需要注意的是,TargetSource和生成的目标对象是两个对象,前面讲的TargetSouce都是单例的,只是生成的目标对象可能是单例的,也可能是多例的。

4. 实现自定义的TargetSource

       对前面各个TargetSource掌握之后,要实现自定义的TargetSource实际上也非常的简单,假设我们这里要生成两个对象进行访问均衡,此时就可以使用自定义的TargetSource。如下是我们要生成的目标对象的声明:

public class Apple {
  private int id;

  public Apple(int id) {
    this.id = id;
  }

  public void eat() {
    System.out.println("eat apple, id: " + id);
  }
}

       这里Apple对象使用id属性进行当前对象的标识,并在eat()方法中将id打印出来了。如下是自定义TargetSource实现:

public class AppleTargetSource implements TargetSource {
  private Apple apple1;
  private Apple apple2;

  public AppleTargetSource() {
    this.apple1 = new Apple(1);
    this.apple2 = new Apple(2);
  }

  @Override
  public Class<?> getTargetClass() {
    return Apple.class;
  }

  @Override
  public boolean isStatic() {
    return false;
  }

  @Override
  public Object getTarget() throws Exception {
    ThreadLocalRandom random = ThreadLocalRandom.current();
    int index = random.nextInt(2);
    return index % 2 == 0 ? apple1 : apple2;
  }

  @Override
  public void releaseTarget(Object target) throws Exception {}
}

       实现自定义TargetSource主要有两个点要注意,一个是getTarget()方法,该方法中需要实现获取目标对象的逻辑,另一个是isStatic()方法,这个方法告知Spring是否需要缓存目标对象,在非单例的情况下一般是返回false。如下是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" 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/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="targetSource" class="chapter7.eg10.AppleTargetSource"/>
    <aop:aspectj-autoproxy/>
</beans>
public class CustomTargetSourceApp {
  public static void main(String[] args) throws Exception {
    ApplicationContext context = new ClassPathXmlApplicationContext("chapter7/eg10/applicationContext.xml");
    TargetSource targetSource = (TargetSource) context.getBean("targetSource");
    for (int i = 0; i < 10; i++) {
      Apple apple = (Apple) targetSource.getTarget();
      apple.eat();
    }
  }
}

       执行结果如下:

eat apple, id: 1
eat apple, id: 1
eat apple, id: 2
eat apple, id: 1
eat apple, id: 1
eat apple, id: 2
eat apple, id: 1
eat apple, id: 1
eat apple, id: 1
eat apple, id: 1

       从执行结果来看,自定义TargetSource的random特性是实现了,只是这里使用id为1的Apple执行次数要多一些,这主要是由于多线程执行会更倾向于使用当前已经获得锁的线程执行锁定代码。

5. 小结

       本文主要首先讲解了Spring是如果在源码层面支持TargetSource的,然后讲解了TargetSource的使用原理,接着对Spring提供的常见TargetSource进行了讲解,最后使用一个自定义的TargetSource讲解了其使用方式。

6、面试科普小知识

       面试官:为什么SpringAOP代理不直接代理target,而需要通过代理TargetSource(target的来源,其内部持有target),间接代理target呢?

        通常情况下,一个proxy(代理对象)只能代理一个target,每次方法调用的目标也是唯一固定的target。但是,如果让proxy代理TargetSource,可以使得每次方法调用的target实例都不同(当然也可以相同,这取决于TargetSource实现)。这种机制使得方法调用变得灵活,可以扩展出很多高级功能,如:target pool(目标对象池)、hot swap(运行时目标对象热替换),等等。

  TargetSource组件本身与SpringIoC容器无关,换句话说,target的生命周期不一定是受spring容器管理的,我们以往的XML中的AOP配置,只是对受容器管理的bean而言的,我们当然可以手动创建一个target,同时使用Spring的AOP框架(而不使用IoC容器)

最后打个广告,如果你觉得这篇文章对你有文章,可以关注我的技术公众号【CodingCode】。你的关注和转发是对我最大的支持,O(∩_∩)O。

以上是关于面试官:Spring代理目标bean时为何通过TargetSource类型对目标bean封装?的主要内容,如果未能解决你的问题,请参考以下文章

面试官:Spring代理目标bean时为何通过TargetSource类型对目标bean封装?

面试官:Spring代理目标bean时为何通过TargetSource类型对目标bean封装?

面试官:展开说说,Spring中Bean对象是如何通过注解注入的?

面试官:展开说说,Spring中Bean对象是如何通过注解注入的?

Spring aop学习整理(spring in action):spring AOP

面试官:说说 Spring Bean 的实例化过程?面试必问的!