文章目录

  • 一、前言
  • 二、基本使用
  • 1. 作用
  • 三、原理实现
  • 1. 预处理
  • 1.1 AbstractBeanDefinition#prepareMethodOverrides
  • 1.2 AutowiredAnnotationBeanPostProcessor#determineCandidateConstructors
  • 2. 真正处理


一、前言

本文是 Spring源码分析:单例bean的获取 - createBean 的衍生文章。主要是因为本人菜鸡,在分析源码的过程中还有一些其他的内容不理解,故开设衍生篇来完善内容以学习。

二、基本使用

1. 作用

  • lookup-method :用于注入方法返回结果,也就是说能通过配置方式替换方法返回结果。(在方法或者抽象方法上使用@Lookup注解,将会根据该方法的返回值,自动在BeanFactory中调用getBean()来注入该Bean)
  • replaced-method :可以实现方法主体或返回结果的替换
    通俗来讲 : lookup-method 可以注入属性bean, replaced-method 替换方法实现。

下面跟着一个Demo来理解

// 基类接口
public interface DemoBase {
    String hello();
}

...
public class DemoA implements DemoBase {
    public DemoBase getDemoBase() {
        return new DemoB();
    }

    @Override
    public String hello() {
        return "demoA hello";
    }
}
...
public class DemoB implements DemoBase {
    @Override
    public String hello() {
        return "DemoB hello";
    }
}
...
public class DemoC implements DemoBase{
    @Override
    public String hello() {
        return "DemoC hello";
    }
}
....

public class DemoMethodReplacer implements MethodReplacer {
    @Override
    public Object reimplement(Object obj, Method method, Object[] args) throws Throwable {
        System.out.println("method = " + method);
        return "reimplement";
    }
}

我们来配置一下bean.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="demoA"  name="demoA"  class="com.kingfish.springbootdemo.property.DemoA">
    	// 指定 DemoA.getDemoBase 方法,返回值是 容器中的demoC
        <lookup-method name="getDemoBase" bean="demoC"></lookup-method>
        // 指定 DemoA.hello方法,其方法经过 demoMethodReplacer.reimplement 代理
        <replaced-method name="hello" replacer="demoMethodReplacer"></replaced-method>
    </bean>
    <bean id="demoB" name="demoB" class="com.kingfish.springbootdemo.property.DemoB"></bean>
    <bean id="demoC" name="demoC" class="com.kingfish.springbootdemo.property.DemoC"></bean>
    <bean id="demoMethodReplacer" name="demoMethodReplacer" class="com.kingfish.springbootdemo.property.DemoMethodReplacer"></bean>
</beans>
@SpringBootApplication
@ImportResource("bean.xml")
public class SpringbootDemoApplication implements ApplicationRunner {

    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(SpringbootDemoApplication.class, args);
        DemoA demoA = run.getBean(DemoA.class);
        System.out.println(demoA.hello());
        System.out.println(demoA.getDemoBase());

}

运行后输出结果如下

spring ldaptemplate search方法详解 spring lookup method_xml

根据上面的demo。我们可以看出

  • lookup-method : name 指定方法名,bean 指定方法返回的bean。这里注意上面的demo中getDemoBase方法没有必要实现,可以直接写一个抽象方法。即当调用 demoA.getDemoBase 方法时返回的是 Spring 容器中name为demoC 的bean。这里可以通过将demoC 设置成原型模式来实现单例模式下原型bean的获取。
  • replaced-mthod : name 指定方法名,replacer 指定代理类。即当调用demoA中的hello方法时,会经过demoMethodReplacer的代理。demoMethodReplacer需要实现MethodReplacer 接口,调用方法时会经过MethodReplacer.reimplement 方法。

三、原理实现

首先需要知道,lookup-method 和 replaced-method 注解标注的方法都会被保存在BeanDefinition.methodOverrides 集合中。这两个功能的实现原理其实也是在bean实例化的时候如果检测到 methodOverrides 属性存在,则会动态的为当前bean生成代理并使用对应的拦截器为bean做增强处理。

spring ldaptemplate search方法详解 spring lookup method_ide_02

Spring 对 这两个属性的处理 在两个部分

1. 预处理

1.1 AbstractBeanDefinition#prepareMethodOverrides

这个阶段的时候bean还没开始创建,先做了一个预先的工作,减少后面的工作量。

这个方法做了一些预处理的工作

详细代码如下:

