spring揭秘05-ApplicationContext

【README】

本文总结自《spring揭秘》,作者王福强,非常棒的一本书,墙裂推荐;

ApplicationContext 继承了 EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver 接口;

  • EnvironmentCapable:与环境变量或配置文件相关;
  • ListableBeanFactory 和 HierarchicalBeanFactory : 都是BeanFactory;
  • MessageSource:国际化(本文先不展开讨论);
  • ApplicationEventPublisher:事件发布;
  • ResourcePatternResolver: 加载资源文件;

考虑到先了解基础功能,本文仅讨论 ApplicationEventPublisher 与 ResourcePatternResolver

【ApplicationContext 】源码

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
    @Nullable
    String getId();

    String getApplicationName();

    String getDisplayName();

    long getStartupDate();

    @Nullable
    ApplicationContext getParent();

    AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;
}

【1】ApplicationContext概述

Spring有2种ioc容器,BeanFactory,ApplicationContext; 其中ApplicationContext 继承自 BeanFactory ,是高级容器,提供了更加丰富的功能;

ApplicationContext 具体实现类;

  • FileSystemXmlApplicationContext: 从文件系统加载bean及相关资源的ApplicationContext实现;
  • ClassPathXmlApplicationContext: 从Classpath加载bean定义及相关资源的ApplicationContext实现;
  • XmlWebApplicationContext: 用于web应用程序的ApplicationContext 实现;

【1.1】spring通过Resource对文件抽象

1)背景:spring通过文件配置(如xml,properties),或者注解来管理对象依赖关系;如果使用文件配置,则spring就需要读取文件;为简化代码,spring使用Resource资源接口对文件进行抽象;

2)Resource资源接口的具体实现:

  • ByteArrayResource: 字节数组资源;
  • ClassPathResource: 从java应用程序的classpath加载资源并封装;
  • FileSystemResource:对 java.io.file 进行封装;可以以文件或URL访问该类型资源;
  • UrlResource: 对 java.net.URL 进行封装;
  • InputStreamResource: 对 InputStream的封装,较少使用;

3)Resource接口定义:

public interface Resource extends InputStreamSource {
    boolean exists();

    default boolean isReadable() {
        return this.exists();
    }

    default boolean isOpen() {
        return false;
    }

    default boolean isFile() {
        return false;
    }

    URL getURL() throws IOException;

    URI getURI() throws IOException;

    File getFile() throws IOException;
...... 

【1.2】统一资源加载策略-ResourceLoader

1)背景:Resource是spring对文件的抽象;ResourceLoader是spring对资源加载(读取文件)的抽象;

public interface ResourceLoader {
    String CLASSPATH_URL_PREFIX = "classpath:";

    Resource getResource(String location);

    @Nullable
    ClassLoader getClassLoader();
}

【1.2.1】 DefaultResourceLoader

1) DefaultResourceLoader定义: 默认资源加载器; 该类默认的资源查找处理逻辑如下:

  • 检查路径是否以 classpath: 前缀开头,如果是,则尝试构造 ClassPathResource 资源;
  • 否则: 尝试通过URL, 根据资源路径来定位资源,如果没有抛出异常,则返回 UrlResource类型的资源;
  • 否则,委派 getResourceByPath(String path) 方法 来定位, 默认实现是构造 ClassPathResource类型资源并返回;

【DefaultResourceLoaderMain】 DefaultResourceLoader测试入口

public class DefaultResourceLoaderMain {
    public static void main(String[] args) {
        ResourceLoader resourceLoader = new DefaultResourceLoader();
        Resource resource = resourceLoader.getResource("D:/studynote/spring_discover/ApplicationContext.txt");
        System.out.println(resource.getClass()); 
        // class org.springframework.core.io.DefaultResourceLoader$ClassPathContextResource

        Resource fileUrlResource = resourceLoader.getResource("file:D:/studynote/spring_discover/ApplicationContext.txt");
        System.out.println(fileUrlResource.getClass()); 
        // class org.springframework.core.io.FileUrlResource

        Resource urllResource = resourceLoader.getResource("https://www.baidu.com/");
        System.out.println(urllResource.getClass()); 
        // class org.springframework.core.io.UrlResource
    }
}

【1.2.2】FileSystemResourceLoader

