SpringFramework事件与监听机制(监听器)

2023-11-18

SpringBoot版本:2.0.2.RELEASE
SpringFramework版本:5.0.6.RELEASE


SpringFramework的监听者这一块设计得也是挺有意思的。监听者的核心设计思想就是通知者模式。随着SpringFramework版本的不断演变,整个事件与监听机制都在不断的优化。监听者可以仅接受自己感兴趣的事件。SpringBoot是基于SpringFramework发展而来的,它的监听者也是融入到SpringFramework的机制里。
在这里插入图片描述
上图是SpringFramework监听者接口方面的UML图。除了EventListener接口外,ApplicationListener、GenericApplicationListener和SmartApplicationListener接口都有自己的实现类。那么,划分不同的接口背后想表达什么意思呢?后面我们逐步解开这个迷。

监听者从何而来

在SpringBoot的工程里,SpringFramework的监听者有两部分来源,一些SpringBoot带来的,别一些是SpringFramework原生。

来自SpringBoot的监听器

在《SpringFramework事件与监听机制(事件)》文章里,我们看到在SpringBoot的启停过程中,SpringBoot的事件是最早发布的,到了中间的部分才发布ContextRefreshedEvent。那么,是不是SpringBoot的事件发布给SpringBoot的监听者,Spring的事件发布给Spring的监听者呢?我们从代码的层面来观察。
根据《SpringBoot事件与监听机制》,SpringBoot通过EventPublishingRunListener发布事件,我们把目光放在EventPublishingRunListener的几个发布事件的方法。它的方法的顺序跟SpringBoot发布各阶段事件的顺序是一致的。
从代码看,SpringBoot发布ApplicationStartingEvent和ApplicationEnvironmentPreparedEvent都有着相同的逻辑:

@Override
	public void starting() {
		this.initialMulticaster.multicastEvent(
				new ApplicationStartingEvent(this.application, this.args));
	}

	@Override
	public void environmentPrepared(ConfigurableEnvironment environment) {
		this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(
				this.application, this.args, environment));
	}

在发布ApplicationPreparedEvent的contextLoaded方法就有些不一样了:

@Override
	public void contextLoaded(ConfigurableApplicationContext context) {
		for (ApplicationListener<?> listener : this.application.getListeners()) {
			if (listener instanceof ApplicationContextAware) {
				((ApplicationContextAware) listener).setApplicationContext(context);
			}
			context.addApplicationListener(listener);
		}
		this.initialMulticaster.multicastEvent(
				new ApplicationPreparedEvent(this.application, this.args, context));
	}

它会先将SpringBoot的监听器加到SpringFramework的context里。然后才发布ApplicationPreparedEvent事件。

后面的ApplicationStartedEvent和ApplicationReadyEvent都是通过SpringFramework的ConfigurableApplicationContext来发布。

@Override
	public void started(ConfigurableApplicationContext context) {
		context.publishEvent(
				new ApplicationStartedEvent(this.application, this.args, context));
	}

	@Override
	public void running(ConfigurableApplicationContext context) {
		context.publishEvent(
				new ApplicationReadyEvent(this.application, this.args, context));
	}

在这里,我们至少看到一个发布者身份转换的过程。而转换的准备阶段就在发布ApplicationPreparedEvent的时候,此时将SpringBoot的监听器都“告诉”SpringFramework的ConfigurableApplicationContext了。用“告诉”这词,因为SpringBoot自己仍然知道监听者的地址,而不是说交给SpringFramework后自己就完全不管了。

我们得往回看下代码,在发布ApplicationPreparedEvent的时候是处于什么阶段。SpringApplication#run方法代码片段如下:

