第一个RAG项目遇到的问题

前言

我们也是做了AI项目的"高级开发"了😱,当然还是API无脑调用咯😂。调用API过程中还是遇到一个问题。

就是我们在和AI对话的时候,数据是流式响应的,接口的响应类型就不是普通的json了,返回的是流式响应了text/event-stream,这时候控制台就报错了。今天就分享一下这个问题怎么解决的吧!

不想看解决过程的,直接点击目录中的解决方案

响应event-stream 后台报错问题

问题描述

  1. 先看看controller层伪代码吧
java 复制代码
@PostMapping(value = "/chat")
public SseEmitter chat(@RequestBody paream req) {
    
    SseEmitter emitter = new SseEmitter(5 * 60 * 1000L);
    // 使用自定义线程池执行异步任务
    ExecutorService executor = Executors.newCachedThreadPool();
    executor.execute(() -> {
        try {
            service.chat(req,emitter);
        } catch (Exception e) {
            emitter.completeWithError(e);
        } finally {
            executor.shutdown();
        }
    });
    return emitter;
}
  1. 看看报错日志吧,报错日志有以下两段
    没有对应的SecurityManager
js 复制代码
org.apache.shiro.UnavailableSecurityManagerException:   
No SecurityManager accessible to the calling code,   
either bound to the org.apache.shiro.util.ThreadContext or as a vm static singleton.   
This is an invalid application configuration.
	at org.apache.shiro.SecurityUtils.getSecurityManager(SecurityUtils.java:123)
..........................

无法将LinkedHashMap类型的数据转换为text/event-stream格式的响应:

js 复制代码
org.springframework.http.converter.HttpMessageNotWritableException: No converter for [class org.jeecg.common.api.vo.Result] with preset Content-Type 'text/event-stream'
	at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:312)
	at 
        ................................
 org.apache.shiro.web.servlet.AbstractShiroFilter$1.call(AbstractShiroFilter.java:373)
	at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:90)
	at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:83)
	at org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:387)
	at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:370)
	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:154)

解决方案

先看解决方案吧,有兴趣的可以看看解决过程,很有意思的代码

java 复制代码
@Bean
public FilterRegistrationBean shiroFilterRegistration() {
    FilterRegistrationBean registration = new FilterRegistrationBean();
    registration.setFilter(new DelegatingFilterProxy("shiroFilterFactoryBean"));
    registration.setEnabled(true);
    //需要处理的请求路径
    registration.addUrlPatterns("/chatsManage/chat");
    //支持异步
    registration.setAsyncSupported(true);
    registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
    return registration;
}

解决过程

首先肯定是把问题丢给AI,这次AI并没有给到我正确的答案

从上面的报错日志来看,应该是和shiro脱不了关系。

1. 为什么会出现No SecurityManager accessible to the calling code

先定位到报错的代码如下:

java 复制代码
    public static SecurityManager getSecurityManager() throws UnavailableSecurityManagerException {
        SecurityManager securityManager = ThreadContext.getSecurityManager();
        if (securityManager == null) {
            securityManager = SecurityUtils.securityManager;
        }
        if (securityManager == null) {
            String msg = "No SecurityManager accessible to the calling code, either bound to the " +
                    ThreadContext.class.getName() + " or as a vm static singleton.  This is an invalid application " +
                    "configuration.";
            throw new UnavailableSecurityManagerException(msg);
        }
        return securityManager;
    }
}

简单的理解就是当前线程没有绑定SecurityManager

因为我们的 controller 层 是通过多线层去操作的,难道是子线程没有自动的去绑定这个 shiro相关的上下文❔

❌我在controller层的父子线程分别打印了 SecurityManager 发现都打印了,那就排除了这种情况

因为不懂SseEmitter的内部原理,子线程应该只是把数据 丢给了SseEmitterSseEmitter 再把数据响应给客户(这个过程干了什么,我也不清楚),也能证明上面的猜想是不对的。

✔只能定位报错时候的线程,看看和父线程 和 子线程有没有什么联系?

controller打印的线程名称 : http-nio-8071-exec-4
controller层异步处理的子线程名称 : pool-24-thread-1
报错日志的线程 : http-nio-8071-exec-6

2025-08-15 16:35:19.515 [http-nio-8071-exec-6] ERROR.......

1.1 定位关键问题

下面提到的 shiro 上下文,本文指得是上下文(ThreadContext)中的资源 比如SecurityManager

通过观察上面的线程名称,发现进来的线程名称:http-nio-8071-exec-4和 返回数据时候报错的线程名称:http-nio-8071-exec-6不一致,中途shiro的上下文相关信息息就丢失了.

emitter.complete();在执行完成的时候,会去处理我们的响应,这时候就会把的处理 丢给tomact线程池去处理,tomcat线程池获取到这个响应任务的时候,shiro的上下文就丢失了。