FileSystemResourceLoader 继承自 DefaultResourceLoader,覆写 getResourceByPath() 方法, 从文件系统加载资源并以 FileSystemResource 类型返回;

【FileSystemResourceLoaderMain】文件系统资源加载器main

public class FileSystemResourceLoaderMain {
    public static void main(String[] args) {
        ResourceLoader fileSystemResourceLoader = new FileSystemResourceLoader();
        Resource resource = fileSystemResourceLoader.getResource("D:/studynote/spring_discover/ApplicationContext.txt");
        System.out.println(resource.getClass());
        // class org.springframework.core.io.FileSystemResourceLoader$FileSystemContextResource

        Resource fileURlResource = fileSystemResourceLoader.getResource("file:D:/studynote/spring_discover/ApplicationContext.txt");
        System.out.println(fileURlResource.getClass());
        // class org.springframework.core.io.FileUrlResource
    }
}

【1.2.3】 ResourcePatternResolver批量加载资源

1)背景: ResourceLoader每次只能根据资源路径读取单个Resource实例;而 ResourcePatternResolver可以批量加载资源(一次读取多个文件)

【ResourcePatternResolver定义】

public interface ResourcePatternResolver extends ResourceLoader {
    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

    Resource[] getResources(String locationPattern) throws IOException; // 读取多个资源
}

2)PathMatchingResourcePatternResolver 是ResourcePatternResolver常用的实现类,其本身也是一个 ResourceLoader资源加载器

public class PathMatchingResourcePatternResolver implements ResourcePatternResolver {
    public PathMatchingResourcePatternResolver() {
        this.resourceLoader = new DefaultResourceLoader();
    }

    public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) {
        Assert.notNull(resourceLoader, "ResourceLoader must not be null");
        this.resourceLoader = resourceLoader;
    }

    public PathMatchingResourcePatternResolver(@Nullable ClassLoader classLoader) {
        this.resourceLoader = new DefaultResourceLoader(classLoader);
    }

PathMatchingResourcePatternResolver 支持实现 ResourLoader级别的资源加载, 支持基于ant风格的路径匹配模式(类型 **/.suffix之类的路径形式), 支持ResourcePatternResolver 新增加的 classpath: 前缀等

PathMatchingResourcePatternResolver 默认使用 DefaultResourceLoader 资源加载器读取资源;当然可以通过构造器传入 加载器给PathMatchingResourcePatternResolver ;

【ResourcePatternResolverMain】PathMatchingResourcePatternResolver 测试main

public class ResourcePatternResolverMain {
    public static void main(String[] args) throws IOException {
        ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        // getResources() 批量加载多个文件或读取多个文件
        Resource[] resources = resourcePatternResolver.getResources("file:D:/studynote/spring_discover/ApplicationContext*.txt");
        for (Resource resource : resources) {
            System.out.println(resource.getFile().getName() + ", class=" + resource.getClass());
        }
        // ApplicationContext.txt, class=class org.springframework.core.io.FileSystemResource
        // ApplicationContext2.txt, class=class org.springframework.core.io.FileSystemResource
    }
}

【1.2.4】Resource与ResourceLoader类图

在这里插入图片描述

【1.3】ApplicationContext与ResourceLoader

1)ApplicationContext定义:

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
    @Nullable
    String getId();

    String getApplicationName();

    String getDisplayName();

    long getStartupDate();

    @Nullable
    ApplicationContext getParent();

    AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;
}

2)ApplicationContext可以加载任何Resource资源 : ApplicationContext 继承 ResourcePatternResolver,也即ApplicationContext 间接继承 ResourceLoader;这就是 ApplicationContext 支持spring资源加载策略底层原理;即 ApplicationContext实现类就是 ResourceLoader(加载单个资源) 或 ResourcePatternResolver(加载多个资源), ApplicationContext本身就可以加载资源,如加载bean.xml配置文件;

3) AbstractApplicationContext 实现 ApplicationContext 接口,类图如下;

在这里插入图片描述

【1.3.1】Resource资源类型注入

1)业务场景: 发送短信需要短信模版,调用者提供对应参数即可; spring可以把短信模版文件抽象为 Resource,ApplicationContext或ClassPathXmlApplicationContext本身作为 ResourceLoader 加载短信模版文件;