public void prepareMethodOverrides() throws BeanDefinitionValidationException {
		// Check that lookup methods exist and determine their overloaded status.
		// 判断是否有方法需要重写
		if (hasMethodOverrides()) {
			getMethodOverrides().getOverrides().forEach(this::prepareMethodOverride);
		}
	}

	protected void prepareMethodOverride(MethodOverride mo) throws BeanDefinitionValidationException {
		// 获取对应的类中的对应方法名的个数
		int count = ClassUtils.getMethodCountForName(getBeanClass(), mo.getMethodName());
		// 等于0抛出异常
		if (count == 0) {
			throw new BeanDefinitionValidationException(
					"Invalid method override: no method with name '" + mo.getMethodName() +
					"' on class [" + getBeanClassName() + "]");
		}
		else if (count == 1) {
			// Mark override as not overloaded, to avoid the overhead of arg type checking.
			// 标记 MethodOverride 暂未被覆盖,避免参数类型检查的开销。
			mo.setOverloaded(false);
		}
	}

解释一下上面的逻辑:

  1. 首先会判断是否有方法需要重写,这里的是根据 RootBeanDefinition 中的 methodOverrides 属性来进行判断,为空则表示没有。
  2. 若上述判断有方法需要覆盖,则会调用prepareMethodOverride(MethodOverride mo) 方法。而在 prepareMethodOverride(MethodOverride mo) 方法中会根据 需要覆盖的方法名称 来获取加载类中关于该方法的实现。如果获取不到 count == 0,则直接抛出异常,如果获取到只有一个 count == 1,则记录该方法并未被重载(因为Spring在方法匹配时,如果一个类中存在若干个重载方法,则在函数调用及增强的时候需要根据参数类型进行匹配,来最终确定调用的方法是哪一个,这里直接设置了该方法并未被重载,在后续方法匹配的时候就不需要进行参数匹配验证,直接调用即可)。
  3. 打个比方,比如指定覆盖A类中的 a方法,但是A类中可能存在多个a方法或者不存在a方法,若count == 0不 存在a方法,则谈何覆盖,直接抛出异常,若count ==1 则a方法的实现只有一个,标记该方法并未被重载后续可跳过参数验证的步骤。

1.2 AutowiredAnnotationBeanPostProcessor#determineCandidateConstructors

在 这里对 @Lookup 注解 进行了一个简单处理,将被 @Lookup 注解修饰的方法添加到RootBeanDefinition 中的 methodOverrides 属性中

public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, final String beanName)
			throws BeanCreationException {

		// Let's check for lookup methods here...
		if (!this.lookupMethodsChecked.contains(beanName)) {
			if (AnnotationUtils.isCandidateClass(beanClass, Lookup.class)) {
				try {
					Class<?> targetClass = beanClass;
					do {
						ReflectionUtils.doWithLocalMethods(targetClass, method -> {
							Lookup lookup = method.getAnnotation(Lookup.class);
							if (lookup != null) {
								Assert.state(this.beanFactory != null, "No BeanFactory available");
								LookupOverride override = new LookupOverride(method, lookup.value());
								try {
									RootBeanDefinition mbd = (RootBeanDefinition)
											this.beanFactory.getMergedBeanDefinition(beanName);
									mbd.getMethodOverrides().addOverride(override);
								}
								catch (NoSuchBeanDefinitionException ex) {
									throw new BeanCreationException(beanName,
											"Cannot apply @Lookup to beans without corresponding bean definition");
								}
							}
						});
						targetClass = targetClass.getSuperclass();
					}
					while (targetClass != null && targetClass != Object.class);

				}
				catch (IllegalStateException ex) {
					throw new BeanCreationException(beanName, "Lookup method resolution failed", ex);
				}
			}
			this.lookupMethodsChecked.add(beanName);
		}
		
	...

}

2. 真正处理

这个阶段bean处于正在创建的过程中,这里是对其他配置的处理,lookup-method,replaced-method就是其中之一。

SimpleInstantiationStrategy#instantiate(RootBeanDefinition, String, BeanFactory)

public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {
		// Don't override the class with CGLIB if no overrides.
		// 判断是否有使用lookup-method,replaced-method 来标注方法。如果没有直接反射即可。
		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 {
			// Must generate CGLIB subclass.
			return instantiateWithMethodInjection(bd, beanName, owner);
		}
	}

这里简单将一下逻辑,不再详细分析代码。
首先判断当前bean是否有使用 lookup-method,replaced-method 属性,若没有使用,则直接使用反射即可。若使用,则不能简单的使用反射,需要将这两个配置提供的功能切入进去,所以就必须使用动态代理的方式将两个特性所对应的逻辑的拦截增强器设置进去。这样就可以保证在调用方法的时候会被相应的拦截器增强,返回值为包含拦截器的代理实例。