聊一聊Spring中的自定义监听器

前言

通过一个简单的自定义的监听器,从源码的角度分一下Spring中监听的整个过程,分析监听的作用。

一、自定义监听案例

1.1定义事件

package com.lazy.snail;import lombok.Getter;
import org.springframework.context.ApplicationEvent;/*** @ClassName UserRegisteredEvent* @Description TODO* @Author lazysnail* @Date 2024/11/8 10:37* @Version 1.0*/
@Getter
public class UserRegisteredEvent extends ApplicationEvent {private final String username;public UserRegisteredEvent(Object source, String username) {super(source);this.username = username;}
}

1.2定义监听

package com.lazy.snail;import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;/*** @ClassName UserRegisteredListener* @Description TODO* @Author lazysnail* @Date 2024/11/8 10:36* @Version 1.0*/
@Component
public class UserRegisteredListener {@EventListenerpublic void handleUserRegisterEvent(UserRegisteredEvent event) {System.out.println("用户注册成功,发送邮件通知");}
}

1.3定义用户服务(发布事件)

package com.lazy.snail;import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;/*** @ClassName UserService* @Description TODO* @Author lazysnail* @Date 2024/11/8 10:37* @Version 1.0*/
@Service
public class UserService {private final ApplicationEventPublisher eventPublisher;public UserService(ApplicationEventPublisher eventPublisher) {this.eventPublisher = eventPublisher;}public void registerUser(String username) {// 用户注册逻辑System.out.println("Registering user: " + username);// 发布用户注册事件eventPublisher.publishEvent(new UserRegisteredEvent(this, username));}
}

1.4测试类

package com.lazy.snail;import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;@Slf4j
public class SpringTest {@Testvoid test() {ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);context.getBean(UserService.class).registerUser("lazysnail");}
}

1.5测试结果

image-20241108154337541

二、事件监听流程

2.1容器启动阶段

2.1.1事件监听方法处理器及默认事件监听工厂

  • 事件监听方法处理器及默认事件监听工厂的bean定义信息注册
    1. 事件监听方法处理器会在后续用于处理自定义监听中的@EventListener注解
    2. 默认事件监听工厂会用于将自定义监听封装为ApplicationListenerMethodAdapter
public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(BeanDefinitionRegistry registry, @Nullable Object source) {// 省略部分代码...// 事件监听方法处理器if (!registry.containsBeanDefinition(EVENT_LISTENER_PROCESSOR_BEAN_NAME)) {RootBeanDefinition def = new RootBeanDefinition(EventListenerMethodProcessor.class);def.setSource(source);beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_PROCESSOR_BEAN_NAME));}// 默认事件监听工厂if (!registry.containsBeanDefinition(EVENT_LISTENER_FACTORY_BEAN_NAME)) {RootBeanDefinition def = new RootBeanDefinition(DefaultEventListenerFactory.class);def.setSource(source);beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_FACTORY_BEAN_NAME));}return beanDefs;
}
  • 事件监听方法处理器及默认事件监听工厂的实例化
    1. refresh方法中,invokeBeanFactoryPostProcessors处理BeanFactoryPostProcessor(EventListenerMethodProcessor实现了BeanFactoryPostProcessor)
    2. 实例化EventListenerMethodProcessor
    3. 调用EventListenerMethodProcessor的postProcessBeanFactory实例化DefaultEventListenerFactory

image-20241108170442533

// EventListenerMethodProcessor
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {this.beanFactory = beanFactory;Map<String, EventListenerFactory> beans = beanFactory.getBeansOfType(EventListenerFactory.class, false, false);List<EventListenerFactory> factories = new ArrayList<>(beans.values());AnnotationAwareOrderComparator.sort(factories);this.eventListenerFactories = factories;
}

2.1.3应用事件广播器创建

  • 容器刷新时,initApplicationEventMulticaster创建SimpleApplicationEventMulticaster
  • 注册单例到容器
// AbstractApplicationContext
public void refresh() throws BeansException, IllegalStateException {// 为容器初始化事件广播器initApplicationEventMulticaster();
}
// AbstractApplicationContext
protected void initApplicationEventMulticaster() {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() + "]");}
}
  • SimpleApplicationEventMulticaster从AbstractApplicationEventMulticaster继承过来一个defaultRetriever对象
  • defaultRetriever中封装了监听器集合
private class DefaultListenerRetriever {public final Set<ApplicationListener<?>> applicationListeners = new LinkedHashSet<>();public final Set<String> applicationListenerBeans = new LinkedHashSet<>();
}
  • 监听集合中的监听是何时添加的

提前实例化单例后EventListenerMethodProcessor对容器中所有监听处理时添加