【ResourceTypeInjectMain】Resource资源类型注入main

public class ResourceTypeInjectMain {
    public static void main(String[] args) throws IOException {
        ApplicationContext container =
                new ClassPathXmlApplicationContext("classpath:chapter05/beans0501resourcetypeinject.xml");
        TaskRemindShortMsgSender shortMsgSender = container.getBean("taskRemindShortMsgSender", TaskRemindShortMsgSender.class);
        shortMsgSender.send("张三");
    }
}

【beans0501resourcetypeinject.xml】

【带classpath】

<bean id="taskRemindShortMsgSender" class="com.tom.springnote.chapter05.chapter0501.TaskRemindShortMsgSender">
    <property name="template" value="classpath:chapter05/shortmsg_template.txt" />
</bean>

【不带classpath】 效果一样

<bean id="taskRemindShortMsgSender" class="com.tom.springnote.chapter05.chapter0501.TaskRemindShortMsgSender">
    <property name="template" value="chapter05/shortmsg_template.txt" />
</bean>

【shortmsg_template.txt】短信模版文件

%s 您好,您有待办任务需要处理

【TaskRemindShortMsgSender】任务提醒短信发送器

public class TaskRemindShortMsgSender {

    /** 短信模版 */
    private Resource template;

    public void send(String username) throws IOException {
        System.out.println("发送短信,内容=" + String.format(template.getContentAsString(StandardCharsets.UTF_8), username));
    }

    public Resource getTemplate() {
        return template;
    }

    public void setTemplate(Resource template) {
        this.template = template;
    }
}

【打印日志】

发送短信,内容=张三 您好,您有待办任务需要处理

【spring加载资源文件原理】

ApplicationContext启动时,会通过 ResourceEditorRegistrar(资源编辑器注册器) 来注册spring提供的针对Resource类型的ResourceEditor到容器中;

【1.3.2】ApplicationContext的Resource加载行为

1)对于URL资源: 其资源路径,通常开始都会有一个协议前缀 ,如 file: 、 htttp: 、 ftp: 等;

2) spring扩展了协议前缀的集合,包括 classpath 与 classpath*: ResourceLoader新增了资源路径协议 classpath:, ResourcePatternResolver 新增了 classpath*: ; 我们可以通过 classpath: 或 classpath*: 等协议前缀告知spring容器从classpath中加载资源;

3)ApplicationContext实现类: ClassPathXmlApplicationContext 与 FileSystemXmlApplicationContext

  • ClassPathXmlApplicationContext 默认从classpath加载资源,可以不用指定 classpath 资源协议前缀;
  • FileSystemXmlApplicationContext 默认从文件系统加载,指定classpath协议前缀,才从classpath 加载;

【ResourceTypeInjectMain】不同ApplicationContext使用classpath协议前缀加载资源对比

public class ResourceTypeInjectMain {
    public static void main(String[] args) throws IOException {
        // 使用 ClassPathXmlApplicationContext 加载配置文件(默认从classpath加载资源)
        ApplicationContext container =
                new ClassPathXmlApplicationContext("classpath:chapter05/beans0501resourcetypeinject.xml");
        TaskRemindShortMsgSender shortMsgSender = container.getBean("taskRemindShortMsgSender", TaskRemindShortMsgSender.class);
        shortMsgSender.send("张三");

        // 使用 FileSystemXmlApplicationContext 加载配置文件(注意指定classpath前缀,否则从文件系统加载,文件系统找不到,则报错)
//        ApplicationContext noClasspathContainer = new FileSystemXmlApplicationContext("chapter05/beans0501resourcetypeinject.xml");
        ApplicationContext container2 =
                new FileSystemXmlApplicationContext("classpath:chapter05/beans0501resourcetypeinject.xml");
        TaskRemindShortMsgSender shortMsgSender2 = container2.getBean("taskRemindShortMsgSender", TaskRemindShortMsgSender.class);
        shortMsgSender2.send("张三");
    }
}

【2】容器内部事件发布

【2.1】 java自定义事件发布

1)事件应用场景:

  • 状态更新:如订单状态发生变化时,可以发布一个订单状态更新事件;
  • 异步处理:如用户注册后发送欢迎邮件;
  • 监控和日志:如方法调用监控, 记录运行日志;
  • 缓存更新:如当产品价格更新时,通过事件将新的价格信息更新到缓存中;
  • 权限验证:如当用户登录系统时,发布一个登录成功事件;

