从零开始 Spring Boot 41:事件

news2024/10/7 18:31:15

从零开始 Spring Boot 41:事件

spring boot

图源:简书 (jianshu.com)

Spring 实现了一个简单、实用的事件框架,利用它我们可以在多个组件之间进行松耦合式的通信。

简单示例

让我们从一个简单的示例开始:

public record Email(String address, String content) {
}

public class EmailsReceivedEvent extends ApplicationEvent {
    @Setter
    @Getter
    private List<Email> emails = Collections.emptyList();

    public EmailsReceivedEvent(Object source) {
        super(source);
    }
}

这里的Email表示一个电子邮件,EmailsReceivedEvent表示我们的系统收到电子邮件后在内部会发布的事件,它可以包含多封邮件。

这里的事件EmailsReceivedEvent继承自ApplicationEvent,这在早期 Spring (4.2之前)是必须的。

Publisher

我们需要定义一个 bean 负责事件的发送:

@Component
public class EmailsReceivedEventPublisher {
    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    /**
     * 发布多个邮件收到的事件
     * @param emails
     */
    public void publish(@NonNull List<Email> emails){
        this.doPublishEvent(emails);
    }

    /**
     * 发布邮件收到的事件(单个邮件)
     * @param email
     */
    public void publish(@NonNull Email email){
        List<Email> emails = Collections.singletonList(email);
        this.doPublishEvent(emails);
    }

    private void doPublishEvent(List<Email> emails){
        EmailsReceivedEvent event = new EmailsReceivedEvent(this);
        event.setEmails(emails);
        applicationEventPublisher.publishEvent(event);
    }
}

具体的事件发送我们需要使用ApplicationEventPublisher.publishEvent,这里通过依赖注入获取一个ApplicationEventPublisher的引用。

如果你因为某些原因不方便那么做(比如因为 bean 在 Spring 启动的早期阶段实例化,无法使用依赖注入),可以使用ApplicationEventPublisherAware获取依赖:

@Component
public class EmailsReceivedEventPublisher implements ApplicationEventPublisherAware {
    private ApplicationEventPublisher applicationEventPublisher;
	// ...
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }
}

当然,像上边这样为某个时间封装一个事件发布用的 bean 并非必须,你完全可以用自己喜欢的方式调用ApplicationEventPublisher.publish来发送事件,但封装事件发布可以让代码更清晰易用。

Listener

下面需要添加我们自定义事件的监听器,具体方式是向 ApplicationContext 中添加一个 bean,并实现ApplicationListener接口:

@Component
public class EmailsReceivedEventListener implements ApplicationListener<EmailsReceivedEvent> {
    @Override
    public void onApplicationEvent(EmailsReceivedEvent event) {
        List<String> addresses = event.getEmails().stream()
                .map(Email::address)
                .collect(Collectors.toList());
        System.out.printf("收到多个电子邮件:%s%n", addresses);
    }
}

接口ApplicationListener是泛型的,所以这里实现的onApplicationEvent方法的参数是类型安全的,换言之该方法只会处理EmailsReceivedEvent类型的已发布事件。

最后,我们使用ApplicationRunner进行测试:

@Configuration
public class WebConfig {
    @Autowired
    private EmailsReceivedEventPublisher emailsReceivedEventPublisher;
    @Bean
    ApplicationRunner applicationRunner(){
        return args -> {
            List<Email> emails = List.of(
                    new Email("tom@qq.com","123"),
                    new Email("lilei@qq.com","hello"),
                    new Email("hanmeimei@qq.com","good day"));
            emailsReceivedEventPublisher.publish(emails);
        };
    }
}

关于ApplicationRunner,可以阅读我的这篇文章。

注解驱动

从 Spring 4.2 开始,Spring 提供以注解的方式来定义事件监听器:

@Component
public class EmailsReceivedEventListener {
    @EventListener
    public void handleEvent(EmailsReceivedEvent event) {
        List<String> addresses = event.getEmails().stream()
                .map(Email::address)
                .collect(Collectors.toList());
        System.out.printf("收到多个电子邮件:%s%n", addresses);
    }
}

@EventListener注解标记的方法将作为事件处理方法,且 Spring 会根据参数(事件)的类型来确定该方法处理何种事件。此时无需再让监听器实现ApplicationListener,事件处理方法的命名也可以更灵活。

