@月黑风高食肉虎 噗噗虎的技术博客

Spring元编程

最近读了一些Spring关于注解的代码,提了两个比较有趣的问题:Q1: 在Spring里面@Controller, @Service, @Repository等与@Component是等价的,如何实现的?Q2: @AliasFor如何实现的?我们来聊聊这些问题。

Q1: 在Spring里面@Controller, @Service, @Repository等与@Component是等价的,如何实现的?

实现的原理很简单,就是注解的注解。

如果查看@Controller@Service@Repository等注解的源码,能发现在他们至上都有@Component注解。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
	// ...
}

org.springframework.stereotype.Service#L47

如果查看AnnotationUtils类中的getAnnotation方法,可以发现其实算法非常简单(5.0版本开始已经重写了这个方法,但原理应该是一样的):

public static <A extends Annotation> A getAnnotation(AnnotatedElement annotatedElement, Class<A> annotationType) {
	try {
		// 获取类型上的注解
		A annotation = annotatedElement.getAnnotation(annotationType);
		// 如果没有找到的话
		if (annotation == null) {
			// 从类型所有的注解上
			for (Annotation metaAnn : annotatedElement.getAnnotations()) {
				// 找注解的注解
				annotation = metaAnn.annotationType().getAnnotation(annotationType);
				if (annotation != null) {
					break;
				}
			}
		}
		return synthesizeAnnotation(annotation, annotatedElement);
	}
	catch (Throwable ex) {
		handleIntrospectionFailure(annotatedElement, ex);
		return null;
	}
}

org.springframework.core.annotation.AnnotationUtils#L180

所以我们是不是可以这样理解,@Service注解“扩展”自@Component注解,是@Component注解的“子类”注解。

但是我要说的是,当Spring容器去扫描Bean的时候,算法比上面的更复杂一点。

Spring去scan component是由ClassPathBeanDefinitionScanner实现的: org.springframework.context.annotation.ClassPathBeanDefinitionScanner

而对各对象进行过滤则是有AnnotationTypeFilter负责的: org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#L184

更深入阅读源码会发现这个复杂的算法核心是AnnotatedElementUtils中的searchWithGetSemantics方法实现的,原理还是一样只是这个方法是个稍复杂的递归: org.springframework.core.annotation.AnnotatedElementUtils#923

另外,你会发现这个方法传入的不是Annotation类型,而是字符串,我觉得可能是为了解决跨ClassLoader类不相等的问题。(猜测)

另外的另外,从5.0版本开始这里的代码好像都重构过了,虽然我还没有仔细看,但我觉得原理应该是一样的,只是使用了更时髦的方式去写。(也有肯能为了能够配合Kotlin)

Q2: @AliasFor如何实现的?

众所周知,在使用@RequestMapping注解的时候,如下两种使用方法是一样的:

@RequestMapping("/path/to/endpoint")
Public HttpEntity<?> someEndpoint() {
	// ...
}

@RequestMapping(path = "/path/to/endpoint")
Public HttpEntity<?> someEndpoint() {
	// ...
}

初看起来这好像没有什么问题,意图很好理解,但是自己写过注解的同学肯定有点细思极恐的感觉。好吧,肯定得多个value()path()的值的判断,二者取其一。那么每个注解进行反射处理的时候是不是都要多写一大堆判断的代码呢?

打开Spring源码,我们发现:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {

	@AliasFor("path")
	String[] value() default {};

	@AliasFor("value")
	String[] path() default {};
	
	// ...

}

hummm,好吧又是黑科技。然后更黑的科技是,如下两种使用效果是一样的:

@RequestMapping(path = "/path/to/endpoint", method = GET)
Public HttpEntity<?> someEndpoint() {
	// ...
}

@GetMapping("/path/to/endpoint")
Public HttpEntity<?> someEndpoint() {
	// ...
}

如果你打开@GetMapping注解的源代码,就会看到:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {

	@AliasFor(annotation = RequestMapping.class)
	String[] value() default {};

	@AliasFor(annotation = RequestMapping.class)
	String[] path() default {};

	// ...
}

在前面一个问题中,我们已经回答了注解的注解是如何实现的。所以@GetMapping注解上的@RequestMapping是可以理解的,说明@GetMapping是“扩展”自@RequestMapping的。

然后我们可以猜测一下@AliasFor注解的意思,它字面上的意思是“为…的别名”。所以@GetMapping#value@RequestMapping#value的别名,@GetMapping#path@RequestMapping#path的别名。OK,字面上很好理解,那么问题来了,它是怎么实现的呢?

诀窍就在getAnnotation方法里调用的synthesizeAnnotation方法里面。当Spring发现对象注解含有@AliasFor时,或者对象注解中的注解含有@AliasFor时,Spring会返回这个注解的“合成注解”,简单来说就是把这个注解的Annotation对象包裹在一个动态代理里面,这样当你获取这个对象的被@AliasFor所注释的属性时,所有必须的信息就可以通过这个动态代理来获得。

也许我说的比较复杂,还是来看源码吧。

@SuppressWarnings("unchecked")
static <A extends Annotation> A synthesizeAnnotation(A annotation, Object annotatedElement) {
	if (annotation == null) {
		return null;
	}
	if (annotation instanceof SynthesizedAnnotation) {
		return annotation;
	}

	Class<? extends Annotation> annotationType = annotation.annotationType();
	if (!isSynthesizable(annotationType)) {
		return annotation;
	}
	
	DefaultAnnotationAttributeExtractor attributeExtractor =
			new DefaultAnnotationAttributeExtractor(annotation, annotatedElement);
	InvocationHandler handler = new SynthesizedAnnotationInvocationHandler(attributeExtractor);
	
	// Can always expose Spring's SynthesizedAnnotation marker since we explicitly check for a
	// synthesizable annotation before (which needs to declare @AliasFor from the same package)
	Class<?>[] exposedInterfaces = new Class<?>[] {annotationType, SynthesizedAnnotation.class};
	return (A) Proxy.newProxyInstance(annotation.getClass().getClassLoader(), exposedInterfaces, handler);
}

org.springframework.core.annotation.AnnotationUtils#L1504

另外要说一下的是,从5.0版本开始这一块的代码已经被全部重构了。

Q3:高级玩法

有了这些关于元编程的知识再加上我之前那篇关于注解切面的实现的文章,我觉得可以玩出很多花样。至于什么样的花样,hummm,再让我好好想想。