第一个RAG项目遇到的问题

0 阅读5分钟

前言

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

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

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

响应event-stream 后台报错问题

问题描述

  1. 先看看controller层伪代码吧
@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
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格式的响应:

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)

解决方案

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

@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并没有给到我正确的答案

image.png

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

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

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

    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的上下文就丢失了。

image.png

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

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

8aad2b201cb8637e9bb805121979ed3.png

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

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

代码如下

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上下文信息了。

@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()

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项目实战