深入理解Spring IOC之扩展篇(五)、基于注解整合Spring框架

简介: 深入理解Spring IOC之扩展篇(五)、基于注解整合Spring框架

本篇讲的是如何将我们自己的业务逻辑和Spring框架整合起来,整合的方式主要采用的是注解,里面涉及到了多个知识点。

我们的目的是做出我们自己的注解,主要是标在接口上,当调用接口里相应的方法的时候,就会执行我们自己的逻辑。


对的,就是现在的MyBatis和Feign的整合方式,这种也是现在比较容易的,如果你业务里面xml用的多,你也可以结合xml来搞,拓展xml的文章我之前已经说过,你可以翻回去看看。


我们需要做很多步工作,我们把这些步骤拆开了一步一步来做


1、自定义注解:


首先当然是需要自定义出我们自己的注解


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface MyAnno {
}


我简单的说一下上面四个注解的意思哈,@Target表述注解可以被标注的地方,ElementType.TYPE表示只能被标注在类上。@Retention表示的是注解的生命周期,这里的RententionPolicy.Runtime表示它在被加载到jvm中之后还依然存在。@Documented表示这个注解会被javadoc工具所记录。@Inherited表示这个注解是会被继承的,其实也就是当A有我们这个@MyAnno的时候,B继承了A,B也会拥有这个注解而已。


画外音:我没有将@Target以及@Retention中所有的值都拿出来讲,因为那样的话我文章就写不完了,而且这也不是我们本章的重点,大家可以自行了解一下这个~


我们有了自己的注解之后,还需要让Spring可以识别的来我们的注解,那么此时我们需要扩展我之前讲过的BeanDefinitionRegistryPostProcessor,代码如下:


@Component
public class MyAnnoConfigurationPostProcessor1 implements BeanDefinitionRegistryPostProcessor, PriorityOrdered {
    private String[] basePackages;
    public MyAnnoConfigurationPostProcessor1() {
        // 暂且写死扫描路径
        this.basePackages = new String[]{"com.example.demo.external5"};
    }
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
        // 1. new 一个ClassPathBeanDefinitionScanner对象,ClassPathBeanDefinitionScanner这个玩意是
        // Spring 默认的注解扫描器
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry);
        // 2.为上面呢创建好的scanner对象添加Filter,主要目的是让它能够认识我们的注解
        scanner.addIncludeFilter(new AnnotationTypeFilter(MyAnno.class));
        // 3.进行扫描
        scanner.scan(this.basePackages);
    }
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {}
}



其实你也可以使用ComonentScan的include属性,这样会来的更简单一些。但是我为了剧情的进一步发展,就先引出BeanDefinitionRegistryPostProcessor


然后我们把我们的注解找个类标上:


@MyAnno
public class Person {
}


接着用这段代码测试下:


public class Test {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(Config.class);
        Person p = annotationConfigApplicationContext.getBean(Person.class);
        System.out.println(p);
    }
}
// 配置类,实际作用就是上面的ComponentScan注解
@ComponentScan(basePackages = "com.example.demo.external5")
public class Config {
}


我们可以看到如下的测试结果:


1686813487008.png


此时我们便拥有了一个自己定义的注解,这个注解现在和Spring原本的这四个注解@Component、@Controller、@Repository、@Service的作用是一样的,并且,它现在还是对接口无效的,因为标在接口上的话,会被Spring的注解扫描器ClassPathBeanDefinitionScanner这玩意忽略掉,因此我们接下来需要自己定义我们自己的注解扫描器。


2、自定义注解扫描器


注意:我们自定义的注解扫描器需要有扫描接口的功能,我们先来简单的实现一下它


public class ClassPathAnnoScanner extends ClassPathBeanDefinitionScanner {
    // 必须有这样一个构造方法,不然报错,因为父类没有无参构造,这是由于java的继承机制导致的
    public ClassPathAnnoScanner(BeanDefinitionRegistry registry) {
        super(registry);
        // 在构造器中就加上filter,使它天生就可以认识我们的自定义注解
        this.addIncludeFilter(new AnnotationTypeFilter(MyAnno.class));
    }
    // 这个方法是使扫描器能够扫描注解的核心
    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
    }
    @Override
    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        // 调用父类方法的扫描功能,返回BeanDefinition
        Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
        if (beanDefinitions.isEmpty()) {
            System.out.println("扫描到的 beanDefinitions 是空的,无法进行进一步操作!");
        }
        return beanDefinitions;
    }
}


