Spring Boot事件机制浅析

1、概述

在设计模式中,观察者模式是一个比较常用的设计模式。维基百科解释如下:

 观察者模式是软件设计模式的一种。在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。

在我们日常业务开发中,观察者模式对我们很大的一个作用,在于实现业务的解耦、传参等。以用户注册的场景来举例子,假设在用户注册完成时,需要给该用户发送邮件、发送优惠劵等等操作,如下图所示:

图片

图片

  • UserService 在完成自身的用户注册逻辑之后,仅仅只需要发布一个 UserRegisterEvent 事件,而无需关注其它拓展逻辑。

  • 其它 Service 可以自己订阅UserRegisterEvent 事件,实现自定义的拓展逻辑。

注意:发布订阅模式属于广义上的观察者模式

在观察者模式中,观察者需要直接订阅目标事件;在目标发出内容改变的事件后,直接接收事件并作出响应

 ╭─────────────╮  Fire Event  ╭──────────────╮│             │─────────────>│              ││   Subject   │              │   Observer   ││             │<─────────────│              │╰─────────────╯  Subscribe   ╰──────────────╯

在发布订阅模式中,发布者和订阅者之间多了一个发布通道;一方面从发布者接收事件,另一方面向订阅者发布事件;订阅者需要从事件通道订阅事件,以此避免发布者和订阅者之间产生依赖关系

 ╭─────────────╮                 ╭───────────────╮   Fire Event   ╭──────────────╮│             │  Publish Event  │               │───────────────>│              ││  Publisher  │────────────────>│ Event Channel │                │  Subscriber  ││             │                 │               │<───────────────│              │╰─────────────╯                 ╰───────────────╯    Subscribe   ╰──────────────╯

简单来说,发布订阅模式属于广义上的观察者模式,在观察者模式的 Subject 和 Observer 的基础上,引入 Event Channel 这个中介,进一步解耦。

2、事件模式中的概念

  • 事件源:事件的触发者,比如注册用户信息,入库,发布“用户XX注册成功”。

  • 事件:描述发生了什么事情的对象,比如:XX注册成功的事件

  • 事件监听器:监听到事件发生的时候,做一些处理,比如 注册成功后发送邮件、赠送积分、发优惠券…

3、spring事件使用步骤

  • 定义事件

    自定义事件,需要继承ApplicationEvent类,实现自定义事件。另外,通过它的 source 属性可以获取事件源,timestamp 属性可以获得发生时间。

  • 定义监听器

    自定义事件监听器,需要实现ApplicationListener接口,实现onApplicationEvent方法,处理感兴趣的事件

  • 创建事件广播器

    创建事件广播器实现ApplicationEventMulticaster接口,也可以使用spring定义好的SimpleApplicationEventMulticaster:

    ApplicationEventMulticaster applicationEventMulticaster = new SimpleApplicationEventMulticaster();
  • 向广播器中注册事件监听器

    将事件监听器注册到广播器ApplicationEventMulticaster中,

    applicationEventMulticaster.addApplicationListener(new SendEmailOnOrderCreaterListener());
  • 通过广播器发布事件

    广播事件,调用ApplicationEventMulticaster#multicastEvent方法广播事件,此时广播器中对这个事件感兴趣的监听器会处理这个事件。

    applicationEventMulticaster.multicastEvent(new OrderCreateEvent(applicationEventMulticaster, 1L));

4、使用方式

4.1 面向接口的方式

案例:实现用户注册成功后发布事件,然后在监听器中发送邮件的功能。

用户注册事件:

创建 UserRegisterEvent事件类,继承 ApplicationEvent 类,用户注册事件。代码如下:

public class UserRegistryEvent extends ApplicationEvent {private String userName;public UserRegistryEvent(Object source, String userName) {super(source);this.userName = userName;}public String getUserName() {return userName;}
}

发送邮件监听器:

创建 SendEmailListener 类,邮箱 Service。代码如下:

@Component
public class SendEmailListener implements ApplicationListener<UserRegistryEvent> {Logger LOGGER = LoggerFactory.getLogger(SendEmailListener.class);@Overridepublic void onApplicationEvent(UserRegistryEvent event) {LOGGER.info("给用户{}发送注册成功邮件!", event.getUserName());}
}

