SpringMVC之监听器(Listener)

一、概述

Listenerservlet规范中定义的一种特殊类。用于监听servletContextHttpSessionservletRequest等域对象的创建和销毁事件。监听域对象的属性发生修改的事件。用于在事件发生前、发生后做一些必要的处理。一般是获取在线人数等业务需求。

如做完某一件事情以后,需要广播一些消息或者通知,告诉其他的模块进行一些事件处理,一般来说,可以一个一个发送请求去通知,但是有一种更好的方式,那就是事件监听,事件监听也是设计模式中发布-订阅模式、观察者模式的一种实现。

观察者模式:

简单的来讲就是你在做事情的时候身边有人在盯着你,当你做的某一件事情是旁边观察的人感兴趣的事情的时候,他会根据这个事情做一些其他的事,但是盯着你看的人必须要到你这里来登记,否则你无法通知到他(或者说他没有资格来盯着你做事情)。

二、定义监听器

Spring提供了两种实现方式:

  • ApplicationListener接口
  • @EventListener注解
  • SmartApplicationListener接口:继承于ApplicationListener接口,支持事件类型过滤和顺序控制

要想顺利的创建监听器,并起作用,这个过程中需要这样几个角色:

  1. 事件(event)可以封装和传递监听器中要处理的参数,如对象或字符串,并作为监听器中监听的目标。
  2. 监听器(listener)具体根据事件发生的业务处理模块,这里可以接收处理事件中封装的对象或字符串。
  3. 事件发布者(publisher)事件发生的触发者。

三、ApplicationListener接口的实现形式

ApplicationListener接口的定义如下:

java 复制代码
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {

    /**
     * Handle an application event.
     * @param event the event to respond to
     */
    void onApplicationEvent(E event);
}

它是一个泛型接口,泛型的类型必须是ApplicationEvent及其子类,只要实现了这个接口,那么当容器有相应的事件触发时,就能触发onApplicationEvent方法**(自动去执行)**。ApplicationEvent类的子类有很多.

3.1 简单使用1

使用方法很简单,就是实现一个ApplicationListener接口,并且将加入到容器中就行。

这里监听的是Application启动时候的事情。

java 复制代码
@Component
public class MyApplicationListener implements ApplicationListener<ApplicationEvent> {

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        System.out.println("事件触发:" + event.getClass().getName());
    }
} 

然后启动自己的springboot项目:

java 复制代码
@SpringBootApplication
@ServletComponentScan
@EnableAsync
@EnableTransactionManagement
public class SpringBootApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootApplication.class, args);
    }
}

结果

事件触发非注解的方式:org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent

3.2 简单使用2 自定义事件和监听器

定义事件

首先,我们需要定义一个事件(MyTestEvent),需要继承SpringApplicationEvent

java 复制代码
public class MyTestEvent extends ApplicationEvent {

    private String msg;

