转载

聊一聊 Spring 中的扩展机制(一)

之前 Spring 源码系列文章中大多是底层源码的分析,通过源码可以让我们能够清晰的了解 Spring 到底是什么,而不是停留于表面的认知。比如当我们要使用 @Autowired 注解时,可以拿到我们想要的 bean ,但是为什么可以是值得思考的。-- 关于阅读源码

Spring源码的阅读结合日常的使用,可以帮助我们更好的掌握这个庞大的技术体系,实际的开发工作中有很多地方可以借鉴它的一些思想来帮助我们更好的实现自己的业务逻辑。

ApplicationListener 扩展

ApplicationListener 其实是 spring 事件通知机制中核心概念;在java的事件机制中,一般会有三个概念:

  • event object : 事件对象
  • event source :事件源,产生事件的地方
  • event listener :监听事件并处理

ApplicationListener 继承自 java.util.EventListener ,提供了对于 Spring 中事件机制的扩展。

ApplicationListener 在实际的业务场景中使用的非常多,比如我一般喜欢在容器初始化完成之后来做一些资源载入或者一些组件的初始化。这里的容器指的就是 Ioc 容器,对应的事件是 ContextRefreshedEvent

@Component
public class StartApplicationListener implements
ApplicationListener<ContextRefreshedEvent> {

    @Override
    public void onApplicationEvent(ContextRefreshedEvent
    contextRefreshedEvent) {
       //初始化资源文件
       //初始化组件 如:cache
    }
}
复制代码

上面这段代码会在容器刷新完成之后来做一些事情。下面通过自定义事件来看看怎么使用,在看具体的 demo 之前,先来了解下一些关注点。

日常工作了,如果要使用 Spring 事件传播机制,我们需要关注的点有以下几点:

ApplicationEvent
ApplicationListener
ApplicationContextAware
Spring

那么下面就按照这个思路来看下 demo 的具体实现。

事件类:UserRegisterEvent

UserRegisterEvent ,用户注册事件;这里作为事件对象,继承自 ApplicationEvent

/**
 * @description: 用户注册事件
 * @email: <a href="glmapper_2018@163.com"></a>
 * @author: guolei.sgl
 * @date: 18/7/25
 */
public class UserRegisterEvent extends ApplicationEvent {

    public String name;

    public UserRegisterEvent(Object o) {
        super(o);
    }

    public UserRegisterEvent(Object o, String name) {
        super(o);
        this.name=name;
    }
}
复制代码

事件发布类:UserService

用户注册服务,这里需要在用户注册时将注册事件发布出去,所以通过实现 ApplicationEventPublisherAware 接口,使 UserService 具有事件发布能力。

ApplicationEventPublisherAware:发布事件,也就是把某个事件告诉的所有与这个事件相关的监听器。

/**
 * @description: 用户注册服务,实现ApplicationEventPublisherAware接口
 ,表明本身具有事件发布能力
 * @email: <a href="glmapper_2018@163.com"></a>
 * @author: guolei.sgl
 * @date: 18/7/25
 */
public class UserService implements ApplicationEventPublisherAware {

    private ApplicationEventPublisher applicationEventPublisher;

    public void setApplicationEventPublisher(ApplicationEventPublisher
    applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    public void register(String name) {
        System.out.println("用户:" + name + " 已注册!");
        applicationEventPublisher.publishEvent(new UserRegisterEvent(name));
    }
}
复制代码

这里的 UserService 实际上是作为事件源存在的,通过 register 将用户注册事件传播出去。那么下面就是需要定义如何来监听这个事件,并且将事件进行消费处理掉,这里就是通过 ApplicationListener 来完成。

监听类:BonusServerListener

当用户触发注册操作时,向积分服务发送消息,为用户初始化积分。

/**
 * @description: BonusServerListener
 积分处理,当用户注册时,给当前用户增加初始化积分
 * @email: <a href="glmapper_2018@163.com"></a>
 * @author: guolei.sgl
 * @date: 18/7/25
 */
public class BonusServerListener implements
ApplicationListener<UserRegisterEvent> {
    public void onApplicationEvent(UserRegisterEvent event) {
        System.out.println("积分服务接到通知,给 " + event.getSource() +
        " 增加积分...");
    }
}
复制代码

注册到容器中

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
        