// DefaultListableBeanFactory
public void preInstantiateSingletons() throws BeansException {// 省略部分代码...// EventListenerMethodProcessorfor (String beanName : beanNames) {Object singletonInstance = getSingleton(beanName);if (singletonInstance instanceof SmartInitializingSingleton) {StartupStep smartInitialize = this.getApplicationStartup().start("spring.beans.smart-initialize").tag("beanName", beanName);SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;if (System.getSecurityManager() != null) {AccessController.doPrivileged((PrivilegedAction<Object>) () -> {smartSingleton.afterSingletonsInstantiated();return null;}, getAccessControlContext());} else {// 单例实例化后处理smartSingleton.afterSingletonsInstantiated();}smartInitialize.end();}}
}
  • 监听器的创建
// EventListenerMethodProcessor
public void afterSingletonsInstantiated() {ConfigurableListableBeanFactory beanFactory = this.beanFactory;Assert.state(this.beanFactory != null, "No ConfigurableListableBeanFactory set");String[] beanNames = beanFactory.getBeanNamesForType(Object.class);// 处理UserRegisteredListenerfor (String beanName : beanNames) {// 省略部分代码...processBean(beanName, type);}
}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;// 省略部分代码...// @EventListener注解的方法(注解上的属性)annotatedMethods = MethodIntrospector.selectMethods(targetType,(MethodIntrospector.MetadataLookup<EventListener>) method ->AnnotatedElementUtils.findMergedAnnotation(method, EventListener.class));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)) {Method methodToUse = AopUtils.selectInvocableMethod(method, context.getType(beanName));// 事件监听工厂创建应用监听器 ApplicationListenerMethodAdapterApplicationListener<?> applicationListener =factory.createApplicationListener(beanName, targetType, methodToUse);if (applicationListener instanceof ApplicationListenerMethodAdapter) {((ApplicationListenerMethodAdapter) applicationListener).init(context, this.evaluator);}// 添加到应用上下文context.addApplicationListener(applicationListener);break;}}}}}
}

2.2客户端调用阶段

  • 发布事件
// AbstractApplicationContext
protected void publishEvent(Object event, @Nullable ResolvableType eventType) {getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
}
  • 拿到内部应用事件广播器(SimpleApplicationEventMulticaster)

  • 广播器广播事件

// SimpleApplicationEventMulticaster
public 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);}}
}
  • 获取监听
    1. 检索应用监听器
    2. 直接从检索器(defaultRetriever)中取出监听