    public MyTestEvent(Object source, String msg) {
        super(source);
        this.msg = msg;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

定义监听器

需要定义一下监听器,自己定义的监听器需要实现ApplicationListener,同时泛型参数要加上自己要监听的事件Class名,在重写的方法onApplicationEvent中,添加自己的业务处理:

当监听到具体的时间的时候,会自动调用onApplication方法

java 复制代码
@Component
public class MyNoAnnotationListener implements ApplicationListener<MyTestEvent> {

    @Override
    public void onApplicationEvent(MyTestEvent event) {
        System.out.println("非注解监听器:" + event.getMsg());
    }
}

事件发布

有了事件,有了事件监听者,那么什么时候触发这个事件呢?

每次想让监听器收到事件通知的时候,就可以调用一下事件发布的操作。首先在类里自动注入ApplicationEventPublisher,这个也就是我们的ApplicationContext,它实现了这个接口。

java 复制代码
@Component
public class MyTestEventPublisher {
    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    /**
     *  事件发布方法
     */
    public void pushListener(String msg) {
        applicationEventPublisher.publishEvent(new MyTestEvent(this, msg));
    }
}

测试

用一个http请求来模拟

java 复制代码
@RestController
public class TestEventListenerController1 {

    @Autowired
    private MyTestEventPubLisher publisher;

    @RequestMapping(value = "/test/testPublishEvent111")
    public void testPublishEvent() {
        publisher.pushListener("我来了!");
    }
}

启动项目,可以看到控制台输出,测试完成:

text 复制代码
事件触发:com.test.personal.unannotation.MyTestEvent // 这个是上面那个事件监控的的输出
非注解监听器:我来了!

四、EventLister的使用

4.1 使用实例

新建事件类PersonUpdateEvent

java 复制代码
@Component
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PersonUpdateEvent {
    private Integer id;
    private String name;
    private Integer age;
}

PersonSaveEvent

java 复制代码
@Component
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PersonSaveEvent {
    private Integer id;
    private String name;
    private Integer age;
}

4.2.1 单一事件监听器

发布事件

java 复制代码
@Service
public class EventPublisher {

    private ApplicationEventPublisher eventPublisher;

    @Autowired
    public void setEventPublisher(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public void publishPersonSaveEvent() {
        PersonSaveEvent saveEvent = new PersonSaveEvent();
        saveEvent.setId(1);
        saveEvent.setName("i余数");
        saveEvent.setAge(18);
        eventPublisher.publishEvent(saveEvent);
    }
}

监听事件

java 复制代码
@Slf4j
@Service
public class EventListenerService {

    @EventListener
    public void handleForPersonSaveEvent(PersonSaveEvent saveEvent) {
        log.info("saveEvent -> {}", saveEvent);
    }
}

结果

text 复制代码
saveEvent -> PersonSaveEvent(id=1, name=i余数, age=18)

4.2.2 使用classes实现多事件监听器

发布事件

在上一个示例的基础上,再多加一个PersonUpdateEvent事件。

java 复制代码
public void publishPersonUpdateEvent() {
    PersonUpdateEvent updateEvent = new PersonUpdateEvent();
    updateEvent.setId(1);
    updateEvent.setName("i余数");
    updateEvent.setAge(19);
    eventPublisher.publishEvent(updateEvent);
}

监听事件

java 复制代码
@EventListener(classes = {PersonSaveEvent.class, PersonUpdateEvent.class})
public void handleForPersonSaveAndUpdateEvent(Object event) {
    log.info("multi handle event -> {}", event);
}

验证结果

可以监听到多个事件

text 复制代码
multi handle event -> PersonSaveEvent(id=1, name=i余数, age=18)
multi handle event -> PersonUpdateEvent(id=1, name=i余数, age=19)

4.2.3 使用condition筛选监听的事件

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

发布事件

java 复制代码
public void publishPersonSaveEvent() {
    PersonSaveEvent saveEvent = new PersonSaveEvent();
    saveEvent.setId(1);
    saveEvent.setName("i余数");
    saveEvent.setAge(18);
    eventPublisher.publishEvent(saveEvent);

    PersonSaveEvent saveEvent2 = new PersonSaveEvent();
    saveEvent2.setId(2);
    saveEvent2.setName("i余数");
    saveEvent2.setAge(18);
    eventPublisher.publishEvent(saveEvent2);
}

监听事件

java 复制代码
public void publishPersonSaveEvent() {
    PersonSaveEvent saveEvent = new PersonSaveEvent();
    saveEvent.setId(1);
    saveEvent.setName("i余数");
    saveEvent.setAge(18);
    eventPublisher.publishEvent(saveEvent);

    PersonSaveEvent saveEvent2 = new PersonSaveEvent();
    saveEvent2.setId(2);
    saveEvent2.setName("i余数");
    saveEvent2.setAge(18);
    eventPublisher.publishEvent(saveEvent2);
}

结果验证

id2的事件不满足条件,所以不会执行。

text 复制代码
只处理id等于1的 -> PersonSaveEvent(id=1, name=i余数, age=18)

4.3 有返回值的监听器

4.3.1 返回一个单一对象

发布事件

java 复制代码
public void publishPersonSaveEvent() {
    PersonSaveEvent saveEvent = new PersonSaveEvent();
    saveEvent.setId(1);
    saveEvent.setName("i余数");
    saveEvent.setAge(18);
    eventPublisher.publishEvent(saveEvent);
}

监听事件

java 复制代码
@EventListener
public void handleForPersonUpdateEvent(PersonUpdateEvent updateEvent) {
    log.info("handle update event -> {}", updateEvent);
}

@EventListener
public PersonUpdateEvent handleHaveReturn(PersonSaveEvent saveEvent) {
    log.info("handle save event -> {}", saveEvent);
    PersonUpdateEvent updateEvent = new PersonUpdateEvent();
    updateEvent.setId(saveEvent.getId());
    updateEvent.setName(saveEvent.getName());
    updateEvent.setAge(saveEvent.getAge());
    return updateEvent;
}

验证结果

可以看到我们监听到了2个事件,PersonSaveEvent是我们主动发布的事件,PersonUpdateEventhandleHaveReturn方法的返回值,会被Spring自动当作一个事件被发送。

text 复制代码
handle save event -> PersonSaveEvent(id=1, name=i余数, age=18)
handle update event -> PersonUpdateEvent(id=1, name=i余数, age=18)

4.3.2 返回一个集合

将监听器稍作修改,使其返回一个集合。

java 复制代码
@EventListener
public List<PersonUpdateEvent> handleHaveReturn(PersonSaveEvent saveEvent) {
    log.info("handle save event -> {}", saveEvent);
    List<PersonUpdateEvent> events = new ArrayList<>();
    PersonUpdateEvent updateEvent = new PersonUpdateEvent();
    updateEvent.setId(saveEvent.getId());
    updateEvent.setName(saveEvent.getName());
    updateEvent.setAge(saveEvent.getAge());
    events.add(updateEvent);

    PersonUpdateEvent updateEvent2 = new PersonUpdateEvent();
    BeanUtils.copyProperties(updateEvent, updateEvent2);
    events.add(updateEvent2);
    return events;
}

查看结果可以发现,集合中的每个对象都被当作一个单独的事件进行发送。

text 复制代码
handle save event -> PersonSaveEvent(id=1, name=i余数, age=18)
handle update event -> PersonUpdateEvent(id=1, name=i余数, age=18)
handle update event -> PersonUpdateEvent(id=1, name=i余数, age=18)

4.3.3 返回一个数组

和返回值为集合一样,数组中的每个对象都被当作一个单独的事件进行发送。

4.4 异步监听器

创建两个监听器,一个同步一个异步,异步监听器就是在方法上加一个@Async注解,但需要注意以下限制:

  • 监听器报错不会传递给事件发起者,因为双方已经不在同一个线程了。
  • 异步监听器的非空返回值不会被当作新的事件发布。如果需要发布新事件,需要注入ApplicationEventPublisher后手动发布。
java 复制代码
@SpringBootApplication
@ServletComponentScan
@EnableAsync
@EnableTransactionManagement
public class SpringBootApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootApplication.class, args);
    }
}
@EventListener
public void handleForPersonSaveEvent(PersonSaveEvent saveEvent) {
    log.info("handle event -> {}", saveEvent);
}