public ConfigurableApplicationContext run(String... args) {
....
	context = createApplicationContext();
	exceptionReporters = getSpringFactoriesInstances(
			SpringBootExceptionReporter.class,
			new Class[] { ConfigurableApplicationContext.class }, context);
	prepareContext(context, environment, listeners, applicationArguments,
			printedBanner);
	refreshContext(context);
....

prepareContext方法里就会发布ApplicationPreparedEvent事件。从方法的名字来看,就是为context做准备。而这方法的上两条命令ConfigurableApplicationContext才被创建。这方法后面紧跟着就是refreshContext方法,在该方法里,最终是由SpringFramework的ConfigurableApplicationContext发布ContextRefreshedEvent事件。也就是说,在context的准备阶段会做一些事情并且知到了SpringBoot的监听器,将它们放进自己的库里。后续ConfigurableApplicationContext发布的事件也会被SpringBoot的监听者监听到。
SpringBoot后面发布ApplicationStartedEvent和ApplicationReadyEvent的事件虽然表面上通过SpringApplicationRunListeners发布,实质上最终由ConfigurableApplicationContext发布。简单地说,在发布ApplicationPreparedEvent时ConfigurableApplicationContext还未成熟,所以此时没有让它发布事件。到了ContextRefreshedEvent时ConfigurableApplicationContext已经作好了准备(ConfigurableApplicationContext#refresh方法已完成),所以就让它来发布。后来的事件也都由ConfigurableApplicationContext来发布了。
上面的论述有点啰嗦,但结论就是并非一直是SpringBoot发布的事件就只有SpringBoot的监听者收到,SpringFramework发布的事件就只有SpringFramework的监听者收到。事实上是从ContextRefreshedEvent事件开始,ConfigurableApplicationContext发布的事件也能够被SpringBoot的监听者收到。

来自SpringFramework的监听器

AbstractApplicationContext,是众多的ApplicationContext子类的父类,而它实现了ConfigurableApplicationContext接口,接口的refresh方法在AbstractApplicationContext已实现,如果子类没重载就继承了它的refresh方法。目前看SpringFramework的其他Context子类没重载这条方法。
在AbstractApplicationContext#refresh方法有prepareBeanFactory(beanFactory)这一条命令,我们再追踪该命令:

protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
....
// Register early post-processor for detecting inner beans as ApplicationListeners.
		beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));
....

