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.rhkb.cn/news/146110.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

五分钟k8s入门到实战-应用配置

ConfigMap.png 背景 在前面三节中已经讲到如何将我们的应用部署到 k8s 集群并提供对外访问的能力&#xff0c;x现在可以满足基本的应用开发需求了。 现在我们需要更进一步&#xff0c;使用 k8s 提供的一些其他对象来标准化我的应用开发。首先就是 ConfigMap&#xff0c;从它的名…

C++ 强制类型转换(int double)、查看数据类型、自动决定类型、三元表达式、取反、

强制类型转换&#xff08; int 与 double&#xff09; #include <iostream> using namespace std;int main() {// 数据类型转换char c1;short s1;int n 1;long l 1;float f 1;double d 1;int p 0;int cc (int)c;// 注意&#xff1a;字符 转 整形时 是有问题的// “…

Apache Derby的使用

Apache Derby是关系型数据库&#xff0c;可以嵌入式方式运行&#xff0c;也可以独立运行&#xff0c;当使用嵌入式方式运行时常用于单元测试&#xff0c;本篇我们就使用单元测试来探索Apache Derby的使用 一、使用IDEA创建Maven项目 打开IDEA创建Maven项目&#xff0c;这里我…

HTML详细基础(三)表单控件

本帖介绍web开发中非常核心的标签——表格标签。 在日常我们使用到的各种需要输入用户信息的场景——如下图&#xff0c;均是通过表格标签table创造出来的&#xff1a; 目录 一.表格标签 二.表格属性 三.合并单元格 四.无序列表 五.有序列表 六.自定义标签 七.表单域 …

BI神器Power Query(27)-- 使用PQ实现表格多列转换(3/3)

实例需求&#xff1a;原始表格包含多列属性数据,现在需要将不同属性分列展示在不同的行中&#xff0c;att1、att3、att5为一组&#xff0c;att2、att3、att6为另一组&#xff0c;数据如下所示。 更新表格数据 原始数据表&#xff1a; Col1Col2Att1Att2Att3Att4Att5Att6AAADD…

山西电力市场日前价格预测【2023-10-02】

日前价格预测 预测说明&#xff1a; 如上图所示&#xff0c;预测明日&#xff08;2023-10-02&#xff09;山西电力市场全天平均日前电价为355.35元/MWh。其中&#xff0c;最高日前电价为521.18元/MWh&#xff0c;预计出现在18: 45。最低日前电价为309.36元/MWh&#xff0c;预计…

Windows权限维持

Meterpreter权限维持 Metasploit 框架提供了一个后渗透模块&#xff0c;可实现自动化地利用沾滞键的权限维持技术。 该模块将用 CMD 替换辅助功能的二进制文件&#xff08; sethc, osk, disp, utilman &#xff09; use post/windows/manage/sticky_keys 设置session 提示&a…

双指针算法——复写零

双指针算法——复写零&#x1f60e; 前言&#x1f64c;复写零板书分析&#xff1a;解题代码&#xff1a;B站视频讲解 总结撒花&#x1f49e; &#x1f60e;博客昵称&#xff1a;博客小梦 &#x1f60a;最喜欢的座右铭&#xff1a;全神贯注的上吧&#xff01;&#xff01;&#…

Arm Cache学习资料大汇总

关键词&#xff1a;cache学习、mmu学习、cache资料、mmu资料、arm资料、armv8资料、armv9资料、 trustzone视频、tee视频、ATF视频、secureboot视频、安全启动视频、selinux视频&#xff0c;cache视频、mmu视频&#xff0c;armv8视频、armv9视频、FF-A视频、密码学视频、RME/CC…

3分钟学会设计模式 -- 单例模式

►使用场景 在编写软件时&#xff0c;对于某些类来说&#xff0c;只有一个实例很重要。例如&#xff0c;一个系统中可以存在多个打印任务&#xff0c;但是只能有一个正在工作的任务&#xff1b;一个系统中可以多次查询数据库&#xff0c;但是只需要一个连接&#xff0c;而不是…