然后在代码中结合BeanDefinitionRegistryPostProcessor去使用:


@Component
public class MyAnnoConfigurationPostProcessor implements BeanDefinitionRegistryPostProcessor, PriorityOrdered {
    private String[] basePackages;
    public MyAnnoConfigurationPostProcessor() {
        // 暂且写死
        this.basePackages = new String[]{"com.example.demo.external5"};
    }
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
        // 1. 使用我们自己的扫描器
        ClassPathAnnoScanner scanner = new ClassPathAnnoScanner(registry);
        // 2.为上面呢创建好的scanner对象添加Filter,主要目的是让它能够认识我们的注解
        scanner.addIncludeFilter(new AnnotationTypeFilter(MyAnno.class));
        // 3.进行扫描
        scanner.scan(this.basePackages);
    }
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        // 空实现即可
    }
}


这时候我们便可以把我们的MyAnno注解标在接口上了,但是,如果你此时把它标在接口上面并且启动的话,那是会报错滴,原因也很简单哈,接口是没有构造方法的,所以无法初始化。我们如果想像MyBatis或者是Feign那样子在我们调用一个接口的方法之后可以执行相应的逻辑的话,需要在运行时期生成一个相应接口的代理,并且这个代理还需要借助FactoryBean来生成(末尾含FactoryBean面试题)。

但是具体是怎么做的呢?其实还是得继续改造我们的注解扫描器,我们来看看改造的代码:


public class ClassPathAnnoScanner extends ClassPathBeanDefinitionScanner {
    // 必须有这样一个构造方法,不然报错,因为父类没有无参构造,这是由于java的继承机制导致的
    public ClassPathAnnoScanner(BeanDefinitionRegistry registry) {
        super(registry);
        this.addIncludeFilter(new AnnotationTypeFilter(MyAnno.class));
    }
    // 这个方法是使扫描器能够扫描注解的核心
    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
    }
    @Override
    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
        if (beanDefinitions.isEmpty()) {
            System.out.println("扫描到的 beanDefinitions 是空的,无法进行进一步操作!");
        } else {
            // 调用修改BeanDefinition的方法
            processBeanDefinitions(beanDefinitions);
        }
        return beanDefinitions;
    }
    // 相比上面,多了个这个修改BeanDefinition的方法
    // 需要在这里把interface的beanClass转为特定的beanClass
    private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
        for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitions) {
            BeanDefinition beanDefinition = beanDefinitionHolder.getBeanDefinition();
            // 这个会使Spring优先选择对应的有参构造
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
            // 把扫描到的interface改为FactoryBean,这样便能以FactoryBean的方式初始化
            beanDefinition.setBeanClassName(MyFactoryBean.class.getName());
        }
    }
}


可以看到哈,相比之前的,我们还需要去修改扫描到的BeanDefinition,不然你让Spring给你初始化接口,Spring是会让你嗝屁的。


3、用FactoryBean创建代理


在用Factory创建代理之前,你首先要知道代理是怎么创建的,如果这都不知道的话麻烦自行百度jdk的动态代理。


首先我们先创建出我们的代理的处理器逻辑