ApplicationListenerDetector实现了DestructionAwareBeanPostProcessor接口,而这接口又继承了BeanPostProcessor接口,因此ApplicationListenerDetector需要实现这些接口声明的方法。而这些接口的方法是由SpringFramework的BeanFactory回调的。

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
	if (bean instanceof ApplicationListener) {
		// potentially not detected as a listener by getBeanNamesForType retrieval
		Boolean flag = this.singletonNames.get(beanName);
		if (Boolean.TRUE.equals(flag)) {
			// singleton bean (top-level or inner): register on the fly
			this.applicationContext.addApplicationListener((ApplicationListener<?>) bean);
		}
....

通过BeanPostProcessor#postProcessAfterInitialization的注释大概知道这方法被回调的时机是SpringFramework容器内的一些Bean刚被初始化的时候。不管怎么说,当程序运行到此时ConfigurableApplicationContext的实现类已经被创建了,所以通过其addApplicationListener方法添加那些实现了ApplicationListener接口的类的对象。我们再回顾下调用顺序:

SpringApplication AbstractApplicationContext ConfigurableListableBeanFactory run refreshContext refresh refresh prepareBeanFactory addBeanPostProcessor(new ApplicationListenerDetector(this) SpringApplication AbstractApplicationContext ConfigurableListableBeanFactory

监听器监听的事件

监听器实现的接口

监听器沿用了通知者模式,而且它进一步地控制着监听者所能接受事件的范围。监听器的UML类图如下:
在这里插入图片描述ApplicationListener是继承于java原生的EventListener接口,并且ApplicationListener使用了泛型--ApplicationEvent,而ApplicationEvent本身也是接口。这至少说明ApplicationListener接口的实现类需要指定ApplicationEvent的实现类型。其目的就是在类定义的层面确定监听器与事件的关系。另外,图中有一些指向接口的箭头,箭头的另一端代表该接口的实现类,它们有的是作为ApplicationListener的直接实现者,有的是实现了ApplicationListener的某一个子接口或者两个子接口。接下来,我们将探讨泛型是如何实现消息过滤的作用,以及实现ApplicationListener接口和实现该接口的子类会有什么不同。

本部分与《SpringBoot事件与监听机制》的“核心的事件发布者”部分相关。在该文章里我曾称SimpleApplicationEventMulticaster为核心的事件发布器。当它要发布事件时,会获取事件类型和事件源类型相匹配的Listener,然后向这些Listener发布事件。SimpleApplicationEventMulticaster#multicastEvent(ApplicationEvent) 方法是发布事件的入口方法 ,跟踪调用链,我们会来到AbstractApplicationEventMulticaster#getApplicationListeners(
ApplicationEvent , ResolvableType )。该方法会调用retrieveApplicationListeners函,该函数更核心的调用命令是supportsEvent,该方法用于判断指定的Listener是否与事件类型以及事件源类型相匹配:

protected boolean supportsEvent(
			ApplicationListener<?> listener, ResolvableType eventType, @Nullable Class<?> sourceType) {

		GenericApplicationListener smartListener = (listener instanceof GenericApplicationListener ?
				(GenericApplicationListener) listener : new GenericApplicationListenerAdapter(listener));
		return (smartListener.supportsEventType(eventType) && smartListener.supportsSourceType(sourceType));
	}

在此我们先列出这些接口实现类的代表,后续我们分别将它们代入这条函数,观察supportsEvent最终会得到什么样的结果:

实现类 实现的接口
AnsiOutputApplicationListener ApplicationListener
ClasspathLoggingApplicationListener GenericApplicationListener
GenericApplicationListenerAdapter GenericApplicationListener,SmartApplicationListener
SourceFilteringListener GenericApplicationListener,SmartApplicationListener
ConfigFileApplicationListener SmartApplicationListener

我们可以先初步观察这条函数的语义:
首先,判断指定的Listener是否实现了GenericApplicationListener,如果是则转换成GenericApplicationListener类型;如果不是,该Listener由GenericApplicationListenerAdapter进行封装。而GenericApplicationListenerAdapter实现了GenericApplicationListener接口和SmartApplicationListener接口。也就是说直接实现了ApplicationListener接口或者SmartApplicationListener接口的Listener都被GenericApplicationListenerAdapter进行封装。GenericApplicationListenerAdapter其实就是运用了适配器模式,将不是GenericApplicationListener接口的Listener适配成GenericApplicationListener接口的实现,目的是后续统一调用。
然后,经过上一步的处理后,以GenericApplicationListener接口的实现类的角度,通过调用接口声明的supportsEventType方法和supportsSourceType方法来得到Listener是否与事件的类型以及事件源类型匹配的结果。

GenericApplicationListenerAdapter的定义

public GenericApplicationListenerAdapter(ApplicationListener<?> delegate) {
		Assert.notNull(delegate, "Delegate listener must not be null");
		this.delegate = (ApplicationListener<ApplicationEvent>) delegate;
		this.declaredEventType = resolveDeclaredEventType(this.delegate);
	}

在构造函数里,除了将被适配的Listener作为delegate属性外,还计算出该Listener的泛型(即支持的事件类型),并保存到declaredEventType属性。构造函数里调用的私有方法resolveDeclaredEventType会调用域为包范围的同名函数:

@Nullable
	static ResolvableType resolveDeclaredEventType(Class<?> listenerType) {
		ResolvableType resolvableType = ResolvableType.forClass(listenerType).as(ApplicationListener.class);
		return (resolvableType.hasGenerics() ? resolvableType.getGeneric() : null);
	}

因为它的作用域为包范围,所以我们可以在自己的测试工程里创建跟它一样的包,然后添加测试类来试试这条函数。
接下来看两条重要的函数:

@Override
public boolean supportsEventType(ResolvableType eventType) {
		if (this.delegate instanceof SmartApplicationListener) {
			Class<? extends ApplicationEvent> eventClass = (Class<? extends ApplicationEvent>) eventType.resolve();
			return (eventClass != null && ((SmartApplicationListener) this.delegate).supportsEventType(eventClass));
		}
		else {
			return (this.declaredEventType == null || this.declaredEventType.isAssignableFrom(eventType));
		}
	}

@Override
	public boolean supportsSourceType(@Nullable Class<?> sourceType) {
		return !(this.delegate instanceof SmartApplicationListener) ||
				((SmartApplicationListener) this.delegate).supportsSourceType(sourceType);
	}

下一节,我们分别将实现不同接口的Listener 代表代入AbstractApplicationEventMulticaster#supportsEvent(ApplicationListener<?>, ResolvableType , @Nullable Class<?> )方法进行观察。

实现ApplicationListener接口的Listener

我们以AnsiOutputApplicationListener为代表进行观察。在AbstractApplicationEventMulticaster#supportsEvent函数里,因为它不是GenericApplicationListener的实现类,所以它被GenericApplicationListenerAdapter封装。

  • 在执行GenericApplicationListenerAdapter#supportsEventType方法时,因为它并不实现SmartApplicationListener接口,所以就用它的泛型与事件类型作比较。
public class AnsiOutputApplicationListener
		implements ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered 

如果事件类型是它支持的ApplicationEnvironmentPreparedEvent一样或者是它的子类,那么此时就返回true,否则返回false。

  • 在执行GenericApplicationListenerAdapter#supportsSourceType函数里,因为它不是SmartApplicationListener接口的实现类,所以直接返回true。
    所以,对于直接实现ApplicationListener接口的Listener,主要考查的是它声明的泛型与事件的类型是否匹配即可。只要事件类型与声明的泛型一样或者是泛型的子类级别,都算是匹配,这种情况下AbstractApplicationEventMulticaster#supportsEvent函数返回true。

实现GenericApplicationListener接口的Listener

我们以ClasspathLoggingApplicationListener为代表进行观察。在AbstractApplicationEventMulticaster#supportsEvent函数里,因为它GenericApplicationListener的实现类,所以只需转换为GenericApplicationListener类型。
ClasspathLoggingApplicationListener#supportsEventType方法如下:

@Override
	public boolean supportsEventType(ResolvableType resolvableType) {
		Class<?> type = resolvableType.getRawClass();
		if (type == null) {
			return false;
		}
		return ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(type)
				|| ApplicationFailedEvent.class.isAssignableFrom(type);
	}

只要事件类型为ApplicationEnvironmentPreparedEvent或者ApplicationFailedEvent及他们的子类即为匹配。
ClasspathLoggingApplicationListener#supportsSourceType方法如下:

@Override
	public boolean supportsSourceType(Class<?> sourceType) {
		return true;
	}

直接就返回ture。
所以对于ClasspathLoggingApplicationListener来说,AbstractApplicationEventMulticaster#supportsEvent函数是否返回真值取决于事件是否为ApplicationEnvironmentPreparedEvent或者ApplicationFailedEvent及他们的子类,如果是则为true,否则就false。
通过以上的分析可以让我们有所启发,如果某个实现了GenericApplicationListener接口的Listener,关键在于supportsEventType方法的实现。这种方式的灵活性在于可以让Listener不再限于单个事件类型,是否需要匹配事件源类型貌似不太重要。

实现GenericApplicationListener接口和SmartApplicationListener接口的Listener

我们以SourceFilteringListener为代表进行观察。在AbstractApplicationEventMulticaster#supportsEvent函数里,因为它是GenericApplicationListener的实现类,所以不需要经过GenericApplicationListenerAdapter的封装。
SourceFilteringListener稍有些特别,它同样有适配器的影子。它的构造函数如下:

public SourceFilteringListener(Object source, ApplicationListener<?> delegate) {
		this.source = source;
		this.delegate = (delegate instanceof GenericApplicationListener ?
				(GenericApplicationListener) delegate : new GenericApplicationListenerAdapter(delegate));
	}
protected SourceFilteringListener(Object source) {
		this.source = source;
	}

我们关注第一条构造函数,因为它最为复杂而且作用域为public。可以看出,它的作用与GenericApplicationListenerAdapter相似。
它的supportsEventType和supportsSourceType方法如下:

@Override
	public boolean supportsEventType(ResolvableType eventType) {
		return (this.delegate == null || this.delegate.supportsEventType(eventType));
	}

	@Override
	public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
		return supportsEventType(ResolvableType.forType(eventType));
	}

	@Override
	public boolean supportsSourceType(@Nullable Class<?> sourceType) {
		return (sourceType != null && sourceType.isInstance(this.source));
	}