    <bean id="userService" class="com.glmapper.extention.UserService"/>
    <bean id="bonusServerListener"
    class="com.glmapper.extention.BonusServerListener"/>
    
</beans>
复制代码

客户端类

/**
 * @description: 客户端类
 * @email: <a href="glmapper_2018@163.com"></a>
 * @author: guolei.sgl
 * @date: 18/7/25
 */
public class MainTest {
    public static void main(String[] args) {
        ApplicationContext context =new 
        ClassPathXmlApplicationContext("beans.xml");
        UserService userService = (UserService)
        context.getBean("userService");
        //注册事件触发
        userService.register("glmapper");
    }
}
复制代码

客户端类中,注册一个 nameglmapper 的用户,执行结果:

用户:glmapper 已注册!
积分服务接到通知,给 glmapper 增加积分...
复制代码

现在来考虑另外一个问题,增加一个功能,用户注册之后给用户发一个邮件。这个其实就是增加一个监听类就可以,前提是这个监听者是监听当前事件的。

/**
 * @description: 邮件服务监听器,当监听到用户的注册行为时,
    给用户发送邮件通知
 * @email: <a href="glmapper_2018@163.com"></a>
 * @author: guolei.sgl
 * @date: 18/7/25
 */
public class EmailServerListener implements
ApplicationListener<UserRegisterEvent> {
    public void onApplicationEvent(UserRegisterEvent event) {
        System.out.println("邮件服务接到通知,给 " + event.getSource() +
        " 发送邮件...");
   
复制代码

这里如果将 UserRegisterEvent 换成 UserLoginEvent ,那么邮件服务将不会有任何行为。

增加发送邮件监听类之后的执行结果:

用户:glmapper 已注册!
邮件服务接到通知,给 glmapper 发送邮件...
积分服务接到通知,给 glmapper 增加积分...
复制代码

Spring 的事件传播机制是基于观察者模式( Observer )实现的,它可以将 Spring Bean 的改变定义为事件 ApplicationEvent ,通过 ApplicationListener 监听 ApplicationEvent 事件,一旦 Spring Bean 使用 ApplicationContext.publishEvent( ApplicationEvent event ) 发布事件后, Spring 容器会通知注册在 容器中所有 ApplicationListener 接口的实现类,最后 ApplicationListener 接口实现类判断是否处理刚发布出来的 ApplicationEvent 事件。

ApplicationContextAware 扩展

ApplicationContextAware 中只有一个 setApplicationContext 方法。实现了 ApplicationContextAware 接口的类,可以在该 Bean 被加载的过程中获取 Spring 的应用上下文 ApplicationContext ,通过 ApplicationContext 可以获取 Spring 容器内的很多信息。

这种一般在需要手动获取 Bean 的注入实例对象时会使用到。下面通过一个简单的 demo 来了解下。

GlmapperApplicationContext 持有 ApplicationContext 对象,通过实现 ApplicationContextAware 接口来给 ApplicationContext 做赋值。

/**
 * @description: GlmapperApplicationContext
 * @email: <a href="glmapper_2018@163.com"></a>
 * @author: guolei.sgl
 * @date: 18/7/29
 */
public class GlmapperApplicationContext implements
ApplicationContextAware {

    private  ApplicationContext applicationContext;
    public void setApplicationContext(ApplicationContext
    applicationContext) throws BeansException {
            this.applicationContext=applicationContext;
    }

    public ApplicationContext getApplicationContext(){
        return applicationContext;
    }
}
复制代码

需要手动获取的 bean :

/**
 * @description: HelloService
 * @email: <a href="glmapper_2018@163.com"></a>
 * @author: guolei.sgl
 * @date: 18/7/29
 */
public class HelloService {
    public void sayHello(){
        System.out.println("Hello Glmapper");
    }
}
复制代码

在配置文件中进行配置:

<bean id="helloService"
class="com.glmapper.extention.applicationcontextaware.HelloService"/>

<bean id="glmapperApplicationContext"
class="com.glmapper.extention.applicationcontextaware.GlmapperApplicationContext"/>
复制代码

客户端类调用:

public class MainTest {
    public static void main(String[] args) {
        ApplicationContext context = new
        ClassPathXmlApplicationContext("beans.xml");
        
        HelloService helloService = (HelloService)
        context.getBean("helloService");
        helloService.sayHello();

        //这里通过实现ApplicationContextAware接口的类来完成bean的获取
        GlmapperApplicationContext glmapperApplicationContext =
        (GlmapperApplicationContext) context.getBean("glmapperApplicationContext");
        
        ApplicationContext applicationContext =
        glmapperApplicationContext.getApplicationContext();
        
        HelloService glmapperHelloService = (HelloService)
        applicationContext.getBean("helloService");
        
        glmapperHelloService.sayHello();
    }
}
复制代码

BeanFactoryAware 扩展

我们知道 BeanFactory 是整个 Ioc 容器最顶层的接口,它规定了容器的基本行为。实现 BeanFactoryAware 接口就表明当前类具体 BeanFactory 的能力。

BeanFactoryAware 接口中只有一个 setBeanFactory 方法。实现了 BeanFactoryAware 接口的类,可以在该 Bean 被加载的过程中获取加载该 BeanBeanFactory ,同时也可以获取这个 BeanFactory 中加载的其它 Bean

来想一个问题,我们为什么需要通过 BeanFactorygetBean 来获取 Bean 呢?Spring已经提供了很多便捷的注入方式,那么通过 BeanFactorygetBean 来获取 Bean 有什么好处呢?来看一个场景。

现在有一个 HelloService ,这个 HelloService 就是打招呼,我们需要通过不同的语言来实现打招呼,比如用中文,用英文。一般的做法是:

public interface HelloService {
    void sayHello();
}

//英文打招呼实现
public class GlmapperHelloServiceImpl implements HelloService {
    public void sayHello() {
        System.out.println("Hello Glmapper");
    }
}

//中文打招呼实现
public class LeishuHelloServiceImpl implements HelloService {
    public void sayHello() {
        System.out.println("你好,磊叔");
    }
}
复制代码

客户端类来调用务必会出现下面的方式:

if (condition=="英文"){
    glmapperHelloService.sayHello();
}
if (condition=="中文"){
    leishuHelloService.sayHello();
}
复制代码

如果有一天,老板说我们要做国际化,要实现全球所有的语言来问候。你是说好的,还是控制不住要动手呢?

那么有没有什么方式可以动态的去决定我的客户端类到底去调用哪一种语言实现,而不是用过if-else方式来罗列呢?是的,对于这些需要动态的去获取对象的场景, BeanFactoryAware 就可以很好的搞定。OK,来看代码改造:

引入 BeanFactoryAware

/**
 * @description: 实现BeanFactoryAware ,让当前bean本身具有 BeanFactory 的能力
 *
 * 实现 BeanFactoηAware 接口的 bean 可以直接访问 Spring 容器,被容器创建以后,
 * 它会拥有一个指向 Spring
 容器的引用,可以利用该bean根据传入参数动态获取被spring工厂加载的bean
 *
 * @email: <a href="glmapper_2018@163.com"></a>
 * @author: guolei.sgl
 * @date: 18/7/29
 */
public class GlmapperBeanFactory implements BeanFactoryAware {

    private BeanFactory beanFactory;

    public void setBeanFactory(BeanFactory beanFactory) throws
    BeansException {
        this.beanFactory=beanFactory;
    }
    /**
     * 提供一个execute 方法来实现不同业务实现类的调度器方案。
     * @param beanName
     */
    public void execute(String beanName){
        HelloService helloService=(HelloService)
        beanFactory.getBean(beanName);
        helloService.sayHello();
    }

}
复制代码

这里为了逻辑方便理解,再加入一个 HelloFacade 类,这个类的作用就是持有一个 BeanFactoryAware 的实例对象,然后通过 HelloFacade 实例对象的方法来屏蔽底层 BeanFactoryAware 实例的实现细节。

public class HelloFacade {
    private GlmapperBeanFactory glmapperBeanFactory;
    //调用glmapperBeanFactory的execute方法
    public void sayHello(String beanName){
        glmapperBeanFactory.execute(beanName);
    }
    public void setGlmapperBeanFactory(GlmapperBeanFactory beanFactory){
        this.glmapperBeanFactory = beanFactory;
    }
}
复制代码

客户端类

public class MainTest {
    public static void main(String[] args) {
        ApplicationContext context = new
        ClassPathXmlApplicationContext("beans.xml");
        
        HelloFacade helloFacade = (HelloFacade)
        context.getBean("helloFacade");

        GlmapperBeanFactory glmapperBeanFactory = (GlmapperBeanFactory)
        context.getBean("glmapperBeanFactory");
        
        //这里其实可以不通过set方法注入到helloFacade中,
        //可以在helloFacade中通过autowired
        //注入;这里在使用main方法来执行验证,所以就手动set进入了
        helloFacade.setGlmapperBeanFactory(glmapperBeanFactory);

        //这个只需要传入不同HelloService的实现类的beanName,
        //就可以执行不同的业务逻辑
        helloFacade.sayHello("glmapperHelloService");
        helloFacade.sayHello("leishuHelloService");

    }
}
复制代码

可以看到在调用者(客户端)类中,只需要通过一个 beanName 就可以实现不同实现类的切换,而不是通过一堆if-else来判断。另外有的小伙伴可能会说,程序怎么知道用哪个 beanName 呢?其实这个也很简单,这个参数我们可以通过一些途径来拼接得到,比如使用一个 prefix 用来指定语言, prefix + HelloService 就可以确定唯一的 beanName

原文  https://juejin.im/post/5b7964d6f265da43412866c7
正文到此结束
Loading...