2)java自定义事件发布,需要用到 EventObject 和 EventListener 接口; 所有自定义事件类型需要继承 java.util.EventObject 类, 事件监听器需要继承 java.util.EventListener接口

3)自定义事件业务场景(不用spring事件): 方法执行前后打印日志;

【MethodExecutionEvent】 自定义事件

public class MethodExecutionEvent extends EventObject {
    private String methodName;
    public MethodExecutionEvent(Object source) {
        super(source);
    }

    public MethodExecutionEvent(Object source, String methodName) {
        super(source);
        this.methodName = methodName;
    }

    public String getMethodName() {
        return methodName;
    }

    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }
}

【IMethodExecutionEventListener】事件监听器接口

public interface IMethodExecutionEventListener extends EventListener {

    void onMethodBegin(MethodExecutionEvent event);
    void onMethodEnd(MethodExecutionEvent event);
}

【MethodExecutionEventListenerImpl】监听器实现类

public class MethodExecutionEventListenerImpl implements IMethodExecutionEventListener {
    @Override
    public void onMethodBegin(MethodExecutionEvent event) {
        System.out.printf("方法%s执行开始\n", event.getMethodName());
    }

    @Override
    public void onMethodEnd(MethodExecutionEvent event) {
        System.out.printf("方法%s执行结束\n", event.getMethodName());
    }
}

【MethodExecutionEventPublisher】事件发布器

public class MethodExecutionEventPublisher {
    private List<IMethodExecutionEventListener> listenerList = new ArrayList<>();

    public void monitorMethod() {
        MethodExecutionEvent event = new MethodExecutionEvent(this, "monitorMethod");
        publishEvent(MethodExecutionStatus.BEGIN, event);
        System.out.println("执行具体业务逻辑");
        publishEvent(MethodExecutionStatus.END, event);
    }

    protected void publishEvent(MethodExecutionStatus status, MethodExecutionEvent event) {
        listenerList.forEach(listener -> {
            if (status.ifBegin()) {
                listener.onMethodBegin(event);
            } else {
                listener.onMethodEnd(event);
            }
        });
    }

    public void addListener(IMethodExecutionEventListener listener) {
        listenerList.add(listener);
    }
}

【MethodExecutionStatus】方法执行状态枚举

public enum MethodExecutionStatus {
    BEGIN,
    END;

    public boolean ifBegin() {
        return BEGIN.equals(this);
    }
}

【MethodExecutionEventPublisherMain】自定义事件发布器main

public class MethodExecutionEventPublisherMain {
    public static void main(String[] args) {
        MethodExecutionEventPublisher eventPublisher = new MethodExecutionEventPublisher();
        eventPublisher.addListener(new MethodExecutionEventListenerImpl());
        eventPublisher.monitorMethod();
    }
}

【打印日志】

方法monitorMethod执行开始
执行具体业务逻辑
方法monitorMethod执行结束

【2.2】spring容器事件发布

【2.2.1】spring容器事件发布类结构

1)ApplicationEvent: spring容器自定义事件类型,继承自 EventObject, 它是一个抽象类, 有3个实现:

  • ContextClosedEvent: spring容器关闭前发布的事件;
  • ContextRefreshedEvent: spring容器在初始化或刷新时发布的事件;
  • RequestHandledEvent: web请求处理后发布的事件;

2)ApplicationListener: spring容器自定义事件监听器接口, 继承自 EventListener;

ApplicationContext容器在启动时, 自动识别并加载 EventListener类型的bean定义, 一旦容器有事件发布,通知监听对应事件的监听器;

3)ApplicationContext:其本身就是事件发布者; 因为它继承了 ApplicationEventPublisher

  • ApplicationContext, 具体说是 AbstractApplicationContext 把事件监听器注册与事件发布逻辑,委托给 ApplicationEventMulticaster接口的实现类
  • 抽象类 AbstractApplicationEventMulticaster 实现了事件多播接口 ApplicationEventMulticaster 接口 ;
  • 具体类 SimpleApplicationEventMulticaster 继承了抽象类 AbstractApplicationEventMulticaster ;提供事件监听器管理,事件发布功能;
    • SimpleApplicationEventMulticaster 底层使用 SyncTaskExecutor 进行事件发布;

