Spring 框架中的 @Enable* 注解是怎样实现的?

本文详细解析了Spring框架中@Enable*注解的工作原理,包括@Import注解的使用,ImportSelector和ImportBeanDefinitionRegistrar接口的功能,以及如何实现自定义的@EnableHelloWorld注解。

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

概述

Spring 的项目中,我们经常会使用 @Enable 开头的注解到配置类中,添加了这种注解之后,便会开启一些功能特性。常用的注解如 @EnableWebMvc、@EnableTransactionManagement、@EnableAsync、@EnableScheduling 等等。集成到 Spring 的第三方的框架中,我们也经常会看到这样的注解,如阿里巴巴开源的缓存框架 jetcache,便需要使用注解 @EnableCreateCacheAnnotation 开启缓存能力。本篇将尝试对其实现进行分析,并实现一个自己的 @Enable* 注解。

@Enable* 注解实现分析

以 @EnableScheduling 注解为例,其源码如下。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {

}

除了最常用的 Java 提供的元注解,这个 @EnableScheduling 使用了 @Import 注解,很明显 @Enable* 就是利用 @Import 注解实现的。查看 @Import 源码如下。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {

	/**
	 * {@link Configuration @Configuration}, {@link ImportSelector},
	 * {@link ImportBeanDefinitionRegistrar}, or regular component classes to import.
	 */
	Class<?>[] value();

}

@Import 可以标注在类型上,只有一个类型为 Class 的 value 属性,根据注释我们可以看到,value 属性值可以是 @Configuration 配置类、ImportSelector、ImportBeanDefinitionRegistrar 或者常规组件类。Spring 读取到这几个类型时的处理如下。

  • @Configuration 配置类:Spring 对配置类进行解析。
  • ImportSelector:实例化该接口实现类,提供元数据给接口中的方法,将返回的类型作为配置类进行解析。
  • ImportBeanDefinitionRegistrar:实例化该类型,提供元数据给接口中的方法,由用户自己控制注入哪些 bean。
  • 常规类:将该类作为 bean 进行注入。

value 值为配置类的时候 Spring 会对配置类进行解析,值为常规组件类的时候 Spring 会把组件类作为 Spring 的 bean 进行注入,这两个相对比较容易理解。下面对剩下的两个类型进行分析。

ImportSelector 定义
ImportSelector 接口定义如下。

public interface ImportSelector {

	/**
	 * 选择并返回哪些类应该被导入,导入的类将作为配置类再次进行解析
	 * AnnotationMetadata 是 @Configuration 配置类的元数据
	 */
	String[] selectImports(AnnotationMetadata importingClassMetadata);

	/**
	 * 排除导入的类
	 * @since 5.2.4
	 */
	@Nullable
	default Predicate<String> getExclusionFilter() {
		return null;
	}

}

ImportSelector#selectImports 方法中的参数 AnnotationMetadata 是使用 @Import 注解的配置类的元数据,返回值将作为配置类再次进行解析。如果返回值是实现 BeanFactoryPostProcessor 或者 BeanPostProcessor 接口的组件,我们将有机会在 Spring 应用上下文或 bean 的生命周期中做一些其他的操作。如果返回值是配置类,我们还能根据 @Conditional 注解根据条件注入一些 bean 。@Conditional 注解的使用见 Spring 条件注解 @Conditional 使用及其底层实现

ImportSelector#getExclusionFilter 方法则用于从导入的类中排除我们不想导入的类。

ImportBeanDefinitionRegistrar 定义
ImportBeanDefinitionRegistrar 定义如下。

public interface ImportBeanDefinitionRegistrar {
	// 注册 BeanDefinition
	default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
			BeanNameGenerator importBeanNameGenerator) {
		registerBeanDefinitions(importingClassMetadata, registry);
	}
	// 注册 BeanDefinition
	default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
	}
}

相比 ImportSelector 只能获取配置类的元数据,ImportBeanDefinitionRegistrar 接口提供了更为灵活的方法,还能收到参数 BeanDefinitionRegistry、BeanNameGenerator 。 BeanDefinitionRegistry 可以方便我们获取已注册的 BeanDefinition,注册自己定义的 BeanDefinition。BeanNameGenerator 则用于生成 bean 的名称。