@Async
@EventListener
public void handleForPersonSaveEventAsync(PersonSaveEvent saveEvent) {
    log.info("async handle event -> {}", saveEvent);
}

从执行结果可以看出,异步线程是task-1,不是主线程main,即异步是生效的。

text 复制代码
handle event -> PersonSaveEvent(id=1, name=i余数, age=18)
async handle event -> PersonSaveEvent(id=1, name=i余数, age=18)

4.5 监听器异常处理

4.5.1 同步异常处理

使用SimpleApplicationEventMulticaster处理同步监听器抛出异常。先定义一个ErrorHandler:

java 复制代码
@Slf4j
@Component
public class MyErrorHandler implements ErrorHandler {
    @Override
    public void handleError(Throwable t) {
        log.info("handle error -> {}", t.getClass());
    }
}

将自定义ErrorHandler绑定到SimpleApplicationEventMulticaster这里涉及一个@PostConstruce

java 复制代码
@Slf4j
@Service
public class EventListenerService {

    @Autowired
    private SimpleApplicationEventMulticaster simpleApplicationEventMulticaster;

    @Autowired
    private MyErrorHandler errorHandler;

    @PostConstruct
    public void init() {
        simpleApplicationEventMulticaster.setErrorHandler(errorHandler);
    }

    @Order(1)
    @EventListener
    public void handleForPersonSaveEvent(PersonSaveEvent saveEvent) throws AuthException {
        log.info("handle event -> {}", saveEvent);
        throw new AuthException("test exception");
    }
}