【2.2.2】spring容器事件发布代码示例

1) 事件发布者: spring容器中, 事件发布者需要调用ApplicationEventPublisher 发布事件; 而 ApplicationContext 继承了 ApplicationEventPublisher ;所以事件发布者需要实现 ApplicationEventPublisherAware接口 或 ApplicationContextAware接口; 当然, 建议实现ApplicationEventPublisherAware接口, 这样语义更加通顺;

  • 事件发布者就是我们的业务bean,即需要发布事件(如监控事件)的bean;

2)总结: 自定义spring事件发布的代码结构:

  • spring事件:继承 ApplicationEvent;
  • spring事件监听器: 实现 ApplicationListener;
  • spring事件发布者: 实现 ApplicationEventPublisherAware; 获取事件发布api ; 更确切的说,可以把事件发布者当做事件源

【BusiEventPublisherMain】事件发布者入口main

public class BusiEventPublisherMain {
    public static void main(String[] args) {
        ApplicationContext container = new ClassPathXmlApplicationContext("chapter05/beans0503springevent.xml");
        BusiSpringEventPublisher eventPublisher = container.getBean("busiSpringEventPublisher", BusiSpringEventPublisher.class);
        eventPublisher.monitorMethod();
    }
}

【beans0503springevent.xml】

<!-- 注册事件监听器 -->
<bean id="busiSpringEventListenerImpl"
      class="com.tom.springnote.chapter05.chapter0503.springevent.BusiSpringEventListenerImpl" />
<!-- 注册事件发布器 -->
<bean id="busiSpringEventPublisher"
      class="com.tom.springnote.chapter05.chapter0503.springevent.BusiSpringEventPublisher" />

【BusiSpringEvent】自定义spring事件

public class BusiSpringEvent extends ApplicationEvent {
    private String methodName;
    private MethodExecutionStatus status;

    public BusiSpringEvent(Object source, String methodName, MethodExecutionStatus status) {
        super(source);
        this.methodName = methodName;
        this.status = status;
    }

    public String getMethodName() {
        return methodName;
    }

    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }

    public MethodExecutionStatus getStatus() {
        return status;
    }

    public void setStatus(MethodExecutionStatus status) {
        this.status = status;
    }
}

【BusiSpringEventListenerImpl】事件监听器

public class BusiSpringEventListenerImpl implements ApplicationListener<BusiSpringEvent> {

    @Override
    public void onApplicationEvent(BusiSpringEvent event) {
        String nowText = BusiDatetimeUtils.timestampToDatetime(event.getTimestamp());
        if (event.getStatus().ifBegin()) {
            System.out.printf("%s 方法%s执行开始\n", nowText, event.getMethodName());
        } else {
            System.out.printf("%s 方法%s执行结束\n", nowText, event.getMethodName());
        }
    }
    @Override
    public boolean supportsAsyncExecution() {
        return ApplicationListener.super.supportsAsyncExecution();
    }
}

【BusiSpringEventPublisher】事件发布者 (事件源)

public class BusiSpringEventPublisher implements ApplicationEventPublisherAware {

    private ApplicationEventPublisher eventPublisher;

    public void monitorMethod() {
        BusiSpringEvent beginEvent = new BusiSpringEvent(this, "monitorMethod", MethodExecutionStatus.BEGIN);
        this.eventPublisher.publishEvent(beginEvent); // 手动发布事件
        System.out.println("执行具体业务逻辑");
        BusiSpringEvent endEvent = new BusiSpringEvent(this, "monitorMethod", MethodExecutionStatus.BEGIN);
        this.eventPublisher.publishEvent(endEvent); // 手动发布事件 
    }

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

【打印日志】

2024-08-15 16:55:15 方法monitorMethod执行开始
执行具体业务逻辑
2024-08-15 16:55:15 方法monitorMethod执行开始

【2.2.3】个人见解

上述代码示例中, 执行具体业务逻辑前后分别手动发布事件, 这类似于手工实现aop编程,不是真正的aop

但个人认为,有侵入性, 不灵活; 如通过发布事件打印日志, 打印日志不应该侵入业务逻辑才对;



  • 13
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值