@Import 实现分析

通过上面的描述,我们知道 @Enable* 注解是通过 @Import 实现的,而 @Import 的处理在 Spring 中的配置类解析过程中。解析 @Import 注解的核心代码位置为 ConfigurationClassParser#processImports,源码如下。

	/**
	 * 处理 @Import 注解
	 *
	 * @param configClass             标注或元标注 @Import 的配置类
	 * @param currentSourceClass      配置类对应的 SourceClass
	 * @param importCandidates        @Import 注解 value 属性值集合或 ImportSelector.selectImports 返回值
	 * @param exclusionFilter         不要处理配置类的过滤器
	 * @param checkForCircularImports 是否检查循环 @Import
	 */
	private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
			Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
			boolean checkForCircularImports) {

		if (importCandidates.isEmpty()) {
			return;
		}

		if (checkForCircularImports && isChainedImportOnStack(configClass)) {
			this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
		} else {
			this.importStack.push(configClass);
			try {
				for (SourceClass candidate : importCandidates) {
					// @Import value 属性值是 ImportSelector.class
					if (candidate.isAssignable(ImportSelector.class)) {
						// Candidate class is an ImportSelector -> delegate to it to determine imports
						Class<?> candidateClass = candidate.loadClass();
						ImportSelector selector = ParserStrategyUtils
								.instantiateClass(candidateClass, ImportSelector.class,
										this.environment, this.resourceLoader, this.registry);
						Predicate<String> selectorFilter = selector.getExclusionFilter();
						if (selectorFilter != null) {
							exclusionFilter = exclusionFilter.or(selectorFilter);
						}
						if (selector instanceof DeferredImportSelector) {
							// @Import value 属性值是 DeferredImportSelector.class
							this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
						} else {
							String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
							Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames,
									exclusionFilter);
							// 递归处理 ImportSelector 返回的配置类
							processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter,
									false);
						}
					} else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
						// Candidate class is an ImportBeanDefinitionRegistrar ->
						// delegate to it to register additional bean definitions
						Class<?> candidateClass = candidate.loadClass();
						ImportBeanDefinitionRegistrar registrar =
								ParserStrategyUtils
										.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
												this.environment, this.resourceLoader, this.registry);
						configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
					} else {
						// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
						// process it as an @Configuration class
						this.importStack.registerImport(
								currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
						processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
					}
				}
			} catch (BeanDefinitionStoreException ex) {
				throw ex;
			} catch (Throwable ex) {
				throw new BeanDefinitionStoreException(
						"Failed to process import candidates for configuration class [" +
								configClass.getMetadata().getClassName() + "]", ex);
			} finally {
				this.importStack.pop();
			}
		}
	}

processImports 方法整体结构较为清晰,对 @Import 注解的 value 属性值进行循环处理。

  1. 首先判断是否为 ImportSelector 类型,如果是则 调用 ImportSelector#selectImports 方法,并将取到的类型作为配置类,进行递归解析 @Import。
  2. 否则如果类型为 ImportBeanDefinitionRegistrar,则对其实例化,并添加到 importBeanDefinitionRegistrars 中,后面将统一进行处理。
  3. 否则将导入的类作为 bean 进行注册,然后作为配置类递归进行处理。

实现自己的 @Enable* 注解

下面尝试实现一个 @EnableHelloWorld ,配置类上使用了该注解后将自动注入一个 HelloWorld 的 bean。代码如下。

public class HelloWorld {

    public void sayHello(){
        System.out.println("hello,world");
    }
}

public class HelloWorldSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{HelloWorld.class.getName()};
    }
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(HelloWorldSelector.class)
public @interface EnableHelloWorld {

}

@EnableHelloWorld
@Configuration
public class Application {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.scan("com.zzuhkp");
        context.refresh();
        context.getBean(HelloWorld.class).sayHello();
    }

}

程序执行后成功打印出“hello,world”,表明自定义 @Enable* 成功生效。

总结

@Enable* 注解通过 @Import 注解实现。@Import 注解在配置类解析过程中进行处理。通过 @Import 可以方便的注入自定义的 bean 。