也可以在@EventListener注解中直接指定要处理的事件类型,无论方法有没有具体事件作为参数,都可以处理该事件:

@Component
public class EmailsReceivedEventListener {
	// ...
    @EventListener(EmailsReceivedEvent.class)
    public void receivedTip(){
        System.out.println("some emails get.");
    }
}

可以在@EventListener中指定多个要处理的事件类型,可以通过这种方式让同一个方法处理多种类型的事件:

@Component
public class EmailsReceivedEventListener {
	// ...
    @EventListener({EmailsReceivedEvent.class, ContextRefreshedEvent.class})
    public void receivedTip(Object event){
        if (event instanceof EmailsReceivedEvent){
            System.out.println("some emails get.");
        }
        else if (event instanceof ContextRefreshedEvent){
            System.out.println("ApplicationContext is refreshed.");
        }
        else{
            ;
        }
    }
}

过滤

如果在测试代码中添加上下面的语句:

emailsReceivedEventPublisher.publish(Collections.emptyList());

我们就会看到下面的输出:

收到多个电子邮件:[tom@qq.com, lilei@qq.com, hanmeimei@qq.com]
收到多个电子邮件:[]

显然,这是因为没有在事件处理器中判断邮件队列是否为空导致的。当然,加上相应的判断很容易,但相比之下,我们有个更简单的实现方式——使用@EventListener注解对事件进行过滤:

@Component
public class EmailsReceivedEventListener {
    @EventListener(condition = "#erEvent.getEmails().isEmpty() == false")
    public void handleEvent(EmailsReceivedEvent erEvent) {
        List<String> addresses = erEvent.getEmails().stream()
                .map(Email::address)
                .collect(Collectors.toList());
        System.out.printf("收到多个电子邮件:%s%n", addresses);
    }
	// ...
}

@EventListenercondition属性是一个SpEL,这个 SpEL 的评估结果将决定是否执行事件处理方法。在这个示例中,#erEvent表示事件处理方法的erEvent参数(事件对象),利用这个参数可以构建“邮件列表不为空”的SpEL表达式,即#erEvent.getEmails().isEmpty() == false

  • 注意,这里的事件处理方法形参命名改为erEvent,而不是之前的event,这是因为在这里的SpEL中,event是一个预定义的变量,指代当前事件。
  • 可以通过多种方式定义SpEL表达式,比如上边的示例,也可以是!#erEvent.getEmails().isEmpty()
  • 关于 SpEL 的更多介绍,可以阅读我的另一篇文章。

新的事件

使用注解声明事件处理方法的另一个额外好处是,事件处理方法可以返回一个值,作为“新的事件”。

看下面这个示例:

public class WasteEmailsReceivedEvent extends ApplicationEvent {
    @Getter
    private final List<Email> emails;

    public WasteEmailsReceivedEvent(Object source, @NonNull List<Email> emails) {
        super(source);
        this.emails = emails;
    }
}

@Component
public class WasteEmailsReceivedEventListener {
    @EventListener(condition = "!#werEvent.getEmails().isEmpty()")
    public void handleEvent(WasteEmailsReceivedEvent werEvent){
        werEvent.getEmails().forEach(email -> System.out.printf("将邮件%s移入垃圾邮件%n", email));
    }
}

WasteEmailsReceivedEvent是一个表示收到了垃圾邮件的事件,WasteEmailsReceivedEventListener监听器负责处理该事件。

用一个 bean 来判断某个邮件是否为黑名单中的邮件:

@Component
public class EmailBlacklist {
    private final Set<String> addresses = Set.of("lilei@qq.com");

    public boolean inBlackList(String address){
        return addresses.contains(address);
    }
}

修改邮件接收事件的监听器,检查收到的邮件中是否有在黑名单中的,如果有,就返回相应的垃圾邮件事件:

@Component
public class EmailsReceivedEventListener {
    @Autowired
    private EmailBlacklist emailBlacklist;

    @EventListener(condition = "!#erEvent.getEmails().isEmpty()")
    public WasteEmailsReceivedEvent handleEvent(EmailsReceivedEvent erEvent) {
        List<String> addresses = erEvent.getEmails().stream()
                .map(Email::address)
                .collect(Collectors.toList());
        System.out.printf("收到多个电子邮件:%s%n", addresses);
        List<Email> wasteEmails = erEvent.getEmails().stream()
                .filter(email -> emailBlacklist.inBlackList(email.address()))
                .toList();
        if (wasteEmails.isEmpty()) {
            return null;
        } else {
            return new WasteEmailsReceivedEvent(this, wasteEmails);
        }
    }
    // ...
}