👼这个时候疑问就来了,为什么在controller层,异步的时候能获取到shiro的上下文,异步线程再提交给tomact线程池的时候,上下文就丢失了呢

org.apache.tomcat.util.net.processSocket():

2 shiro上下文如何在异步线程中丢失的呢

上面已经定位到问题,是因为异步响应的时候,没有获取到shiro的上下文。但是奇怪的是为什么controller中自己开启的异步中又能获取到shiro SecurityManager呢,目前就要搞清楚shiro上下文资源在异步中是如何传递的❔

代码如下

java 复制代码
public abstract class ThreadContext {

    /**
     * Private internal log instance.
     */
    private static final Logger log = LoggerFactory.getLogger(ThreadContext.class);

    public static final String SECURITY_MANAGER_KEY = ThreadContext.class.getName() + "_SECURITY_MANAGER_KEY";
    public static final String SUBJECT_KEY = ThreadContext.class.getName() + "_SUBJECT_KEY";
   // securityManager 和 subject 两个资源都是存到 resource中的
    private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap<Map<Object, Object>>();
    ............................

📖可以看到resouces是被InheritableThreadLocalMap包装的,也就在父线程中创建子线程的时候会自动复制被它包装的资源。

InheritableThreadLocalMap 继承的是 java.lang.InheritableThreadLocal,不清楚的可以看我这篇文章必须掌握的【InheritableThreadLocal】

💫了解了InheritableThreadLocal之后,shiro的上下文资源在线程池中去获取,本身就存在问题❗。线程池中线程会被复用,同时shiro的上下文资源也会被复用,在线程池中创建的时候,会把父线程的InheritableThreadLocal复制,如果创建时候没有,那么后面就获取不到,如果创建的时候拿到了,后续线程也一直复用这个资源,也会存在问题。

3.解决shiro上下文资源,在异步请求中的问题

controller能获取到用户信息,是因为 shiro的过滤器对请求除了处理,把相关信息进行了set。异步响应的时候 绑定的信息就丢失了,所以我们在异步响应的时候,再执行拦截器的逻辑就能重新绑定shiro上下文信息了。

java 复制代码
@Bean
public FilterRegistrationBean shiroFilterRegistration() {
    FilterRegistrationBean registration = new FilterRegistrationBean();
    registration.setFilter(new DelegatingFilterProxy("shiroFilterFactoryBean"));
    registration.setEnabled(true);
    //需要处理的请求路径
    registration.addUrlPatterns("/chatsManage/chat");
    //支持异步
    registration.setAsyncSupported(true);
    registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
    return registration;
}

这样就解决了,异步响应信息丢失的问题

总结

本篇文章,从异步请求报错的问题引发了对shiro 上下文资源在线程中传递的原理的探索。最后也是通过shiro的过滤器对request再次处理代码org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal()

java 复制代码
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
        throws ServletException, IOException {
       ....
    try {
        final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
        final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
        final Subject subject = createSubject(request, response);
        //noinspection unchecked
        subject.execute(new Callable() {
            public Object call() throws Exception {
                // 绑定资源
                updateSessionLastAccessTime(request, response);
                executeChain(request, response, chain);
                return null;
            }
        });
    } 
}

希望本篇文章对你有所帮助,觉得不错的可以给博主点个赞🤞

推荐阅读:必须掌握的【InheritableThreadLocal】

面试宝典:25最新面试题长期更新

微服务项目:从0到1项目实战

相关推荐
Monly216 分钟前
RabbitMQ:SpringAMQP 入门案例
spring boot·rabbitmq·java-rabbitmq
Monly219 分钟前
RabbitMQ:SpringAMQP Fanout Exchange(扇型交换机)
spring boot·rabbitmq·java-rabbitmq
kaika19 分钟前
告别复杂配置!使用 1Panel 运行环境功能轻松搭建 Java 应用
java·1panel·建站·halo
每天学习一丢丢16 分钟前
Spring Boot + Vue 项目用宝塔面板部署指南
vue.js·spring boot·后端
有梦想的攻城狮17 分钟前
Java 11中的Collections类详解
java·windows·python·java11·collections
六千江山37 分钟前
从字符串中提取符合规则的汽车车牌
java
33255_40857_280591 小时前
从韩立结婴看Java进阶:一个10年老码农的修仙式成长指南
java
赵星星5201 小时前
透彻理解Java中的深拷贝与浅拷贝:从误区到最佳实践
java·后端
心月狐的流火号1 小时前
Java CompletableFuture 核心API
java
黑客影儿1 小时前
Java技术总监的成长之路(技术干货分享)
java·jvm·后端·程序人生·spring·tomcat·maven