2023年中国艺术涂料市场发展历程及趋势分析:艺术涂料市场规模将进一步扩大[图]

艺术涂料是一种用于绘画和装饰&#xff0c;具有各种纹理或通过涂装手段后具有高装饰性的新型涂料。由于具有高度饱和的颜色、良好的遮盖力和可塑性&#xff0c;呈现立体装饰效果好、色彩搭配适当、风格独具特色的特点&#xff0c;而使得涂装出的饰面自然贴合、更加美观漂亮&…

【EasyPoi】SpringBoot使用EasyPoi自定义模版导出Excel

EasyPoi 官方文档&#xff1a;http://doc.wupaas.com/docs/easypoi Excel模版导出 引入依赖 <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency…

数据结构与算法-(7)---栈的应用-(3)表达式转换

&#x1f308;write in front&#x1f308; &#x1f9f8;大家好&#xff0c;我是Aileen&#x1f9f8;.希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流. &#x1f194;本文由Aileen_0v0&#x1f9f8; 原创 CSDN首发&#x1f412; 如…

【刷题笔记10.2】LeetCode: 罗马数字转整数

LeetCode: 罗马数字转整数 一、题目描述 二、分析 方法一&#xff1a; 将给定字符串s中的"IV", “IX”, “XL”, “XC”, “CD”, “CM” 全部替换为其他字符如&#xff1a;a, b, c, d, e, f 这种&#xff0c;然后就可以遍历累加了。 s s.replace("IV",…

Grafana 开源了一款 eBPF 采集器 Beyla

eBPF 的发展如火如荼&#xff0c;在可观测性领域大放异彩&#xff0c;Grafana 近期也发布了一款 eBPF 采集器&#xff0c;可以采集服务的 RED 指标&#xff0c;本文做一个尝鲜介绍&#xff0c;让读者有个大概了解。 eBPF 基础介绍可以参考我之前的文章《eBPF Hello world》。理…

Linux命令(二)(文件相关)

目录可以更快找到你想要的命令 1. 命令入门2. 文件(touch、vim、cat、more、mv)touch&#xff1a;用于创建空文件与修改时间戳选项用例 vim&#xff1a;文本编辑工具&#xff08;三种模式&#xff09;1. 命令模式&#xff1a;2. 插入模式3. 底线命令模式 cat&#xff1a;在终端…

网站使用SSL证书是趋势吗?

随着互联网技术的不断发展&#xff0c;网络安全问题日益受到重视。其中&#xff0c;SSL证书作为网站安全的基石&#xff0c;其重要性不言而喻。SSL证书能够加密网站与用户之间的通信&#xff0c;保护用户隐私&#xff0c;防止信息被窃取和篡改。因此&#xff0c;越来越多的网站…

Android 性能优化—— 启动优化提升60%

应用启动速度 一个应用App的启动速度能够影响用户的首次体验&#xff0c;启动速度较慢(感官上)的应用可能导致用户再次开启App的意图下降&#xff0c;或者卸载放弃该应用程序 本文将从两个方向优化应用的启动速度 : 1.视觉体验优化 2.代码逻辑优化 视觉优化 应用程序启动有…

KNN(下):数据分析 | 数据挖掘 | 十大算法之一

⭐️⭐️⭐️⭐️⭐️欢迎来到我的博客⭐️⭐️⭐️⭐️⭐️ &#x1f434;作者&#xff1a;秋无之地 &#x1f434;简介&#xff1a;CSDN爬虫、后端、大数据领域创作者。目前从事python爬虫、后端和大数据等相关工作&#xff0c;主要擅长领域有&#xff1a;爬虫、后端、大数据…

初识Java 12-1 流

目录 Java 8对流的支持 流的创建 随机数流 int类型的区间范围 generate() iterate() 流生成器 Arrays 正则表达式 本笔记参考自&#xff1a; 《On Java 中文版》 ||| 流的概念&#xff1a;流是一个与任何特定的存储机制都没有关系的元素序列。 流与对象的成批处理有关…