因为垃圾邮件事件监听器中加了空邮件过滤,所以这里其实可以不用判断wasteEmails是否为空,直接返回new WasteEmailsReceivedEvent(this, wasteEmails),示例中这样做是为了展示在不需要产生新消息的情况下可以返回一个null

现在运行示例,因为有一个垃圾邮件lilei@qq.com,所以处理EmailsReceivedEvent的监听器会产生一个新的WasteEmailsReceivedEvent事件,后者也会继续触发监听器WasteEmailsReceivedEventListener

如果需要在事件处理方法中生成多个后续事件,可以返回一个包含多个事件的容器(Collection)或者数组:

@Component
public class EmailsReceivedEventListener {
	// ...
    @EventListener(condition = "!#erEvent.getEmails().isEmpty()")
    public List<WasteEmailsReceivedEvent> handleEvent(EmailsReceivedEvent erEvent) {
        // ...
        return wasteEmails.stream()
                .map(email -> new WasteEmailsReceivedEvent(EmailsReceivedEventListener.this, List.of(email)))
                .collect(Collectors.toList());
    }
    // ...
}

这个示例中,为每个垃圾邮件单独生成一个WasteEmailsReceivedEvent事件,并返回一个WasteEmailsReceivedEvent事件组成的List

Event

从 Spring 4.2 开始,不再强制要求自定义事件必须继承自ApplicationEvent,因此我们的示例可以改写为:

public class EmailsReceivedEvent {
    @Setter
    @Getter
    private List<Email> emails = Collections.emptyList();
    private final Object source;

    public EmailsReceivedEvent(Object source) {
        this.source = source;
    }
}

相应的,ApplicationEventPublisher接口也添加了一个发布Object类型事件的重载方法:

@FunctionalInterface
public interface ApplicationEventPublisher {
    default void publishEvent(ApplicationEvent event) {
        this.publishEvent((Object)event);
    }

    void publishEvent(Object event);
}

事务绑定事件

从 Spring 4.2 开始,我们可以使用一个特殊的@TransactionalEventListener,利用它可以监听事务特定阶段产生的事件。

@TransactionalEventListener@EventListener的扩展。

看一个示例:

@Data
@TableName("email")
public class EmailEntity {
    @TableId(type = IdType.AUTO)
    private long id;
    @TableField
    @NonNull
    private String address;
    @TableField
    @NonNull
    private String content;

    public Email toEmail() {
        return new Email(this.getAddress(), this.getContent());
    }
}

public interface EmailMapper extends BaseMapper<EmailEntity> {
}

public class EmailAddEvent extends ApplicationEvent {
    @Getter
    private final EmailEntity email;

    public EmailAddEvent(Object source, EmailEntity email) {
        super(source);
        this.email = email;
    }
}

@Service
public class EmailService {
    @Autowired
    private EmailMapper emailMapper;
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    /**
     * 将电子邮件批量添加到持久层
     */
    @Transactional
    public void addEmails(List<Email> emails){
        this.addEmailEntities(emails.stream()
                .map(Email::toEmailEntity)
                .collect(Collectors.toList()));
    }

    @Transactional
    public void addEmailEntities(List<EmailEntity> emailEntities){
        emailEntities.forEach(emailEntity -> {
            eventPublisher.publishEvent(new EmailAddEvent(EmailService.this, emailEntity));
        });
        emailEntities.forEach(emailEntity -> emailMapper.insert(emailEntity));
    }
}

注意,这里事件发布动作必须在数据库插入相关API调用之前,否则事务回滚后产生的异常会阻止事件发布相关代码的运行,也就无法监听和处理事务回滚后的相关事件。

示例中使用 MyBatisPlus 向持久层批量加入电子邮件记录,每条记录添加时都会发送一个EmailAddEvent事件,表示添加电邮记录这个行为的发生。