supportsEventType返回值取决于被代理对象的supportsEventType返回值,而supportsSourceType取决于自己支持的事件源类型是否为作为参数的事件源类型或者父类型。
虽然SourceFilteringListener的构造函数有一些特殊,但我们依然可以得出此结论,实现了GenericApplicationListener接口和SmartApplicationListener接口的Listener可以匹配多个事件,是否需要匹配事件源类型看具体需求。总之从接口的设计上是区分了事件类型以及事件源类型的判断。

实现SmartApplicationListener接口的Listener

我们以ConfigFileApplicationListener为代表进行观察。在AbstractApplicationEventMulticaster#supportsEvent函数里,因为它不是GenericApplicationListener的实现类,所以需要进行适配。
它的supportsEventType和supportsSourceType方法如下:

@Override
	public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
		return ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventType)
				|| ApplicationPreparedEvent.class.isAssignableFrom(eventType);
	}

	@Override
	public boolean supportsSourceType(Class<?> aClass) {
		return true;
	}

supportsEventType返回值取决于事件是否为ApplicationEnvironmentPreparedEvent和ApplicationPreparedEvent及其子类,而supportsSourceType直接返回true。
对以上4个具体的事件监听器的分析,我们可以得到如下结论:

直接实现的接口 依赖于泛型 可匹配的事件
ApplicationListener
GenericApplicationListener 多个
GenericApplicationListener和SmartApplicationListener 多个
SmartApplicationListener 多个
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