结果:可以看到捕获的异常是UndeclaredThrowableException

text 复制代码
handle event -> PersonSaveEvent(id=1, name=i余数, age=18)
handle error -> class java.lang.reflect.UndeclaredThrowableException

4.5.2 异步异常处理

使用SimpleAsyncUncaughtExceptionHandler来处理@Async抛出的异常。

java 复制代码
@Configuration
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new SimpleAsyncUncaughtExceptionHandler();
    }
}

监听器代码:人为的抛出一个异常。

java 复制代码
@Async
@EventListener
public void handleForPersonSaveEvent(PersonSaveEvent saveEvent) throws AuthException {
    log.info("handle event -> {}", saveEvent);
    throw new AuthException("test exception");
}

结果:SimpleAsyncUncaughtExceptionHandler捕获到了@Async方法抛出的异常

text 复制代码
INFO 4416 - [task-1] i.k.s.e.listener.EventListenerService: handle event -> PersonSaveEvent(id=1, name=i余数, age=18)
ERROR 4416 - [task-1] .a.i.SimpleAsyncUncaughtExceptionHandler: Unexpected exception occurred invoking async method: 
    public void xxxx.handleForPersonSaveEvent(xxxx.PersonSaveEvent) throws javax.security.auth.message.AuthException

4.6 监听器排序

如果同时有多个监听器监听同一个事件,默认情况下监听器的执行顺序是随机的,如果想要他们按照某种顺序执行,可以借助Spring的另外一个注解@Order实现。

创建三个监听器,并使用@Order排好序。

java 复制代码
@Order(1)
@EventListener
public void handleForPersonSaveEvent(PersonSaveEvent saveEvent) {
    log.info("handle event1 -> {}", saveEvent);
}

@Order(2)
@EventListener
public void handleForPersonSaveEvent2(PersonSaveEvent saveEvent) {
    log.info("handle event2 -> {}", saveEvent);
}

@Order(3)
@EventListener
public void handleForPersonSaveEvent3(PersonSaveEvent saveEvent) {
    log.info("handle event3 -> {}", saveEvent);
}

从执行结果可以看到,确实是按照@Order中指定的顺序执行的。

text 复制代码
handle event1 -> PersonSaveEvent(id=1, name=i余数, age=18)
handle event2 -> PersonSaveEvent(id=1, name=i余数, age=18)
handle event3 -> PersonSaveEvent(id=1, name=i余数, age=18)

五、使用场景

5.1 应用启动时缓存预热(系统事件监听)

需求描述: 在应用启动完成后,自动加载热门商品数据到Redis缓存,提升接口响应速度。

java 复制代码
@Component
public class CacheWarmUpListener {

    private final ProductService productService;
    private final RedisTemplate<String, Product> redisTemplate;

    @Autowired
    public CacheWarmUpListener(ProductService productService,
                               RedisTemplate<String, Product> redisTemplate) {
        this.productService = productService;
        this.redisTemplate = redisTemplate;
    }

    @EventListener(ApplicationReadyEvent.class)
    public void warmUpCache() {
        List<Product> hotProducts = productService.getTop100HotProducts();
        hotProducts.forEach(product ->
                redisTemplate.opsForValue().set("product:" + product.getId(), product));

        System.out.println("=== 已预热" + hotProducts.size() + "条商品数据到Redis ===");
    }
}