### 使用AOP及其注解 #### AOP简介 面向切面编程(Aspect-Oriented Programming, AOP)是一种编程范式,旨在通过程序化手段解决横切关注点的问题。在Spring框架中,AOP允许开发者定义方法拦截器和切入点,从而将日志记录、事务管理等功能模块化。 #### 注解驱动的AOP配置 为了简化AOP配置并提高灵活性,Spring支持基于注解的方式来声明切面。这主要依赖于`@AspectJ`风格的支持,在Java类上应用特定的元数据标签来指示它们作为切面存在[^1]。 #### 关键注解解释 - **@Aspect**注解用于标记一个类为切面类。它告诉Spring容器该类包含了多个可能影响其他对象行为的方法——即所谓的“增强处理”。 - **@Pointcut** 定义了一个匹配连接点的选择条件。例如,下面的例子指定了所有来自`com.kfit.user.service.UserService`包下的任何公共成员函数调用都将是潜在的织入位置。 ```java @Pointcut("execution(* com.kfit.user.service.UserService.*(..))") private void pointcut() {} ``` - **@Before/@AfterReturning/@AfterThrowing/@Around** 这些都是不同类型的通知(Advice),分别表示前置通知、返回后通知、异常抛出后的通知以及环绕通知。其中最灵活的是`@Around`,它可以完全控制目标方法执行前的行为,并决定是否继续执行原定操作或直接返回自定义的结果。 - **@Order** 当有多个切面应用于同一个连接点时,可以通过此属性指定这些切面之间的相对顺序。较低数值意味着更高的优先级[^4]。 #### 实现示例 假设有一个简单的应用程序结构如下: ##### 用户服务接口 (`UserService`) ```java package com.example.demo; public interface UserService { String getUserInfo(); } ``` ##### 默认实现 (`UserServiceImpl`) ```java package com.example.demo; @Component public class UserServiceImpl implements UserService { @Override public String getUserInfo(){ System.out.println("Fetching user info..."); return "John Doe"; } } ``` ##### 日志切面 (`LoggingAspect`) ```java package com.example.aspect; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import java.util.Arrays; // Mark this as an aspect and give it a higher precedence over other aspects. @Component @Order(1) @Aspect public class LoggingAspect { // Define the pointcut expression that matches all methods within our service layer. @Pointcut("within(com.example..*) && execution(public * *(..))") private void anyPublicMethodInServiceLayer(){} // Before advice to log method entry details before proceeding with actual call. @Before("anyPublicMethodInServiceLayer()") public void logEntryToAnyPublicMethodJoinPoint(JoinPoint joinPoint){ System.out.printf("[BEFORE] Entering %s.%s(%s)%n", joinPoint.getTarget().getClass(), joinPoint.getSignature().getName(), Arrays.toString(joinPoint.getArgs())); } // After returning advice logs successful completion of matched operations. @AfterReturning(pointcut="anyPublicMethodInServiceLayer()",returning="result") public void afterSuccessfulReturn(Object result){ System.out.printf("[AFTER RETURNING] Method returned '%s'%n",result); } // Around advice allows us more control by wrapping around target operation entirely. @Around("@annotation(org.springframework.web.bind.annotation.GetMapping)") public Object handleWebRequests(ProceedingJoinPoint pjp)throws Throwable{ long start=System.currentTimeMillis(); try{ System.out.print("[AROUND-BEGIN]"); Object outcome=pjp.proceed(); // Proceed with original invocation. System.out.printf("\t[AROUND-END] Execution took %d ms%n",System.currentTimeMillis()-start); return outcome; // Return whatever was supposed to be returned from intercepted method. }catch(Throwable t){ throw new RuntimeException(t); // Rethrow exceptions wrapped inside runtime exception. } } } ``` ##### 启动类 (`DemoApplication`) ```java @SpringBootApplication @EnableAspectJAutoProxy(proxyTargetClass=true) // Enable AspectJ proxy support explicitly here. public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class,args).getBean(UserService.class).getUserInfo(); } } ``` 上述代码展示了如何利用Spring AOP特性创建一个名为`LoggingAspect`的日志记录组件,其职责是在每次访问`UserService#getUserInfo()`之前打印一条消息,并在其成功完成后再次输出另一条信息。此外还提供了一种方式去监控带有@GetMapping标注的HTTP请求性能指标。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大鹏cool

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值