SpringFramework事件与监听机制(监听器) 的相关文章

  • 内容多,鼠标略过显示内容

    格式化单元格提示信息 function formatCellTooltip value return span title value span th 异常类型 th
  • knime工具介绍(1)

    本文旨在介绍knime在数据分析中可具体扮演的角色 安利给大家这个超好用数据分析工具 截图部分转自亚洲数析协会公开课截图 如有侵权请及时私信处理 因为内容比较多 先慢慢更新 未完待续9 14 一 数据分析的全流程均可以用到这个工具 台湾数析
  • 解决:修改JAVA_HOME后,Java版本无法正常切换

    经验总结 步骤1 检查路径是否正确 步骤2 将JAVA HOME配置到path最前面 步骤3 删除 C ProgramData Oracle Java javapath 目录下三个 exe 文件 步骤4 重新测试是否 可正常切换Java 版
  • 软件测试从自学到工作,软件测试学习到底要怎样进行?

    前言 首先 请不要奢望有多么简单的办法 学习没有捷径 这里只是让你明白这一点 顺便根据个人经验帮你理一下学习的过程 其实有文章是说怎么学习以及学习什么的 但是可能还是有些抽象 或者内容有点多 有点杂 以至于不少朋友仍然觉得不知道如何下手 大
  • R语言描述性统计

    使用Hmisc这个包 只需要调用 my data read csv test csv Hmisc describe my data 可以打印出各个变量的均值方差等信息
  • mysql远程连接权限grant all privileges on *.* to ‘root‘@‘%‘ identified by ‘123456‘ with grant option语句报错

    mysql远程连接权限grant all privileges on to root identified by 123456 with grant option语句报错 记录一下自己安装mysql遇到的小坑 grant all privi
  • Integer中缓存池讲解

    文章目录 一 简介 二 实现原理 三 修改缓存范围 一 简介 Integer缓存池是一种优化技术 用于提高整数对象的重用和性能 在Java中 对于整数值在 128 到 127 之间的整数对象 会被放入缓存池中 以便重复使用 这是因为在这个范
  • Centos7操作系统服务器优化流程(关闭防火墙、关闭selinux、更换yum源、安装Docker和docker-compose)

    Centos7 测试环境服务器优化流程 本文讲解内容 将Centos7操作系统作为公司开发环境或者自学者搭建DevOps流程而优化的几项内容 生产环境慎用 防止被网络攻击 纯干货教程 已在本地操作多次 请放心使用 推荐一个笔者长期使用的ss
  • 卡西欧casio手表质量怎么样

    Casio的仿货 淘宝在300以上的质量都还可以 500以上手感就挺好了 我买了一个4折的 没问题 绝对真货 有真货单的 带激光防伪标 好像是广东出的 就是没发票 不过店家保一年 但我觉得casio的质量还是可以的 一年内不会有问题 1年后

