【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 ;
但个人认为,有侵入性, 不灵活; 如通过发布事件打印日志, 打印日志不应该侵入业务逻辑才对;