// 这个是属于jdk动态代理的东西
public class MyServiceProxy implements InvocationHandler {
    private Class target;
    public MyServiceProxy(Class target) {
        this.target = target;
    }
    public Object getProxy() {
        // 创建代理的核心逻辑
        return Proxy.newProxyInstance(target.getClassLoader(), new Class[]{target}, this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 每次被代理的接口的方法被调用就会走到这里来,在这里我们可以做很多事情
        // 我这里只是简单打印了方法的全路径名称而已
        // 你可以在这里根据每个方法的名称不同做不同的事情,甚至还可以根据方法参数里的method去拿方法的注解,获取注解的信息,进而做更多的事情
        System.out.println(method.getDeclaringClass().getName() + "." + method.getName());
        return null;
    }
}


然后再实现我们自己的FactoryBean:


public class MyFactoryBean<T> implements FactoryBean<T> {
    // 必须是接口的class
    private Class<T> clazz;
    public MyFactoryBean() {
    }
    public MyFactoryBean(Class<T> clazz) {
        this.clazz = clazz;
    }
    @Override
    public T getObject() throws Exception {
        // 创建代理
        return (T) new MyServiceProxy(clazz).getProxy();
    }
    @Override
    public Class<?> getObjectType() {
        return clazz;
    }
    public void setClass(Class<T> clazz){
        this.clazz = clazz;
    }
}



在这里,先说一下FactoryBean的机制。FactoryBean这个也是用于创建对象的,如果我们某个类比如A.java实现了FactoryBean的话,并且你给这个类标上了@Component这样的注解,那么,当调用getBean("a")的时候,我们获取到的是FactoryBean中getObject返回的对象。如果我们想要得到FactoryBean本身应该则应该调用在参数前加上"&",比如getBean("&a")这样去调用,或者getBean(A.class)这样传个Class对象进去也可以,具体的原因可以看我前面的文章。


此时我们可以尝试着把我们的@MyAnno注解加在接口上:


@MyAnno
public interface TestService {
    void eat();
}


接口很简单,也没有实现类。然后用如下测试代码进行测试


public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(Config.class);
        TestService testService = (TestService) annotationConfigApplicationContext.getBean("testService");
        testService.eat();
    }


完了可以看到控制台输出的内容如下:


1686813468522.png


此时还没完,因为别忘了,我们的MyAnnoConfigurationPostProcessor里面的扫描路径是写死的。一般来说,我们会把扫描路径配合一个注解写到启动类上,方便统一管理,就像Mybatis的@MapperScan那样。明确目标后,然后我们来解决我们的问题


4、使用注解配置扫描路径


我们定义一下我们自己的Scanner:


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(MyAnnoScannerRegistrar.class)
public @interface MyAnnoScanner {
    // value 为包扫描的路径
    String[] value() default {};
}


注意这个import注解引进来的这个class是重中之重,它是用来对我们这个MyAnnoScanner里面value值对应的包来进行扫描的,我们来看看代码:


// 必须实现ImportBeanDefinitionRegistrar
public class MyAnnoScannerRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // 获取被Import注解所标着的类的元数据
        AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MyAnnoScanner.class.getName()));
        List<String> basePackages = new ArrayList();
        // 获取MyAnnoScanner里面的路径
        for (String pkg : annoAttrs.getStringArray("value")) {
            if (StringUtils.hasText(pkg)) {
                basePackages.add(pkg);
            }
        }
        ClassPathAnnoScanner scanner = new ClassPathAnnoScanner(registry);
        for (String basePackage : basePackages) {
            // 针对每个路径进行扫描
            scanner.doScan(basePackage);
        }
    }
}


最后我们在我们的Config.java上加上我们的注解:


@ComponentScan(basePackages = "com.example.demo.external5")
@MyAnnoScanner(value = "com.example.demo.external5")
public class Config {
}


注意哈,之前的MyAnnoConfigurationPostProcessor这个类我们就可以干掉了,因为它此时已经完全没什么用了。我们用和上面一样的测试代码,发现最后的输出了我们想要的东西,此时,完整的一个整合就结束了。


多说一下关于Import注解的东西,你只需要记住,当Import进来的类,没有实现ImportBeanDefinitionRegistrar这个接口的时候,这个类就会被放进Spring容器中, 你可以通过@Autowired的方式去自动注入它;反之如果实现了ImportBeanDefinitionRegistrar,那么这个类之后是不会放入Spring中,这个原因涉及到的代码在ConfigurationClassPostProcessor的方法postProcessBeanDefinitionRegistry中,具体是还是比较复杂的,我后边如果有时间也会专门再去写文章讲这些。


关于FactoryBean的面试题