随机推荐

  • Jupyter 配置默认工作目录(起始位置)

    没有配置文件 1 安装了 Anaconda 在Anaconda prompt中输入以下命令 也可以用来查找已有配置文件路径 jupyter lab jupyter lab generate config jupyter notebook j
  • OVP保护芯片首选ETA7008,耐压36V,过压保护点可调

    产品描述主要特点 低成本 过压保护点可调 高耐压 低内阻 快速响应ETA7008是一款低侧过压保护 OVP IC 仅具有34mohm开关电阻 确保非常低的导通电阻和高保护电压 负端保护 耐压36V 过压保护点可设 导通内阻小 可蕞大过4A电
  • clang-format configurator - 交互式创建 clang-format 格式配置文件

    clang format configurator 交互式创建 clang format 格式配置文件 clang format configurator https zed0 co uk clang format configurator
  • Apache APISIX 默认密钥漏洞(CVE-2020-13945)

    Vulhub Apache APISIX 默认密钥漏洞 CVE 2020 13945 文章目录 Vulhub Apache APISIX 默认密钥漏洞 CVE 2020 13945 APISIX简介 漏洞复现 payload分析 APISI
  • PCB板框文件丢失的问题

    问题 PCB 板框文件丢失的问题 在制作好PCB并导出Gerber文件后 送厂制板的时候审查被提醒说没有边框文件 缺少 GM1 层 解决办法 经过反复检查 确定添加了边框文件 BOARD GEOMETRY CUT Design outlin
  • Spark Job写文件个数的控制以及小文件合并的一个优化

    文章目录 背景说明 通过引入额外Shuffle对写入数据进行合并 EnsureRepartitionForWriting Rule CoalesceShufflePartitions Rule OptimizeShuffleWithLoca
  • xdoj单词排序

    标题 单词排序 描述 定义一个二维字符数组str 10 20 行号表示单词序号 列号表示单词最大长度 输入一个正整数N N 10 表示单词数 使用函数wd sort 完成单词的排序 按字母顺序从小到大排列单词 使用指针完成地址传递 主函数完
  • 常用加密解密算法【RSA、AES、DES、MD5】介绍和使用

    为了防止我们的数据泄露 我们往往会对数据进行加密 特别是敏感数据 我们要求的安全性更高 下面将介绍几种常用的加密算法使用 这些算法的加密对象都是基于二进制数据 如果要加密字符串就使用统一编码 如 utf8 进行编码后加密 1 摘要算法 常用
  • Java FileReader读取文件

    import java io FileReader import java io IOException public class FileReaderCls public static void main String args read
  • Java基础——GUI——Swing中常用容器和组件

    1 swing中常用容器 1 JFrame 常用方法 1 构造方法 2 设置窗体可见 3 设置点击窗体的执行的操作 4 设置窗体的大小和位置 等价于上面两个方法 不管窗体多大 窗体运行起来都会出现在屏幕的中心 5 获取窗体容器 在容器中添加
  • 断点续传和多线程下载

    断点续传和多线程下载 HTTP是通过在Header里两个参数实现的 客户端发请求时对应的是Range 服务器端响应时对应的是Content Range Range 客户端发请求的范围 Content Range 服务端返回当前请求范围和文件
  • fadeOut、fadeIn

    p This is a paragraph p
  • 《Python 黑帽子》学习笔记 - 准备 - Day 1

    信息安全是一个有意思的方向 也是自己的爱好 从零开始 想在工作之余把这个爱好培养为自己的技术能力 而 web 安全相对来说容易入门些 于是选择 web 渗透测试作为学习的起点 并选择同样是容易入门的 Python 作为编程工具 潜心学习 持
  • 卡尔曼滤波算法 C语言实现 示例

    1 概念 卡尔曼滤波 Kalman filtering 是一种利用 k时刻 状态预测值 先验估计值 k 1时刻 状态最优估计值 后验估计值 k时刻 状态预测协方差 先验预测协方差 真实值与预测值之间的协方差 k时刻 状态最优估计协方差 后验
  • C# 字符串去掉括号和括号里面的内容

    using System Text RegularExpressions var majorname 考古学 清华大学 Replace Replace
  • 快速排序和归并排序的相同点和不同点(JAVA)

    首先我们贴出来快速排序的代码 public class QuickSort public int QuickSort int a int left int right int temp a left while left lt right
  • 小程序(十六)消息功能

    文章目录 一 数据库设计 二 系统消息的发送与收取设计 三 业务设计 四 消息实体设计 五 SpringBoot异步任务 1 开启异步注解功能 2 线程池创建 3 异步任务 发送消息 同步获取消息 4 controller 一 数据库设计
  • 字体样式的CSS表示大全

    https blog csdn net kellogg and nina article details 78349654 华文细黑 STHeiti Light STXihei 华文黑体 STHeiti 华文楷体 STKaiti 华文宋体
  • idea中创建xml,xml中路径报错

    1 先创建模板 2 新建xml 3 下面的路径可能会报红 4 解决方法
  • SpringFramework事件与监听机制(监听器)

    SpringBoot版本 2 0 2 RELEASE SpringFramework版本 RELEASE 文章目录 监听者从何而来 来自SpringBoot的监听器 来自SpringFramework的监听器 监听器监听的事件 监听器实现的