spring-core:获取类/方法/字段/字段上直接定义的注解

文章讲述了在Spring框架中,由于@AliasFor的存在,如何正确获取类上直接定义的注解,以及使用MergedAnnotations.from和SearchStrategy.DIRECT策略来避免basePackages和value字段的混淆。

AnnotatedElement.getAnnotation

如何获取一个类上定义的注解?
这个问题似乎不应该问,我们知道如果要获取一个类/方法/字段/字段上直接定义的注解是很方便的,如Class.getAnnotation(Class<CasbanScan>)就能实现,
只要实现了java.lang.reflect.AnnotatedElement接口的对象(Class,Method,Field)都有这个方法,
但是在Spring框架下,如果注解中有@AliasFor定义了别名的字段,要区别对待。
如下示例,
Class.getAnnotation获取注解对象,basePackages字段是空的,value字段才能获取正确的值。

	@Test
	public void test2GetDirectlyAnnotation() {
		try {
			{
				System.out.printf("-----> For %s\n",UserSummy.class.getSimpleName());
				System.out.println("\t==== USING Class.getAnnotation ====");
				CasbanScan annot = UserSummy.class.getAnnotation(CasbanScan.class);
				System.out.println("\t"+annot.toString());
				assertArrayEquals(new String[] {"hello"}, annot.value());
				System.out.printf("\tvalue() size:%d\n",annot.value().length);
				System.out.printf("\tbasePackages() size:%d\n",annot.basePackages().length);
				assertEquals(0, annot.basePackages().length);
			}
		} catch (Throwable e) {
			e.printStackTrace();
			fail();
		}
	}
	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.TYPE)
	public @interface CasbanScan {
		@AliasFor("basePackages")
		String[] value() default {};
		@AliasFor("value")
		String[] basePackages() default {};
	}

	@CasbanScan("hello")
	public static class UserSummy{
	}

Spring框架将注解(Annotation)用到了极致,对注解定义扩展了很多灵活了定义,@AliasFor就是一种灵活扩展。所以在Spring框架下,要获取正确直接定义在类上的注解。Class.getAnnotation(Class<CasbanScan>)方法并不一定合适。

Spring-core

SearchStrategy

为适应各种场景,Spring通过枚举类型SearchStrategy来定义搜索策略,
其中SearchStrategy.DIRECT 定义为仅查找直接声明的注释,而不考虑@Inherited注释,也不搜索父类或实现的接口。
所以只要使用SearchStrategy.DIRECT 作为搜索策略就只查找直接声明的注释。

MergedAnnotations

MergedAnnotations.from(AnnotatedElement element)方法就是使用SearchStrategy.DIRECT 作为搜索策略 创建一个新的MergedAnnotations实例,其中包含来自指定元素的所有注释和元注释
如下是MergedAnnotations.from(AnnotatedElement element)方法的实现

	static MergedAnnotations from(AnnotatedElement element) {
		return from(element, SearchStrategy.DIRECT);
	}

所以我们可以使用MergedAnnotations.from(AnnotatedElement element)方法来获取直接注解定义,

MergedAnnotation<CasbanScan> mergedAnnotation = 
	MergedAnnotations.from(UserSummy.class).get(CasbanScan.class);
if(mergedAnnotation.isDirectlyPresent()){
	System.out.println("FOUND @CasbanScan in UserSummy");
	/** 将 MergedAnnotation转为方便输出的 AnnotationAttributes 对象 */
	System.out.println(mergedAnnotation.asAnnotationAttributes());
	/** 直接从MergedAnnotation对象读取字段 */
	System.out.println(mergedAnnotation.getStringArray("basePackages")[0]);
	System.out.println(mergedAnnotation.getStringArray("value")[0]);
}

完整测试代码如下:

import static org.junit.Assert.*;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.Test;

import org.springframework.core.annotation.AliasFor;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;

public class SpringAnnotationUtilsTest {

