前言
我们也是做了AI项目的"高级开发"了😱,当然还是API无脑调用咯😂。调用API过程中还是遇到一个问题。
就是我们在和AI对话的时候,数据是流式响应的,接口的响应类型就不是普通的json
了,返回的是流式响应了text/event-stream
,这时候控制台就报错了。今天就分享一下这个问题怎么解决的吧!
不想看解决过程的,直接点击目录中的解决方案
响应event-stream 后台报错问题
问题描述
- 先看看
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;
}
- 看看报错日志吧,报错日志有以下两段
没有对应的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
的内部原理,子线程应该只是把数据 丢给了SseEmitter
,SseEmitter
再把数据响应给客户(这个过程干了什么,我也不清楚),也能证明上面的猜想是不对的。
✔只能定位报错时候的线程,看看和父线程 和 子线程有没有什么联系?
controller打印的线程名称 : http-nio-8071-exec-4
controller层异步处理的子线程名称 : pool-24-thread-1
报错日志的线程 : http-nio-8071-exec-62025-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项目实战