/*** 根据给定的事件、源(我理解是容器)检索监听器* */
// AbstractApplicationEventMulticaster
private Collection<ApplicationListener<?>> retrieveApplicationListeners(ResolvableType eventType, @Nullable Class<?> sourceType, @Nullable CachedListenerRetriever retriever) {List<ApplicationListener<?>> allListeners = new ArrayList<>();Set<ApplicationListener<?>> filteredListeners = (retriever != null ? new LinkedHashSet<>() : null);Set<String> filteredListenerBeans = (retriever != null ? new LinkedHashSet<>() : null);Set<ApplicationListener<?>> listeners;Set<String> listenerBeans;synchronized (this.defaultRetriever) {// 默认检索器中获取应用监听,监听已经在Spring启动阶段注册完成listeners = new LinkedHashSet<>(this.defaultRetriever.applicationListeners);listenerBeans = new LinkedHashSet<>(this.defaultRetriever.applicationListenerBeans);}// 省略部分代码...AnnotationAwareOrderComparator.sort(allListeners);if (retriever != null) {if (filteredListenerBeans.isEmpty()) {retriever.applicationListeners = new LinkedHashSet<>(allListeners);retriever.applicationListenerBeans = filteredListenerBeans;}else {retriever.applicationListeners = filteredListeners;retriever.applicationListenerBeans = filteredListenerBeans;}}return allListeners;
}		
  • 调用监听

invokeListener

image-20241108153641450

三、总结

  • 个人理解:

事件发布是一个抽象的概念,真正将事件发布出去的是SimpleApplicationEventMulticaster,发布事件实际做的事情,找到监听器,过滤出能够处理这个事件的监听器,然后执行监听器中针对这个事件的业务逻辑。

3.1监听流程总结

3.1.1. Spring 容器启动

  • 在 Spring 启动过程中,ApplicationContext 被初始化,它作为核心容器,提供了事件发布和监听的机制。
  • Spring 使用 ApplicationEventPublisher 作为事件发布的核心接口,事件的发布与处理都在 ApplicationContext 内部实现。

3.1.2. 监听器注册

  • 在 Spring 中,可以通过以下几种方式注册监听器:
    • 实现 ApplicationListener 接口:将实现类作为 Spring Bean 注册,Spring 会自动将它识别为事件监听器。
    • 通过 XML 配置:在 XML 文件中配置 <bean class="com.example.MyEventListener"/>,将监听器注册到 ApplicationContext
    • 注解方式:使用@EventListener注解
  • 监听器的作用:每当发布的事件类型与监听器泛型参数中的事件类型匹配时,监听器的 onApplicationEvent 方法就会被调用。

3.1.3. 事件发布

  • 发布者:Spring 中的任何组件都可以通过 ApplicationEventPublisher 发布事件。通常,ApplicationContext 本身实现了 ApplicationEventPublisher,可以直接调用 publishEvent() 发布事件。
  • 事件传播器:默认情况下,Spring 使用 SimpleApplicationEventMulticaster 作为事件传播器,它负责查找符合条件的监听器并将事件分发给它们。
  • 发布事件的方法:通过 applicationContext.publishEvent(new CustomEvent(this)) 来发布事件。

3.1.4. 事件广播给监听器

  • 筛选监听器SimpleApplicationEventMulticaster 会检查所有注册的监听器,筛选出对当前事件感兴趣的监听器(基于事件类型的匹配)。
  • 同步与异步:在 Spring 环境中,默认情况下事件是同步传递的,所有监听器在主线程中执行。如果需要异步,可以通过自定义 SimpleApplicationEventMulticaster 并配置线程池。

3.1.5. 监听器处理事件

  • 监听逻辑执行:每个匹配的监听器会调用 onApplicationEvent() 方法,执行相应的业务逻辑。
  • 异常处理:如果监听器抛出异常,SimpleApplicationEventMulticaster 会捕获并记录日志,但不会影响其他监听器的执行。

3.1.6. 事件传播的扩展

  • 在某些场景中,一个事件的监听器可能会发布新的事件,这会形成事件链。Spring 容器会递归地将这些新事件广播给感兴趣的监听器。

3.2应用场景

3.2.1. 解耦业务逻辑

  • 场景描述:在业务流程中,常常需要在某个操作完成后执行附加逻辑,比如用户注册后发送欢迎邮件、推送通知、或更新统计数据。
  • 实现方式:通过监听器监听用户注册事件,执行后续的附加操作。这样,核心业务逻辑与附加逻辑可以解耦,各自独立管理。
  • 示例:用户注册成功后触发 UserRegistrationEvent,监听器接收事件后完成发送邮件或通知的任务。

3.2.2. 事务性事件

  • 场景描述:在某些情况下,需要确保只有当事务成功提交后,才会发布事件。比如在订单创建后,确保库存减少或通知支付系统。
  • 实现方式:通过 @TransactionalEventListener 监听事务性事件,确保事件只有在事务提交成功时才会触发。
  • 示例:订单创建完成并且数据库事务成功提交后,触发 OrderCreatedEvent,通知库存系统减少库存。

3.2.3. 异步处理任务

  • 场景描述:对于不需要实时完成的任务,可以通过异步监听器来解放主线程,避免阻塞。
  • 实现方式:在事件监听器方法上使用 @Async,使其在独立线程中执行异步任务。
  • 示例:用户在系统中上传文件,文件处理逻辑通过事件异步执行,以保证上传接口的快速响应。

3.2.4. 应用启动或关闭事件

  • 场景描述:在应用启动或关闭时,通常需要执行一些初始化或清理操作,比如加载配置、检查依赖服务、关闭资源等。
  • 实现方式:通过监听 ApplicationReadyEventContextClosedEvent 等应用上下文事件,实现启动和关闭时的操作。
  • 示例:在应用启动完成后加载配置文件,或在应用关闭时清理缓存或关闭数据库连接。

3.2.5. 状态变化或监控

  • 场景描述:在系统中监控某些状态的变化,比如监控服务状态、资源使用情况、流量变化等。
  • 实现方式:使用自定义事件来捕获和广播状态变化,监听器实时响应状态变化,执行对应操作。
  • 示例:当服务发现高负载时,发布 HighLoadEvent,监听器响应并调整系统参数或生成告警。

3.2.6. 领域驱动设计(DDD)中的事件处理

  • 场景描述:在领域驱动设计中,事件驱动架构常用于处理不同领域的事件交互,比如订单模块的事件会影响到支付、物流等模块。
  • 实现方式:通过领域事件(如订单支付事件、库存更新事件)来实现模块间的松耦合通信,避免模块之间的直接依赖。
  • 示例:在电商系统中,用户下单后触发 OrderPlacedEvent,物流模块监听该事件并安排发货。

3.2.7. 跨服务通信

  • 场景描述:在微服务架构中,服务之间往往需要基于事件进行异步通信,降低耦合度。
  • 实现方式:通过发布事件到消息中间件(如 Kafka、RabbitMQ),各服务监听感兴趣的事件。
  • 示例:支付服务完成支付后触发 PaymentCompletedEvent,订单服务监听该事件并更新订单状态。

3.2.8. 监听应用配置变化

  • 场景描述:在应用运行期间,可能需要动态刷新配置,比如数据库连接、缓存配置等。
  • 实现方式:通过监听配置中心的配置更新事件,触发配置的刷新。
  • 示例:当配置中心检测到 Redis 缓存配置更新后触发 CacheConfigUpdateEvent,应用的缓存配置自动刷新。

3.2.9. 处理安全或认证事件

  • 场景描述:在用户认证、权限验证等过程中,可以发布事件来处理安全相关操作。
  • 实现方式:监听认证成功、认证失败等事件,执行相应的业务逻辑,比如记录日志、锁定账户。
  • 示例:用户多次登录失败后触发 AuthenticationFailureEvent,监听器响应后锁定用户账户并生成告警。

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

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

相关文章

VMWareTools安装及文件无法拖拽解决方案

文章目录 1 安装VMWare Tools2 安装vmware tools之后还是无法拖拽文件解决方案2.1 确认vmware tools安装2.2 客户机隔离2.3 修改自定义配置文件2.4 安装open-vm-tools-desktop软件 1 安装VMWare Tools 打开虚拟机VMware Workstation&#xff0c;启动Ubuntu系统&#xff0c;菜单…

ADC前端控制与处理模块--AD7606_Module

总体框架 AD7606_Module主要由3个模块组成组成&#xff0c;AD7606_Data_Pkt和AD7606_Drive以及AD7606_ctrl。 1.AD7606_Data_Pkt主要作用是把AD芯片数据组好数据包&#xff0c;然后发送给上位机&#xff1b; 2.AD7606_Drive主要负责和芯片的交互部分 3.AD7606_ctrl控制模块的作…

Unity 插件 - Project窗口资源大小显示

Unity 插件 - Project窗口资源大小显示 &#x1f354;功能&#x1f32d;安装 &#x1f354;功能 &#x1f4a1;.显示Project Assets 和Packages下所有文件的大小&#xff08;右侧显示&#xff09; &#x1f4a1;.统计选中文件夹及其子文件夹下所有文件的大小并显示&#xff08…

HTB:Photobomb[WriteUP]

目录 连接至HTB服务器并启动靶机 使用nmap对靶机进行端口开放扫描 再次使用nmap对靶机开放端口进行脚本、服务扫描 使用ffuf进行简单的子域名扫描 使用浏览器直接访问该域名 选取一个照片进行下载&#xff0c;使用Yakit进行抓包 USER_FLAG&#xff1a;a9afd9220ae2b5731…

ssm教室信息管理系统+vue

系统包含&#xff1a;源码论文 所用技术&#xff1a;SpringBootVueSSMMybatisMysql 免费提供给大家参考或者学习&#xff0c;获取源码看文章最下面 需要定制看文章最下面 目 录 目 录 III 1 绪论 1 1.1 研究背景 1 1.2目的和意义 1 1.3 论文结构安排 2 2 相关技术 3 …

详解Java之Spring MVC篇二

目录 获取Cookie/Session 理解Cookie 理解Session Cookie和Session的区别 获取Cookie 获取Session 获取Header 获取User-Agent 获取Cookie/Session 理解Cookie HTTP协议自身是“无状态”协议&#xff0c;但是在实际开发中&#xff0c;我们很多时候是需要知道请求之间的…

量子计算及其在密码学中的应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 量子计算及其在密码学中的应用 量子计算及其在密码学中的应用 量子计算及其在密码学中的应用 引言 量子计算概述 定义与原理 发展…

当财政支持减弱时,国有企业如何实现降本增效?

当财政支持减弱时&#xff0c;国有企业如何实现降本增效&#xff1f; 随着市场环境的不断变化和上级市场化政策要求的不断推进&#xff0c;部分国有企业面临着双重压力&#xff0c;一方面&#xff0c;市场的快速变革要求企业不断创新、提升竞争力&#xff1b;另一方面&#xff…

引入 axios,根据 api 文档生成调用接口

起步 | Axios Docs 安装 axios npm install axios 生成 api 调用接口【可选】 https://github.com/ferdikoomen/openapi-typescript-codegen 安装 npm install openapi-typescript-codegen --save-dev 然后执行生成代码 # http://localhost:8805/api/user/v3/api-docs&a…

ElasticSearch的Python Client测试

一、Python环境准备 1、下载Python安装包并安装 https://www.python.org/ftp/python/3.13.0/python-3.13.0-amd64.exe 2、安装 SDK 参考ES官方文档: https://www.elastic.co/guide/en/elasticsearch/client/index.html python -m pip install elasticsearch一、Client 代…

在双显示器环境中利用Sunshine与Moonlight实现游戏串流的同时与电脑其他任务互不干扰

我和老婆经常会同时需要操作家里的电脑&#xff0c;在周末老婆有时要用电脑加班上网办公&#xff0c;而我想在难得的周末好好地Game一下&#xff08;在客厅用电视机或者平板串流&#xff09;&#xff0c;但是电脑只有一个&#xff0c;以往我一直都是把电脑让给老婆&#xff0c;…

【第六节】windows汇编开发工具-RadAsm与Masm

一、介绍RadAsm和Masm相关概念 1.1 什么是Win32Asm&#xff1f; Win32Asm是一种基于32位汇编语言的编程语言&#xff0c;专门用于在Windows操作系统下进行开发。Win32Asm的全称是“Windows下的32位汇编语言编程”&#xff0c;它使用微软的MASM&#xff08;Microsoft Macro Ass…

EHOME视频平台EasyCVR视频融合平台支持哪些摄像机接入?监控摄像头镜头的种类有哪些?

在现代安防监控领域&#xff0c;视频融合平台扮演着至关重要的角色&#xff0c;它们不仅能够整合不同品牌和型号的摄像机&#xff0c;还能提供稳定可靠的视频流传输和高效的视频管理功能。EasyCVR视频融合平台以其卓越的兼容性和灵活性&#xff0c;逐渐成为构建复杂监控网络的首…

从pg_depend和pg_class开始了解MogDB/openGauss/postgresql的系统元数据设计

前言 学习任何一种数据库&#xff0c;必须要了解它的数据字典&#xff0c;这样有利于了解数据库的结构、解读部分日志、定位一些问题。PG/OG系数据库的系统元数据遵从一个统一的设计规则&#xff0c;可以让初学者快速入门。本文以MogDB为例&#xff0c;剖析一下PG/OG系数据库的…

[译] APT分析报告:13.Trellix对Iran网络空间能力评估

这是作者新开的一个专栏&#xff0c;主要翻译国外知名安全厂商的技术报告和安全技术&#xff0c;了解它们的前沿技术&#xff0c;学习它们威胁溯源和恶意代码分析的方法&#xff0c;希望对您有所帮助。当然&#xff0c;由于作者英语有限&#xff0c;会借助LLM进行校验和润色&am…

vue2,vue3,uniapp,小程序实现前端url生成二维码

最近遇到一个项目&#xff0c;api返回url地址&#xff0c;前端通过地址生成二维码。 话不多说直接上代码&#xff0c;亲测有效&#xff0c;希望能帮助大家&#xff0c;同时如果有更好的方法希望大家能够分享 1、第一步&#xff0c;在项目的utils文件夹下面创建一个weapp-qrco…

Python的函数(补充浅拷贝和深拷贝)

一、定义 函数的定义&#xff1a;实现【特定功能】的代码块。 形参&#xff1a;函数定义时的参数&#xff0c;没有实际意义 实参&#xff1a;函数调用/使用时的参数&#xff0c;有实际意义 函数的作用&#xff1a; 简化代码提高代码重用性便于维护和修改提高代码的可扩展性…

FPGA学习笔记#4 Vitis HLS 入门的第一个工程

本笔记使用的Vitis HLS版本为2022.2&#xff0c;在windows11下运行&#xff0c;仿真part为xcku15p_CIV-ffva1156-2LV-e&#xff0c;这一篇终于没有再大量使用别人的内容&#xff0c;是我自己从头捋到尾的结果&#xff0c;不过之后的笔记还是要参照别人的教程就是了。 学习笔记&…

Linux中给普通账户一次性提权

我在以前文章中Linux常见指令大全&#xff08;必要知识点&#xff09;-CSDN博客 写过sudo的概念与用法。其实本质就是提权用的但是在某些场景下就算提权了也不能使用。 例如&#xff1a;打开主工作目录 他不相信你这个用户&#xff0c;虽然你是erman 解决方法 使用root账号打开…

A027-基于Spring Boot的农事管理系统

&#x1f64a;作者简介&#xff1a;在校研究生&#xff0c;拥有计算机专业的研究生开发团队&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取&#xff0c;记得注明来意哦~&#x1f339; 赠送计算机毕业设计600…