相信不少人在面试中是遇到过这样一个面试题的:你能说说FactoryBean和BeanFactory的区别吗?我们可以说他们都是工厂,都是用来创建对象的,但是创建对象的场景是天差地别的,BeanFactory是可以用来创建各种各样的对象,但是FactoryBean是用来创建某一类的复杂对象的。并且BeanFactory人家的实现类都可以说是一个一个的容器,但是FactoryBean就不是了。

目录
相关文章
|
6天前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
22 4
|
8天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,包括版本兼容性、安全性、性能调优等方面。
62 1
|
3天前
|
Java API 数据库
Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐
本文通过在线图书管理系统案例,详细介绍如何使用Spring Boot构建RESTful API。从项目基础环境搭建、实体类与数据访问层定义,到业务逻辑实现和控制器编写,逐步展示了Spring Boot的简洁配置和强大功能。最后,通过Postman测试API,并介绍了如何添加安全性和异常处理,确保API的稳定性和安全性。
10 0
|
4天前
|
人工智能 开发框架 Java
总计 30 万奖金,Spring AI Alibaba 应用框架挑战赛开赛
Spring AI Alibaba 应用框架挑战赛邀请广大开发者参与开源项目的共建,助力项目快速发展,掌握 AI 应用开发模式。大赛分为《支持 Spring AI Alibaba 应用可视化调试与追踪本地工具》和《基于 Flow 的 AI 编排机制设计与实现》两个赛道,总计 30 万奖金。
|
5天前
|
人工智能 Java API
阿里云开源 AI 应用开发框架:Spring AI Alibaba
近期,阿里云重磅发布了首款面向 Java 开发者的开源 AI 应用开发框架:Spring AI Alibaba(项目 Github 仓库地址:alibaba/spring-ai-alibaba),Spring AI Alibaba 项目基于 Spring AI 构建,是阿里云通义系列模型及服务在 Java AI 应用开发领域的最佳实践,提供高层次的 AI API 抽象与云原生基础设施集成方案,帮助开发者快速构建 AI 应用。本文将详细介绍 Spring AI Alibaba 的核心特性,并通过「智能机票助手」的示例直观的展示 Spring AI Alibaba 开发 AI 应用的便利性。示例源
|
5天前
|
架构师 Java 开发者
得物面试:Springboot自动装配机制是什么?如何控制一个bean 是否加载,使用什么注解?
在40岁老架构师尼恩的读者交流群中,近期多位读者成功获得了知名互联网企业的面试机会,如得物、阿里、滴滴等。然而,面对“Spring Boot自动装配机制”等核心面试题,部分读者因准备不足而未能顺利通过。为此,尼恩团队将系统化梳理和总结这一主题,帮助大家全面提升技术水平,让面试官“爱到不能自已”。
得物面试:Springboot自动装配机制是什么?如何控制一个bean 是否加载,使用什么注解?
|
10天前
|
XML Java 数据库
Spring boot的最全注解
Spring boot的最全注解
|
8天前
|
Java 数据库连接 数据库
让星星⭐月亮告诉你,SSH框架01、Spring概述
Spring是一个轻量级的Java开发框架,旨在简化企业级应用开发。它通过IoC(控制反转)和DI(依赖注入)降低组件间的耦合度,支持AOP(面向切面编程),简化事务管理和数据库操作,并能与多种第三方框架无缝集成,提供灵活的Web层支持,是开发高性能应用的理想选择。
12 1
|
5天前
|
XML Java 数据格式
Spring IOC容器的深度解析及实战应用
【10月更文挑战第14天】在软件工程中,随着系统规模的扩大,对象间的依赖关系变得越来越复杂,这导致了系统的高耦合度,增加了开发和维护的难度。为解决这一问题,Michael Mattson在1996年提出了IOC(Inversion of Control,控制反转)理论,旨在降低对象间的耦合度,提高系统的灵活性和可维护性。Spring框架正是基于这一理论,通过IOC容器实现了对象间的依赖注入和生命周期管理。
17 0
|
11月前
|
Java Spring
spring框架之AOP模块(面向切面),附带通知类型---超详细介绍
spring框架之AOP模块(面向切面),附带通知类型---超详细介绍
113 0