关键点说明:

  • 使用ApplicationReadyEvent而非ApplicationStartedEvent,确保数据库连接等基础设施已就绪
  • 通过构造函数注入依赖,避免字段注入的循环依赖问题
  • 预热数据量较大时建议采用分页异步加载

5.2 订单创建后发送多平台通知(自定义事件)

需求描述: 当订单创建成功后,需要同时发送短信通知用户、邮件通知客服、更新ERP系统库存。

步骤1:定义自定义事件

java 复制代码
public class OrderCreatedEvent extends ApplicationEvent {
    private final Order order;

    public OrderCreatedEvent(Object source, Order order) {
        super(source);
        this.order = order;
    }

    public Order getOrder() {
        return order;
    }
}

步骤2:在Service中发布事件

java 复制代码
@Service
public class OrderService {

    private final ApplicationEventPublisher eventPublisher;

    @Autowired
    public OrderService(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    @Transactional
    public Order createOrder(OrderCreateRequest request) {
        Order newOrder = createNewOrder();
        eventPublisher.publishEvent(new OrderCreatedEvent(this, newOrder));
        return newOrder;
    }
}

步骤3:多监听器处理事件

java 复制代码
@Component
public class OrderNotificationListener {

    // 短信通知(最高优先级)
    @EventListener
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public void sendSms(OrderCreatedEvent event) {
        Order order = event.getOrder();
        SmsService.send(order.getUserPhone(),
                "您的订单#" + order.getId() + "已创建,金额:" + order.getAmount());
    }

    // 邮件通知(异步处理)
    @Async
    @EventListener
    public void sendEmail(OrderCreatedEvent event) {
        Order order = event.getOrder();
        EmailTemplate template = EmailTemplate.buildOrderConfirm(order);
        EmailService.send(template);
    }

    // ERP系统库存更新(条件过滤)
    @EventListener(condition = "#event.order.items.?[isPhysicalProduct].size() > 0")
    public void updateErpInventory(OrderCreatedEvent event) {
        ERPInventoryService.updateStock(event.getOrder().getItems());
    }
}

配置异步支持:

java 复制代码
@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "notificationTaskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("Notification-");
        executor.initialize();
        return executor;
    }
}

优势:

  • 解耦核心业务与通知逻辑
  • 通过@Order控制短信优先于邮件发送
  • 使用@Async避免邮件发送阻塞主线程
  • 条件表达式跳过虚拟商品库存更新

5.3 全局请求耗时统计(ServletRequestListener)

需求描述: 统计所有API请求的处理时间,识别慢接口。

java 复制代码
@Component
public class RequestMetricsListener implements ServletRequestListener {

    private static final ThreadLocal<Long> startTimeHolder = new ThreadLocal<>();

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        startTimeHolder.set(System.currentTimeMillis());
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        long startTime = startTimeHolder.get();
        long duration = System.currentTimeMillis() - startTime;

        HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
        String endpoint = request.getRequestURI();
        String method = request.getMethod();

        MetricsService.recordRequestMetrics(endpoint, method, duration);

        // 慢请求预警
        if (duration > 3000) {
            AlarmService.notifySlowRequest(endpoint, method, duration);
        }

        startTimeHolder.remove();
    }
}

注册监听器:

java 复制代码
@Bean
public ServletListenerRegistrationBean<RequestMetricsListener> metricsListener() {
    return new ServletListenerRegistrationBean<>(new RequestMetricsListener());
}

统计结果示例:

text 复制代码
GET /api/products 平均耗时 45ms | 95分位 120ms
POST /api/orders 平均耗时 250ms | 最大耗时 3200ms(需优化)

5.4 应用优雅停机(ContextClosedEvent)

需求描述:在应用关闭时,确保完成:1)停止接收新请求 2)等待进行中的任务完成 3)释放资源。

