自定义注解实现Spring容器注入Bean方式(类似于mybatis的@MapperScans)
作者:豆腐脑lr
前言
本文通过自定义注解@MyService
和@MyServiceScans
,将SpringBoot项目中带有@MyService
或@MyServiceScans(basePackages={"com.whut.scaner.service"})
包内的类注入到Spring容器中。
文字的目录如下:
1. 导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
2. 创建自定义注解
- @MyService注解
package com.whut.scaner.annotation; import java.lang.annotation.*; @Documented @Inherited @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface MyService { }
- MyServiceScans注解
这个注解有一个数组参数basePackages
可以存放一个或者多个包的全路径。
@Documented @Inherited @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface MyServiceScans { String[] basePackages() default {}; }
3. 定义包扫描器
ClassPathBeanDefinitionScanner
作用就是在Spring启动时自动扫描项目中的类,并创建并注册它的bean定义,使得我们能在需要时从Spring上下文中取得所需的bean。因此,ClassPathBeanDefinitionScanner是实现Spring自动化配置的关键构件。
这里我们自定义一个类来继承ClassPathBeanDefinitionScanner
,同时重写了一个带有是否使用默认Bean过滤器的boolean useDefaultFilters
值的构造器(因为我们后续要用自己的过滤器)。
package com.whut.scaner.config; import com.whut.scaner.annotation.MyService; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; import org.springframework.core.type.filter.AnnotationTypeFilter; import java.util.Set; public class MyServiceClassPathBeanDefinitionScanner extends ClassPathBeanDefinitionScanner { public MyServiceClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters) { super(registry, useDefaultFilters); } /** * @addIncludeFilter 将自定义的注解添加到扫描任务中 */ protected void registerFilters() { /** * 注入@MyService注解标记的类 */ addIncludeFilter(new AnnotationTypeFilter(MyService.class)); } @Override protected Set<BeanDefinitionHolder> doScan(String... basePackages) { return super.doScan(basePackages); } }
4. 注册Bean到容器
这里通过实现Spring的ImportBeanDefinitionRegistrar
接口并配合@Import
注解,实现Spring容器注入。
- ImportBeanDefinitionRegistrar是Spring框架的一部分。
- 它是一个接口,可以在运行时注册额外的bean定义。
- 这工作一般是在应用启动时由Spring容器处理,但在某些情况下,开发者可能想要编程地控制bean的注册。
- 当一个类实现ImportBeanDefinitionRegistrar接口时,Spring会调用该类的registerBeanDefinitions方法。
- 在这个方法中,开发者可以使用BeanDefinitionRegistry参数将自定义的bean定义添加到registry中。
- 当Spring后续创建并初始化beans时,这些新注册的bean定义就会被考虑在内。
4.1 @Myservice标识的类注入到容器
package com.whut.scaner.config; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.type.AnnotationMetadata; @Slf4j public class MyServiceRegister implements ImportBeanDefinitionRegistrar { /** * 方法1: 将带有@MyService注解的类注入到Spring容器 */ @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { //自定义的扫描类MyClassPathBeanDefinitionScanner, 实现了ClassPathBeanDefinitionScanner接口 // 当前MyClassPathBeanDefinitionScanner已被修改为扫描带有指定注解的类 MyServiceClassPathBeanDefinitionScanner scanner = new MyServiceClassPathBeanDefinitionScanner(registry, false); scanner.registerFilters(); // 过滤带有注解的类并注入到容器中 scanner.doScan("com.whut.scaner"); } }
4.2 @MyServiceScans包扫描注入
package com.whut.scaner.config; import com.whut.scaner.annotation.MyServiceScans; import com.whut.scaner.filter.MyServicePackageFilter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.AnnotationMetadata; import org.springframework.lang.NonNull; import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.List; @Slf4j public class MyServiceScansRegister implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata annotationMetadata , @NonNull BeanDefinitionRegistry beanDefinitionRegistry) { AnnotationAttributes annotationAttrs = AnnotationAttributes .fromMap(annotationMetadata.getAnnotationAttributes(MyServiceScans.class.getName())); if (annotationAttrs == null) { log.warn("EsMapperScan not exist"); return; } //构造扫描器,并将spring的beanDefinitionRegistry注入到扫描器内,方便将扫描出的BeanDefinition注入进入beanDefinitionRegistry MyServiceClassPathBeanDefinitionScanner scanner = new MyServiceClassPathBeanDefinitionScanner(beanDefinitionRegistry, false); List<String> basePackages = new ArrayList<>(); for (String pkg : annotationAttrs.getStringArray("basePackages")) { if (StringUtils.hasText(pkg)) { basePackages.add(pkg); } } //添加相关过滤器(为了用户无感知,不过滤@MyService注解,直接处理basePackages下面的所有类) scanner.addIncludeFilter(new MyServicePackageFilter()); //扫描并注入 scanner.doScan(StringUtils.toStringArray(basePackages)); } }
MyServicePackageFilter
package com.whut.scaner.filter; import lombok.NonNull; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.TypeFilter; /** * 直接过滤 直接处理basePackages下面的所有类 */ public class MyServicePackageFilter implements TypeFilter { @Override public boolean match(MetadataReader metadataReader, @NonNull MetadataReaderFactory metadataReaderFactory) { // 为了用户无感知,不用过滤出带有@Myservice的类,直接处理basePackages下面的所有类 // return metadataReader.getAnnotationMetadata() // .hasAnnotation("com.whut.scanner.service"); return true; } }
4.3 导入自定义注解注册类
这里我们设置被扫描的包为:com.whut.scaner.service2
@Configuration @Import({MyServiceRegister.class, MyServiceScansRegister.class}) @MyServiceScans(basePackages={"com.whut.scaner.service2"}) public class MyConfiguration { }
5. 测试
首先创建2个不同包下的Service,如下图所示:
使用@MyService
注解标注的类。
package com.whut.scaner.service; import com.whut.scaner.annotation.MyService; @MyService public class UserService { public void test() { System.out.println("我是UserService的test()方法"); } }
放在指定被扫描的包下(com.whut.scaner.service2
)的类。
package com.whut.scaner.service2; public class StudentService { public void test() { System.out.println("我是StudentService的test()方法"); } }
在SpringBoot启动类进行测试
@SpringBootApplication public class App { public static void main( String[] args ) { ConfigurableApplicationContext context = SpringApplication.run(App.class, args); UserService userService = context.getBean(UserService.class); userService.test(); StudentService studentService = context.getBean(StudentService.class); studentService.test(); } }
启动项目后,控制台打印:
_ _ |_ _ _|_. ___ _ | _
| | |\/|_)(_| | |_\ |_)||_|_\
/ |
3.5.3.2
2023-10-17 14:19:43.086 INFO 23988 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2023-10-17 14:19:43.093 INFO 23988 --- [ main] com.whut.scaner.App : Started App in 1.987 seconds (JVM running for 3.286)
我是UserService的test()方法
我是StudentService的test()方法
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。