因为是批量插入,为了确保数据库的数据一致性,我们需要使用事务,在这里就是用@Transactional标记批量插入方法。

  • 因为 Spring 事务是通过 AOP 实现,所以关联的"自调用"方法(这里是addEmails)同样要用@Transactional标记。
  • 要运行这个示例,需要添加数据库相关依赖,并且连接上一个数据库。
  • 关于如何在 Spring Boot 中使用 MyBatisPlus 和数据库,可以看我的相关文章:
    • 从零开始 Spring Boot 3:数据库 - 红茶的个人站点 (icexmoon.cn)
    • 从零开始 Spring Boot 4:Mybatis Plus - 红茶的个人站点 (icexmoon.cn)

现在,如果所有数据成功插入,就会正常提交事务,否则就会触发事务回滚,数据库会恢复到插入前。

创建一个监听器用于监听在这个事务中所发布的事件:

@Component
public class EmailAddEventListener {
    @TransactionalEventListener
    public void addSuccess(EmailAddEvent eaEvent){
        System.out.printf("Email %s is already add to db.%n", eaEvent.getEmail());
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void addFail(EmailAddEvent eaEvent){
        System.out.printf("Email %s add to db failed.%n", eaEvent.getEmail());
    }
}

addSuccess用于监听事务成功提交后事务产生的EmailAddEvent事件,addFail用于监听事务回滚后事务产生的EmailAddEvent事件。

实际上是通过@TransactionalEventListenerphase属性决定在事务的哪个阶段触发事件监听:

  • AFTER_COMMIT,事务成功提交,默认值。
  • AFTER_ROLLBACK,事务失败,回滚。
  • AFTER_COMPLETION,事务完成(无论失败还是成功)。
  • BEFORE_COMMIT,事务提交之前。

可以用下边的表结构进行测试:

CREATE TABLE `email` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `address` varchar(255) NOT NULL,
  `content` text NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `address_idx` (`address`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

这个表的address字段设置了唯一索引,如果重复添加相同的数据,就会触发事务回滚以及相应事件监听。

要说明的是,TransactionalEventListener仅用于监听事务中发布的事件,如果没有事务也就不起作用。

排序

可以用@Order对监听器进行排序,以确保某个监听器在另一个之前被调用:

@Component
public class EmailsReceivedEventListener {
    @Autowired
    private EmailBlacklist emailBlacklist;

    @Order(2)
    @EventListener(condition = "!#erEvent.getEmails().isEmpty()")
    public List<WasteEmailsReceivedEvent> handleEvent(EmailsReceivedEvent erEvent) {
        // ...
    }

    @Order(1)
    @EventListener({EmailsReceivedEvent.class, ContextRefreshedEvent.class})
    public void receivedTip(Object event) {
        // ...
    }
}

数字越小越先被执行。

异步

默认情况下,Spring 的事件模型是同步的(单线程),这样的好处是事件发布和监听都是顺序执行的,并且可以很容易地在事件中返回新的事件来触发新的后续处理。此外,前边介绍的事务绑定事件也只能在这种情况下生效。

我们看下边的示例:

@Component
public class WasteEmailsReceivedEventListener {
    @EventListener(condition = "!#werEvent.getEmails().isEmpty()")
    public void handleEvent(WasteEmailsReceivedEvent werEvent) throws InterruptedException {
        Thread.sleep(1000);
        werEvent.getEmails().forEach(email -> System.out.printf("将邮件%s移入垃圾邮件%n", email));
    }
}

现在,处理垃圾邮件的监听器每次执行都要等待1秒,运行测试很容易能看到这种“迟滞”。

@Async

如果每封垃圾邮件移入回收站这个动作都可以并行执行,那我们就可以用异步执行(@Async)来改善性能:

@EnableAsync
public class WebConfig {
    // ...
}

@Component
public class WasteEmailsReceivedEventListener {
    @Async
    @EventListener(condition = "!#werEvent.getEmails().isEmpty()")
    public void handleEvent(WasteEmailsReceivedEvent werEvent) throws InterruptedException {
        // ...
    }
}

再次运行就能看到垃圾处理过程是有多么的迅速。

需要注意的是,因为是异步执行,所以如果事件处理方法中产生异常,调用方是无法捕获这个异常的。此外,异步执行的时候也不能通过返回事件的方式发布新的事件,而是需要手动发布事件:

public class WasteEmailRemovedEvent extends ApplicationEvent {
    @Getter
    private final Email email;
    public WasteEmailRemovedEvent(Object source, Email email) {
        super(source);
        this.email = email;
    }
}

@Component
public class WasteEmailsReceivedEventListener {
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    @Async
    @EventListener(condition = "!#werEvent.getEmails().isEmpty()")
    public void handleEvent(WasteEmailsReceivedEvent werEvent) throws InterruptedException {
        // ...
        werEvent.getEmails().forEach(email -> {
            this.eventPublisher.publishEvent(new WasteEmailRemovedEvent(WasteEmailsReceivedEventListener.this, email));
        });
    }
}

@Component
public class WasteEmailRemovedEventListener {
    @EventListener
    public void eventHandler(WasteEmailRemovedEvent werEvent){
        System.out.printf("Email %s is already removed.%n", werEvent.getEmail());
    }
}

ApplicationEventMulticaster

除了使用@Async让事件处理方法异步执行外,我们还可以修改事件模型的默认策略,让所有的事件监听都异步进行,比如:

@Configuration
public class WebConfig {
	// ...
    @Bean(name = "applicationEventMulticaster")
    public ApplicationEventMulticaster simpleApplicationEventMulticaster() {
        SimpleApplicationEventMulticaster eventMulticaster =
                new SimpleApplicationEventMulticaster();
        eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
        return eventMulticaster;
    }
}

此时并没有用@EnableAsync开启异步相关注解,也没有用@Async标记相应方法,但运行示例就能发现所有事件监听都很快发生,几乎没有延迟。

这样做会导致事务绑定事件(@TransactionalEventListener)无法正常使用。

Application Event

Sprring 本身就定义了很多事件,用于内部的处理,比如 Spring 在启动阶段产生的ContextRefreshedEvent等,我们可以用同样的方式监听这些事件,以在特定阶段执行某些任务。

相关的内容我在这篇文章中有过介绍,这里不过多赘述。

泛型事件

可以在定义一个有泛型参数的事件,并利用泛型来区分事件并监听:

public class GenericMsgEvent<T> extends ApplicationEvent {
    @Getter
    private final T msg;

    public GenericMsgEvent(Object source, T msg) {
        super(source);
        this.msg = msg;
    }
}

@Component
public class GenericMsgEventListener {
    @EventListener
    public void strEventHandler(GenericMsgEvent<String> gmEvent){
        System.out.printf("String msg event is get, msg:%s.%n", gmEvent.getMsg());
    }

    @EventListener
    public void intEventHandler(GenericMsgEvent<Integer> gmEvent){
        System.out.printf("Int msg event is get, msg:%s.%n", gmEvent.getMsg());
    }
}

这里两个方法strEventHandlerintEventHandler分别用于泛型参数是String和泛型参数是Integer时的事件监听。

看起来很不错,但实际上有一个“陷阱”,假如你像下边这样发布事件:

eventPublisher.publishEvent(new GenericMsgEvent<String>(this, "hello"));

实际上并不会触发任何事件监听,这是因为new GenericMsgEvent<String>(this, "hello")中的泛型String仅存在于编译阶段,运行时会对泛型进行类型擦除,实际上这里相当于GenericMsgEvent<Object>,所以不会触发针对泛型参数是StringInteger定义的监听器。

关于 Java 泛型中的类型擦除,可以阅读我的这篇文章。

所以,使用这类事件的正确方式是派生出一个特定类型后发布该类型的事件,比如:

public class IntMsgEvent extends GenericMsgEvent<Integer>{
    public IntMsgEvent(Object source, Integer msg) {
        super(source, msg);
    }
}

public class StringMsgEvent extends GenericMsgEvent<String> {
    public StringMsgEvent(Object source, String msg) {
        super(source, msg);
    }
}

现在可以用这两个类型发布事件:

eventPublisher.publishEvent(new StringMsgEvent(this, "hello"));
eventPublisher.publishEvent(new IntMsgEvent(this, 11));

这两个事件都会被对应的监听器方法监听到。

当然,这种方式多少有些“无趣”,并且不得不定义大量派生类型,为此,Spring 给我们提供了一种额外方式:

public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {
    @Getter
    private final T entity;

    public EntityCreatedEvent(Object source, T entity) {
        super(source);
        this.entity = entity;
    }

    @Override
    public ResolvableType getResolvableType() {
        ResolvableType resolvableType = ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(entity));
        return resolvableType;
    }
}

在这里,泛型事件EntityCreatedEvent<T>实现了ResolvableTypeProvider接口,并且在方法getResolvableType中返回一个“确切的”泛型类型。

在前文中我提到过ResolvableType这个类型,Spring 可以通过它来确认泛型的具体类型。

监听器的写法与之前的示例一致,这里不再赘述。

现在无需使用任何派生类,直接使用泛型事件进行发布:

eventPublisher.publishEvent(new EntityCreatedEvent<>(this, new Email("123@tomcom","sdfdsf")));

The End,谢谢阅读。

本文中的完整示例可以从这里获取。

参考资料