	@Test
	public void test2GetDirectlyAnnotation() {
		try {
			System.out.printf("-----> For %s\n",UserSummy.class.getSimpleName());
			{
				/**
				 * 直接调用 Class.getAnnotation方法可以读取到注解,但对于用@AliasFor定义了别名的字段,要区别对待。
				 * 如下,basePackages字段是空的,value字段才能获取正确的值 
				 */
				System.out.println("\t==== USING Class.getAnnotation ====");
				CasbanScan annot = UserSummy.class.getAnnotation(CasbanScan.class);
				System.out.println("\t"+annot.toString());
				assertArrayEquals(new String[] {"hello"}, annot.value());
				System.out.printf("\tvalue() size:%d\n",annot.value().length);
				System.out.printf("\tbasePackages() size:%d\n",annot.basePackages().length);
				assertEquals(0, annot.basePackages().length);
			}
			{
				System.out.println("\t==== USING AnnotationUtils.isAnnotationDeclaredLocally  ====");
				boolean declaredLocally = AnnotationUtils.isAnnotationDeclaredLocally(CasbanScan.class, UserSummy.class);
				System.out.printf("\t@%s declaredLocally in %s:%b\n",CasbanScan.class.getSimpleName(),UserSummy.class.getSimpleName(), declaredLocally);
				assertTrue(declaredLocally);
				System.out.println("\t==== USING Spring ====");
				MergedAnnotations mergedAnnotations= MergedAnnotations.from(UserSummy.class);
				assertTrue(mergedAnnotations.isDirectlyPresent(CasbanScan.class));
				MergedAnnotation<CasbanScan> mergedAnnotation= mergedAnnotations.get(CasbanScan.class);
				assertTrue(mergedAnnotation.isDirectlyPresent());
				/** 将 MergedAnnotation转为方便输出的 AnnotationAttributes 对象 */
				System.out.println("\t"+mergedAnnotation.asAnnotationAttributes());
				/** 直接从MergedAnnotation对象读取字段 */
				assertArrayEquals(new String[] {"hello"}, mergedAnnotation.getStringArray("basePackages"));
				assertArrayEquals(new String[] {"hello"}, mergedAnnotation.getStringArray("value"));
				System.out.println("\t==== USING Annotation synthesized ====");
				/**
				 * 获取合成注解对象 
				  */
				CasbanScan annot = mergedAnnotation.synthesize();
				/** 调用AnnotationUtils.getAnnotationAttributes将 Annotation对象转为方便输出的Map  */
				System.out.printf("\t%s\n", AnnotationUtils.getAnnotationAttributes(annot));
				/*
				 * 对于用@AliasFor定义了别名的字段不论用读取哪个字段都是有正确的值
				 */
				assertArrayEquals(new String[] {"hello"}, annot.value());
				assertArrayEquals(new String[] {"hello"}, annot.basePackages());
			}
			{
				System.out.printf("-----> For %s\n",VipSummy.class.getSimpleName());
				MergedAnnotations mergedAnnotations= MergedAnnotations.from(VipSummy.class);
				boolean declaredLocally = mergedAnnotations.isDirectlyPresent(CasbanScan.class);
				System.out.printf("\t%s FOUND directly @%s in %s\n", declaredLocally?"":"NOT",CasbanScan.class.getSimpleName(),VipSummy.class.getSimpleName());
				assertFalse(mergedAnnotations.isDirectlyPresent(CasbanScan.class));
				MergedAnnotation<CasbanScan> mergedAnnotation= mergedAnnotations.get(CasbanScan.class);
				assertFalse(mergedAnnotation.isDirectlyPresent());
				
			}
		} catch (Throwable e) {
			e.printStackTrace();
			fail();
		}
	}
	
	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.TYPE)
	public @interface CasbanScan {
		@AliasFor("basePackages")
		String[] value() default {};
		@AliasFor("value")
		String[] basePackages() default {};
	}

	@CasbanScan("hello")
	public static class UserSummy{
	}
	public static class VipSummy extends UserSummy{
	}
}

Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled. 11-04-2025 08:46:40.440 ERROR [main] [TID: N/A] o.s.b.d.LoggingFailureAnalysisReporter: *************************** APPLICATION FAILED TO START *************************** Description: Field identityAccessInternalApiService in com.tplink.smb.omada.manager.adaptor.service.LicenseManagmentServiceImpl required a single bean, but 2 were found: - com.tplink.smb.omada.identityaccess.port.common.service.IdentityAccessInternalApiServiceImpl: defined in URL [jar:nested:/home/ubuntu/server/lib/server.jar/!BOOT-INF/lib/identityaccess-core-6.1.0.126.jar!/com/tplink/smb/omada/identityaccess/port/common/service/IdentityAccessInternalApiServiceImpl.class] - com.tplink.smb.omada.identityaccess.port.grpc.client.IdentityAccessInternalApiServiceGrpcClient: defined in URL [jar:nested:/home/ubuntu/server/lib/server.jar/!BOOT-INF/lib/identityaccess-port-grpc-client-6.1.0.910-SNAPSHOT.jar!/com/tplink/smb/omada/identityaccess/port/grpc/client/IdentityAccessInternalApiServiceGrpcClient.class] This may be due to missing parameter name information Action: Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed Ensure that your compiler is configured to use the '-parameters' flag. You may need to update both your build tool settings as well as your IDE. (See https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-6.1-Release-Notes#parameter-name-retention) 给我分析一下这段错误的产生原因,以及修改的建议
最新发布
11-05
以下是添加 **MongoDB Profile** 后的完整配置,支持多数据源(MySQL、Cassandra、MongoDB)切换,并通过 `spring.profiles.active` 控制: ```yaml spring: profiles: active: mongodb # 默认激活MongoDB,可通过启动参数覆盖(如 --spring.profiles.active=mysql) --- # MySQL配置 spring: profiles: mysql datasource: url: jdbc:mysql://localhost:3306/user_db?useSSL=false&serverTimezone=UTC username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jpa: properties: hibernate: dialect: org.hibernate.dialect.MySQL8Dialect hibernate: ddl-auto: update # 自动更新表结构 --- # Cassandra配置 spring: profiles: cassandra data: cassandra: contact-points: localhost port: 9042 keyspace-name: mycas schema-action: CREATE_IF_NOT_EXISTS # 自动创建键空间和表 autoconfigure: exclude: - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration - org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration --- # MongoDB配置(新增) spring: profiles: mongodb data: mongodb: uri: mongodb://localhost:27017/user_db # MongoDB连接URI auto-index-creation: true # 自动创建索引(根据实体@Indexed注解) autoconfigure: exclude: - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration - org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration - org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration # 排除Cassandra自动配置 # 通用管理端点配置 management: endpoints: web: exposure: include: "metrics,health" # 业务配置 app: max-users: 7 ``` --- ### 关键说明 1. **Profile 切换**: - 默认激活 `mongodb`,可通过启动参数动态切换: ```bash java -jar app.jar --spring.profiles.active=mysql ``` 2. **MongoDB 配置要点**: - `uri`:支持完整连接参数(如认证、副本集): ```yaml uri: mongodb://user:password@host1:27017,host2:27017/user_db?authSource=admin&replicaSet=rs0 ``` - `auto-index-creation`:根据实体的 `@Indexed` 注解自动创建索引。 3. **冲突排除**: - 同时排除 JDBC 和 Cassandra 的自动配置,避免多数据源冲突。 4. **多Profile共存**: - 如需同时启用多个Profile(如开发环境用H2+MongoDB),可修改为: ```yaml spring: profiles: active: h2,mongodb ``` --- ### 实体适配示例 确保实体兼容 MongoDB(使用 `@Document` 而非 `@Entity`): ```java import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.index.Indexed; @Document(collection = "users") // MongoDB集合名 public class User { @Id private String id; @Indexed(unique = true) // 创建唯一索引 private String email; // 其他字段、getter/setter... } ``` --- ### 测试建议 1. **验证Profile加载**: ```java @Autowired private Environment env; @Test void checkActiveProfile() { assertTrue(Arrays.asList(env.getActiveProfiles()).contains("mongodb")); } ``` 2. **MongoDB操作测试**: ```java @Autowired private MongoTemplate mongoTemplate; @Test void testMongoInsert() { User user = new User(); user.setEmail("test@example.com"); mongoTemplate.save(user); assertNotNull(user.getId()); } ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

10km

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

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

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

打赏作者

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

抵扣说明:

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

余额充值