注意:

  • 实现 ApplicationListener 接口,通过 E 泛型设置感兴趣的事件,如UserRegistryEvent;

  • 实现 #onApplicationEvent(E event) 方法,针对监听的 UserRegisterEvent 事件,进行自定义处理。

用户注册服务:注册功能+发布用户注册事件

创建UserRegisterService 类,用户 Service。代码如下:

@Service
@Slf4j
public class UserRegisterService implements ApplicationEventPublisherAware {private ApplicationEventPublisher applicationEventPublisher;@Overridepublic void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {this.applicationEventPublisher = applicationEventPublisher;}public void registryUser(String userName) {// 用户注册(入库等)log.info("用户{}注册成功", userName);applicationEventPublisher.publishEvent(new UserRegistryEvent(this, userName));//applicationEventPublisher.publishEvent(event);}}

注意:

  • 上面实现了ApplicationEventPublisherAware接口,spring容器会通过setApplicationEventPublisher将ApplicationEventPublisher注入进来,然后我们就可以使用这个来发布事件了;

  • 在执行完注册逻辑后,调用 ApplicationEventPublisher 的 [#publishEvent(ApplicationEvent event)]方法,发布[UserRegisterEvent]事件

调用:

@RestController
public class SpringEventController {@Autowiredprivate UserRegisterService userRegisterService;@GetMapping("test-spring-event")public Object test(String name){LocalDateTime dateTime = LocalDateTime.now();userRegisterService.registryUser(name);return dateTime.toString() + ":spring";}}

运行 http://localhost:12000/server/test-spring-event?name=name1

输出:

用户name1注册成功
给用户name1发送注册成功邮件!

原理:
spring容器在创建bean的过程中,会判断bean是否为ApplicationListener类型,进而会将其作为监听器注册到AbstractApplicationContext#applicationEventMulticaster中,

AbstractApplicationContext.java -》ApplicationEventPublisher@Overridepublic void addApplicationListener(ApplicationListener<?> listener) {Assert.notNull(listener, "ApplicationListener must not be null");if (this.applicationEventMulticaster != null) {this.applicationEventMulticaster.addApplicationListener(listener); // 广播器中添加监听器}this.applicationListeners.add(listener);}// 发布事件protected void publishEvent(Object event, @Nullable ResolvableType eventType) {Assert.notNull(event, "Event must not be null");// Decorate event as an ApplicationEvent if necessaryApplicationEvent applicationEvent;if (event instanceof ApplicationEvent) {applicationEvent = (ApplicationEvent) event;}else {applicationEvent = new PayloadApplicationEvent<>(this, event);if (eventType == null) {eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();}}// Multicast right now if possible - or lazily once the multicaster is initializedif (this.earlyApplicationEvents != null) {this.earlyApplicationEvents.add(applicationEvent);}else {getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);}// Publish event via parent context as well...if (this.parent != null) {if (this.parent instanceof AbstractApplicationContext) {((AbstractApplicationContext) this.parent).publishEvent(event, eventType);}else {this.parent.publishEvent(event);}}}

这块的源码在下面这个方法中,

org.springframework.context.support.ApplicationListenerDetector#postProcessAfterInitialization

@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) {if (bean instanceof ApplicationListener) {// potentially not detected as a listener by getBeanNamesForType retrievalBoolean flag = this.singletonNames.get(beanName);if (Boolean.TRUE.equals(flag)) {// singleton bean (top-level or inner): register on the flythis.applicationContext.addApplicationListener((ApplicationListener<?>) bean);}else if (Boolean.FALSE.equals(flag)) {if (logger.isWarnEnabled() && !this.applicationContext.containsBean(beanName)) {// inner bean with other scope - can't reliably process eventslogger.warn("Inner bean '" + beanName + "' implements ApplicationListener interface " +"but is not reachable for event multicasting by its containing ApplicationContext " +"because it does not have singleton scope. Only top-level listener beans are allowed " +"to be of non-singleton scope.");}this.singletonNames.remove(beanName);}}return bean;}

4.2 面向@EventListener注解的方式

可以通过 condition 属性指定一个SpEL表达式,如果返回 “true”, “on”, “yes”, or “1” 中的任意一个,则事件会被处理,否则不会。

  	@EventListener(condition = "#userRegistryEvent.userName eq 'name2'")public void getCustomEvent(UserRegistryEvent userRegistryEvent) {LOGGER.info("EventListener 给用户{}发送注册邮件成功!", userRegistryEvent.getUserName());}

运行http://localhost:12000/server/test-spring-event?name=name1

输出:

用户name1注册成功
给用户name1发送注册成功邮件!

运行http://localhost:12000/server/test-spring-event?name=name2

输出:

用户name2注册成功
给用户name2发送注册成功邮件!
EventListener 给用户name2发送注册邮件成功!

原理:

EventListenerMethodProcessor实现了SmartInitializingSingleton接口,SmartInitializingSingleton接口中的afterSingletonsInstantiated方法会在所有单例的bean创建完成之后被spring容器调用。spring中处理@EventListener注解源码位于下面的方法中

org.springframework.context.event.EventListenerMethodProcessor#afterSingletonsInstantiated

public class EventListenerMethodProcessorimplements SmartInitializingSingleton, ApplicationContextAware, BeanFactoryPostProcessor {@Overridepublic void afterSingletonsInstantiated() {.........try {processBean(beanName, type); //bean}catch (Throwable ex) {throw new BeanInitializationException("Failed to process @EventListener " +"annotation on bean with name '" + beanName + "'", ex);}}}}}private void processBean(final String beanName, final Class<?> targetType) {if (!this.nonAnnotatedClasses.contains(targetType) &&AnnotationUtils.isCandidateClass(targetType, EventListener.class) &&!isSpringContainerClass(targetType)) {Map<Method, EventListener> annotatedMethods = null;try {annotatedMethods = MethodIntrospector.selectMethods(targetType,(MethodIntrospector.MetadataLookup<EventListener>) method ->AnnotatedElementUtils.findMergedAnnotation(method, EventListener.class));}catch (Throwable ex) {// An unresolvable type in a method signature, probably from a lazy bean - let's ignore it.if (logger.isDebugEnabled()) {logger.debug("Could not resolve methods for bean with name '" + beanName + "'", ex);}}if (CollectionUtils.isEmpty(annotatedMethods)) {this.nonAnnotatedClasses.add(targetType);if (logger.isTraceEnabled()) {logger.trace("No @EventListener annotations found on bean class: " + targetType.getName());}}else {// Non-empty set of methodsConfigurableApplicationContext context = this.applicationContext;Assert.state(context != null, "No ApplicationContext set");List<EventListenerFactory> factories = this.eventListenerFactories;Assert.state(factories != null, "EventListenerFactory List not initialized");for (Method method : annotatedMethods.keySet()) {for (EventListenerFactory factory : factories) {if (factory.supportsMethod(method)) { // 此处,针对所有EventListener注解的方法,均返回true,Method methodToUse = AopUtils.selectInvocableMethod(method, context.getType(beanName));ApplicationListener<?> applicationListener =factory.createApplicationListener(beanName, targetType, methodToUse);if (applicationListener instanceof ApplicationListenerMethodAdapter) {((ApplicationListenerMethodAdapter) applicationListener).init(context, this.evaluator);}context.addApplicationListener(applicationListener);// 往容器中注入监听器,同 接口方式break;}}}if (logger.isDebugEnabled()) {logger.debug(annotatedMethods.size() + " @EventListener methods processed on bean '" +beanName + "': " + annotatedMethods);}}}}
}

4.3 监听器排序

如果某个事件有多个监听器,默认情况下,监听器执行顺序是无序的,不过我们可以为监听器指定顺序。

4.3.1 通过接口实现监听器:

三种方式指定监听器顺序:

  • 实现org.springframework.core.Ordered接口#getOrder,返回值越小,顺序越高

  • 实现org.springframework.core.PriorityOrdered接口#getOrder

  • 类上使用org.springframework.core.annotation.Order注解

4.3.2 通过@EventListener:

可以在标注@EventListener的方法上面使用@Order(顺序值)注解来标注顺序,

4.4 监听器异步模式

监听器最终通过ApplicationEventMulticaster内部的实现来调用,默认实现类SimpleApplicationEventMulticaster,这个类是支持监听器异步调用的。

	@Overridepublic void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));Executor executor = getTaskExecutor();for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {if (executor != null) {executor.execute(() -> invokeListener(listener, event));}else {invokeListener(listener, event);}}}

上面的invokeListener方法内部就是调用监听器,从代码可以看出,如果当前executor不为空,监听器就会被异步调用,所以如果需要异步只需要让executor不为空就可以了,但是默认情况下executor是空的,此时需要我们来给其设置一个值,下面我们需要看容器中是如何创建广播器的,我们在那个地方去干预。

AnnotationConfigServletWebServerApplicationContext -》 ServletWebServerApplicationContext -》 GenericWebApplicationContext -》 GenericApplicationContext -》 AbstractApplicationContext -》 ConfigurableApplicationContext -》 ApplicationContext -》 ApplicationEventPublisher

通常我们使用的容器是继承于AbstractApplicationContext类型的,在容器启动的时候会调用AbstractApplicationContext#initApplicationEventMulticaster,初始化广播器:

	private ApplicationEventMulticaster applicationEventMulticaster;public static final String APPLICATION_EVENT_MULTICASTER_BEAN_NAME = "applicationEventMulticaster";protected void initApplicationEventMulticaster() {ConfigurableListableBeanFactory beanFactory = getBeanFactory();if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) { // 判断容器中是否有一个 applicationEventMulticaster bean,有的话直接拿到使用this.applicationEventMulticaster =beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);if (logger.isTraceEnabled()) {logger.trace("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");}}else {this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);if (logger.isTraceEnabled()) {logger.trace("No '" + APPLICATION_EVENT_MULTICASTER_BEAN_NAME + "' bean, using " +"[" + this.applicationEventMulticaster.getClass().getSimpleName() + "]");}}}

判断spring容器中是否有名称为applicationEventMulticaster的bean,如果有就将其作为事件广播器,否则创建一个SimpleApplicationEventMulticaster作为广播器,并将其注册到spring容器中。

自定义一个类型为SimpleApplicationEventMulticaster名称为applicationEventMulticaster的bean就可以了,顺便给executor设置一个值,就可以实现监听器异步执行了。

实现如下:

@Configuration
public class SyncListenerConfig {@Beanpublic ApplicationEventMulticaster applicationEventMulticaster() {// 创建一个事件广播器SimpleApplicationEventMulticaster result = new SimpleApplicationEventMulticaster();// 给广播器提供一个线程池,通过这个线程池来调用事件监听器ThreadPoolTool threadPoolTool = new ThreadPoolTool();ThreadPoolExecutor executor = threadPoolTool.build();// 设置异步执行器result.setTaskExecutor(executor);return result;}
}@Slf4j
//@Data
public class ThreadPoolTool {private static int corePoolSize = Runtime.getRuntime().availableProcessors();private static int maximumPoolSize = corePoolSize * 2;private static long keepAliveTime = 10;private static TimeUnit unit = TimeUnit.SECONDS;private static BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(3);private static ThreadFactory threadFactory = new NameTreadFactory();private static RejectedExecutionHandler handler = new MyIgnorePolicy();private ThreadPoolExecutor executor;public ThreadPoolExecutor build() {executor  = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit,workQueue, threadFactory, handler);executor.prestartAllCoreThreads(); // 预启动所有核心线程return executor;}
}@Slf4j
public class NameTreadFactory implements ThreadFactory {private AtomicInteger mThreadNum = new AtomicInteger(1);@Overridepublic Thread newThread(Runnable r) {Thread thread = new Thread(r, "my-thread-" + mThreadNum.getAndIncrement());log.info(thread.getName() + " has been created");return thread;}
}

运行后输出:

INFO []2023-02-15 14:58:49.182[org.im.eventtest.spring.UserRegisterService][31][http-nio-12000-exec-1][INFO]-用户name2注册成功
INFO []2023-02-15 14:58:49.184[org.im.eventtest.spring.SendEmailListener][24][my-thread-16][INFO]-给用户name2发送注册成功邮件!
INFO []2023-02-15 14:58:49.278[org.im.eventtest.spring.SendEmailListener][30][my-thread-15][INFO]-EventListener 给用户name2发送注册邮件成功!

5、使用建议

  • 可以使用spring事件机制来传参、解耦等;

  • 对于一些非主要的业务(失败后不影响主业务处理),可以使用异步的事件模式;

  • spring中事件无论是使用接口的方式还是使用注解的方式,都可以(最好团队内部统一使用一种方式)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/145269.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

R语言分析:如何轻松地把数据分为三、四、五等份?

有网友问了&#xff0c;我如何对连续型资料进行分组&#xff0c;常见的有按照中位数分组、四分位数分组&#xff0c;甚至分为5组。 这个问题其实很简单的了。 用两个函数&#xff0c;一个是quantile函数&#xff0c;另外一个是cut函数 1. quantile()函数的应用 该函数目的是获得…

基于Web的在线学习平台设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

Java后端接口编写流程

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; Java后端接口编写流程 Java后端接口编写流程&#xff0c;更具业务逻辑编写Java后端接口&#xff0c;提供给前端访问 实现逻辑流程 POJO&#xff1a;实体类编写 Data B…

SpringBoot整合RabbitMQ实现延迟队列功能

&#x1f468;&#x1f3fb;‍&#x1f4bb; 热爱摄影的程序员 &#x1f468;&#x1f3fb;‍&#x1f3a8; 喜欢编码的设计师 &#x1f9d5;&#x1f3fb; 擅长设计的剪辑师 &#x1f9d1;&#x1f3fb;‍&#x1f3eb; 一位高冷无情的编码爱好者 大家好&#xff0c;我是 DevO…

SpringBoot——常用注解

Spring Web MVC与Spring Bean注解 Controller/RestController Controller是Component注解的一个延伸&#xff0c;Spring 会自动扫描并配置被该注解标注的类。此注解用于标注Spring MVC的控制器。 Controller RequestMapping("/api/v1") public class UserApiContr…

【计算机视觉】3.传统计算机视觉方法

传统计算机视觉方法 一、大纲图像分割人脸检测行人检测 二、图像分割基于阈值检测的方法基于边缘检测的方法基于区域的分割方法基于图论的分割方法 三、人脸检测四、行人检测五、SVM六、DPM 一、大纲 图像分割 基于阈值、基于边缘 基于区域、基于图论 人脸检测 Haar-like 特征…

IntelliJ IDEA 上 使用git 合并其他分支,合并某一个提交

git 合并其他分支 找到git——>merge… 选择需要合并的分支&#xff0c;不能选和当前分支一样噢 合并&#xff0c;推送即可 合并某个提交到其他分支 点击左下角git——>右键切换分支——>选择需要合并的分支——>选择需要合并的代码——>ch 推送即可

【AI视野·今日NLP 自然语言处理论文速览 第三十八期】Thu, 21 Sep 2023

AI视野今日CS.NLP 自然语言处理论文速览 Thu, 21 Sep 2023 Totally 57 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computation and Language Papers Chain-of-Verification Reduces Hallucination in Large Language Models Authors Shehzaad Dhuliawala, Mojt…

Leetcode684. 冗余连接

Every day a Leetcode 题目来源&#xff1a;684. 冗余连接 解法1&#xff1a;并查集 因为需要判断是否两个节点被重复连通&#xff0c;所以我们可以使用并查集来解决此类问题。 代码&#xff1a; /** lc appleetcode.cn id684 langcpp** [684] 冗余连接*/// lc codestart…

LeetCode —— 回溯

77. 组合 给定两个整数 n 和 k&#xff0c;返回范围 [1, n] 中所有可能的 k 个数的组合。 示例&#xff1a;输入&#xff1a;n 4, k 2 输出&#xff1a; [ [1,2], [1,3], [1,4], [2,3], [2,4], [3,4]] class Solution {List<List<Integer>> list new…

【100天精通Python】Day67:Python可视化_Matplotlib 绘制动画,2D、3D 动画 示例+代码

1 绘制2D动画&#xff08;animation&#xff09; Matplotlib是一个Python绘图库&#xff0c;它提供了丰富的绘图功能&#xff0c;包括绘制动画。要绘制动画&#xff0c;Matplotlib提供了FuncAnimation类&#xff0c;允许您创建基于函数的动画。下面是一个详细的Matplotlib动画示…

【AI视野·今日CV 计算机视觉论文速览 第257期】Fri, 29 Sep 2023

AI视野今日CS.CV 计算机视觉论文速览 Fri, 29 Sep 2023 Totally 99 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computer Vision Papers Learning to Transform for Generalizable Instance-wise Invariance Authors Utkarsh Singhal, Carlos Esteves, Ameesh M…

升级:远程桌面软件玩游戏指南

你有没有遇到过这样的场景&#xff1a;你想玩一款特定的游戏&#xff0c;但却受到设备功能的限制&#xff1f;这就是游戏远程桌面的概念变得非常宝贵的地方。从本质上讲&#xff0c;它允许您远程利用高端游戏计算机的功能&#xff0c;使您能够在自己的设备上玩游戏。 可以考虑…

面向对象之旅:核心理念、设计方法与UML详解(软件设计师笔记)

&#x1f600;前言 面向对象技术是现代软件工程的核心&#xff0c;为软件设计和开发带来了一种强大且有序的方法。通过将现实世界的实体和概念映射到可操作的代码结构&#xff0c;该技术使我们能够更高效、清晰和可靠地创建复杂的软件系统。在本章中&#xff0c;我们将详细介绍…

VS编译器常见的错误

以上问题在编译器中出现可以在编译器中最上面加入&#xff1a; #define_CRT_SECURE_NO_WARNINGS 或者将scanf修改为scanf_s 一定要在最上端&#xff01;&#xff01;&#xff01;最上端&#xff01;&#xff01;&#xff01;最上端加入&#xff01;&#xff01;&#xff01; 虽…

modbusTCP【codesys】

1添加控制器【控制器】&#xff1a; 右击左侧树型菜单【Device】→选择【添加设备】&#xff0c;在弹出对话框中下拉选择【以太网适配器】下的【Ethernet】&#xff0c;最后点击【添加设备】。 注意固件能支持的版本。我的是3.5.17 2添加主站【主站】 2添加从站【从站】 双…

设计模式2、抽象工厂模式 Abstract Factory

解释说明&#xff1a;提供一个创建一系列相关或相互依赖对象的接口&#xff0c;而无需指定他们具体的类。 简言之&#xff0c;一个工厂可以提供创建多种相关产品的接口&#xff0c;而无需像工厂方法一样&#xff0c;为每一个产品都提供一个具体工厂 抽象工厂&#xff08;Abstra…

python:bottle + eel 模仿 mdict 查英汉词典

Eel 是一个轻量的 Python 库&#xff0c;用于制作简单的类似于离线 HTML/JS GUI 应用程序&#xff0c;并具有对 Python 功能和库的完全访问权限。 Eel 托管一个本地 Web 服务器&#xff0c;允许您使用 Python 注释函数&#xff08;annotate functions&#xff09;&#xff0c;…

ThreeJS-3D教学三:平移缩放+物体沿轨迹运动

我们在项目中会有一些这样的需求&#xff0c;我们可视化一个场景&#xff0c;需要俯视、平移、缩放&#xff0c;方便观察场景中的数据或者模型&#xff0c;之所以把这个案例拿出来 1、这是个很实用的需求&#xff0c;我相信很多人会用到 2、我自己认为在实际案例中我们可以学习…

APS手动编译,CLion测试

一、简介 APSI——Asymmetric PSI&#xff1a; 私用集交集&#xff08;PSI&#xff09;是指这样一种功能&#xff0c;即双方都持有一组私用项&#xff0c;可以在不向对方透露任何其他信息的情况下检查他们有哪些共同项。集合大小的上限被假定为公共信息&#xff0c;不受保护。 …