  • 从零开始 Spring Boot 3:数据库 - 红茶的个人站点 (icexmoon.cn)
  • 从零开始 Spring Boot 4:Mybatis Plus - 红茶的个人站点 (icexmoon.cn)
  • 标准和自定义事件
  • Spring Events | Baeldung

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

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

相关文章

sqli-labs靶场通关(21-30)

Less-21 还是adminadmin登录 可以看出uname是base64加密过的&#xff0c;解码得到&#xff1a;admin。 那么本关和less-20相似&#xff0c;只是cookie的uname值经过base64编码了。 抓包看一下也是如此 那么我们只需要上传paylaod的时候base64加密一下就可以了 base64加密工…

多线程-线程的创建的方式3、4:实现Callable与线程池

JDK5.0新增线程创建方式 简要概况&#xff1a; 1. 创建多线程的方式三&#xff1a;实现Callable&#xff08;jdk5.0新增的&#xff09;与之前的方式的对比&#xff1a;与Runnable方式的对比的好处 > call()可以有返回值&#xff0c;更灵活 > call()可以使用throws的方式…

模板模式(十六)

相信自己&#xff0c;请一定要相信自己 上一章简单介绍了代理模式(十五), 如果没有看过, 请观看上一章 一. 模板模式 引用 菜鸟教程里面的 模板模式介绍: https://www.runoob.com/design-pattern/template-pattern.html 在模板模式&#xff08;Template Pattern&#xff09;…

简要介绍 | 三维点云配准:理论、方法与挑战

三维点云配准&#xff1a;理论、方法与挑战 注&#xff1a;”简要介绍“系列仅从概念上对某一领域进行非常简要的介绍&#xff0c;不适合用于深入和详细的了解 三维点云配准 是计算机视觉和机器人领域的重要课题&#xff0c;涉及从不同视角或时间点采集的三维点云数据之间寻找最…

面试半年,总结了 1000 道 2023 年 Java 架构师岗面试题

半年前还在迷茫该学什么&#xff0c;怎样才能走出现在的困境&#xff0c;半年后已经成功上岸阿里&#xff0c;感谢在这期间帮助我的每一个人。 面试中总结了 1000 道经典的 Java 面试题&#xff0c;里面包含面试要回答的知识重点&#xff0c;并且我根据知识类型进行了分类&…

Linux5.5 Mysql索引、事务与存储引擎

文章目录 计算机系统5G云计算第四章 LINUX Mysql索引、事务与存储引擎一、Mysql索引1. 索引的概念2.索引的作用3.索引的副作用4.创建索引的原则依据5.索引的分类和创建1&#xff09;普通索引2&#xff09;唯一索引3&#xff09;主键索引4&#xff09;组合索引5&#xff09;全文…

验证attention是否在图像分类问题上起决定性作用

来源&#xff1a;投稿 作者&#xff1a;摩卡 编辑&#xff1a;学姐 Motivation 现阶段出现了大量的Transformer-style图像分类模型&#xff0c;并且这些模型在ImageNet上取得了不俗的成绩&#xff0c;这些Transformer-style模型将取得高性能的功劳归功于Multi-head attention注…

【软件设计】模块设计耦合的其中类型

一.什么是高内聚、低耦合&#xff1f; 在结构化分析与模块设计方法中&#xff0c;模块化是一个很重要的概念&#xff0c;它是将一个待开发的软件分解成为若干个小的模块&#xff0c;每个模块可以独立地开发、测试。使得复杂问题的“分而治之”&#xff0c;令程序的结构清晰、易…

vue3+router4的基本使用

一、安装router npm i vue-router二、路由跳转 2.1 创建路由实例 在src目录下创建router文件夹&#xff0c;在其中创建一个index.js文件&#xff0c;创建路由实例。 通过vue-router的createRouter方法创建一个router对象。其中有history和routes 1.history&#xff1a; histo…

微机保护的数据采集系统(2)

&#xff08;二&#xff09;采样保持电路&#xff08;S&#xff0f;H&#xff09;和模拟低通滤波器&#xff08;ALF&#xff09; 1&#xff0e;采样保持电路&#xff08;S&#xff0f;H&#xff09; &#xff08;1&#xff09;采样保持原理。 S&#xff0f;H电路的作用是在一个…

Linux进程间通信 - 共享内存

之前的文章中我们讲述了匿名管道与命名管道相关的知识点&#xff0c;在本文中我们将继续讲述一种进程间通信的方式&#xff1a;共享内存。 systemV共享内存 共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间&#xff0c;这些进程间数据传递不再涉及到…

Downie 4 4.6.18 MAC上最新最好用的一款视频下载工具

Downie for Mac 简介 Downie是Mac下一个简单的下载管理器&#xff0c;可以让您快速将不同的视频网站上的视频下载并保存到电脑磁盘里然后使用您的默认媒体播放器观看它们。 Downie 4 下载 Downie 4 for Mac Downie 4 for Mac软件特点 支持许多站点 -当前支持1000多个不同的…

间接采购管理中常见的五大挑战

间接采购&#xff0c;有时也被称为间接费用或尾部支出&#xff0c;这些商品或服务不是制造产品直接必需的&#xff0c;而是日常运营所需的。 ● 办公室和行政用品 ● 商店地点的消耗品&#xff08;例如&#xff0c;清洁用品&#xff09; ● 设施管理费用 ● 专业服务 ● 旅行…

Linux 服务器重启之后执行指定脚本文件

有些时候&#xff0c;我们部署服务、中间件、数据库等应用的机器可能会因为机房停电、断电而宕机&#xff0c;这样大部分服务就随之关闭了&#xff0c;可能会需要手动去进行重启&#xff0c;我们可以通过 Linux 的开机启动来实现服务自动重启。 一、配置 /etc/rc.d/rc.local 文…

VOSviewer软件的基础与应用

VOSviewer是一款免费且专业的文献计量分析软件&#xff0c;也是一个知识图谱可视化工具&#xff0c;由荷兰莱顿大学开发&#xff0c;主要用于构建和查看文献计量知识图谱&#xff0c;基于文献的共引和共被引原理&#xff0c;具有可视化能力强、适合于大规模样本数据的特点&…

抖音矩阵系统源代码开发部署--源码搭建

抖音矩阵系统是一个具有强大功能的开放性平台&#xff0c;通过数据挖掘技术能够实现精准的用户画像和个性化推荐&#xff0c;这也是抖音成为国内最受欢迎的短视频平台之一的原因之一。矩阵系统的开发需要大量的技术支持和数据分析&#xff0c;同时也需要综合运用大数据、机器学…

Mysql主从复制及读写分离

&#x1f353; 简介&#xff1a;java系列技术分享(&#x1f449;持续更新中…&#x1f525;) &#x1f353; 初衷:一起学习、一起进步、坚持不懈 &#x1f353; 如果文章内容有误与您的想法不一致,欢迎大家在评论区指正&#x1f64f; &#x1f353; 希望这篇文章对你有所帮助,欢…

华为OD机试 JavaScript 实现【密码验证合格程序】【牛客练习题 HJ20】,附详细解题思路

一、题目描述 密码要求: 长度超过8位包括大小写字母.数字.其它符号,以上四种至少三种不能有长度大于2的包含公共元素的子串重复 &#xff08;注&#xff1a;其他符号不含空格或换行&#xff09; 二、输入描述 一组字符串。 三、输出描述 如果符合要求输出&#xff1a;OK&…

A stochastic parrot in every pot 每口锅里都会有一只随机鹦鹉? | 经济学人社论双语精翻

本期双语文章来自《经济学人》2023年5月13日周报社论&#xff1a;《人工智能经济学》&#xff08;The economics of AI&#xff09; A stochastic parrot in every pot? 每口锅里都会有一只随机鹦鹉&#xff1f; What does a leaked Google memo reveal about the future of A…

软文营销,如何写出优质的新闻稿

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 一&#xff0c;什么是软文营销&#xff1f; 软文营销是一种通过撰写和发布具有信息性、有趣性和可读性的文章&#xff0c;来推广产品、服务或品牌的营销策略。软文是指以一种较为隐晦、…