java 复制代码
@Component
public class GracefulShutdownListener implements ApplicationListener<ContextClosedEvent> {

    private final ThreadPoolTaskExecutor taskExecutor;
    private final DataSource dataSource;

    @Autowired
    public GracefulShutdownListener(ThreadPoolTaskExecutor taskExecutor,
                                    DataSource dataSource) {
        this.taskExecutor = taskExecutor;
        this.dataSource = dataSource;
    }

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        // 1. 关闭线程池
        shutdownExecutor(taskExecutor);

        // 2. 关闭数据库连接池
        if (dataSource instanceof HikariDataSource) {
            ((HikariDataSource) dataSource).close();
        }

        // 3. 其他清理工作...
        System.out.println("=== 资源释放完成,应用安全退出 ===");
    }

    private void shutdownExecutor(ExecutorService executor) {
        executor.shutdown();
        try {
            if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
}

停机流程**:**

  1. 收到SIGTERM信号
  2. 关闭新请求入口
  3. 等待30秒处理进行中请求
  4. 强制关闭剩余任务
  5. 释放数据库连接池
  6. 应用退出

5.5 分布式锁异常恢复

需求描述: 当获取Redis分布式锁失败时,触发重试机制并记录竞争情况。

自定义事件

java 复制代码
public class LockAcquireFailedEvent extends ApplicationEvent {
    private final String lockKey;
    private final int retryCount;

    public LockAcquireFailedEvent(Object source, String lockKey, int retryCount) {
        super(source);
        this.lockKey = lockKey;
        this.retryCount = retryCount;
    }

    // getters...
}

事件发布

java 复制代码
public class DistributedLock {

    private final ApplicationEventPublisher eventPublisher;

    public boolean tryLock(String key, int maxRetries) {
        int attempts = 0;
        while (attempts < maxRetries) {
            if (RedisClient.acquireLock(key)) {
                return true;
            }
            attempts++;
            eventPublisher.publishEvent(new LockAcquireFailedEvent(this, key, attempts));
            Thread.sleep(100 * attempts);
        }
        return false;
    }
}

监听处理

java 复制代码
@Component
public class LockFailureHandler {

    private static final Map<String, AtomicInteger> LOCK_CONTENTION = new ConcurrentHashMap<>();

    @EventListener
    public void handleLockFailure(LockAcquireFailedEvent event) {
        String lockKey = event.getLockKey();
        LOCK_CONTENTION.computeIfAbsent(lockKey, k -> new AtomicInteger(0))
                .incrementAndGet();

        // 竞争激烈时动态调整策略
        if (event.getRetryCount() > 3) {
            adjustBackoffStrategy(lockKey);
        }
    }

    @Scheduled(fixedRate = 10_000)
    public void reportContention() {
        LOCK_CONTENTION.forEach((key, count) ->
                MetricsService.recordLockContention(key, count.get()));
    }

    private void adjustBackoffStrategy(String key) {
        // 动态增加等待时间或告警
    }
}

六、总结

  • 过滤器:用于属性甄别,对象收集(不可改变过滤对象的属性和行为)
  • 监听器:用于对象监听,行为记录(不可改变监听对象的属性和行为)
  • 拦截器:用于对象拦截,行为干预(可以改变拦截对象的属性和行为)
相关推荐
Lear5 小时前
SpringBoot之自动装配
后端
karry_k5 小时前
Redis如何搭建搭建一主多从?
后端·面试
用户5975653371105 小时前
【Java多线程与高并发系列】第2讲:核心概念扫盲:进程 vs. 线程
后端
Lear5 小时前
SpringBoot异步编程
后端
间彧5 小时前
Java LongAdder详解与应用实战
后端
Lear5 小时前
Spring MVC 拦截器与过滤器的区别及执行顺序
后端
Lear5 小时前
SpringMVC之过滤器(Filter)
后端
WoodWall5 小时前
WebServer 02 Reactor模式
c++·后端
Tech有道5 小时前
滴滴面经分享:那个让我差点翻车的Spring面试题!
后端