Spring Boot中Application类HashMap变量在控制器中未更新问题求助
嘿,这个问题我之前碰到过,核心原因是Spring Boot的CGLIB代理在“搞事情”!让我给你拆解清楚:
问题根源
你的Application类加了@SpringBootApplication注解,这个注解包含了@Configuration——而Spring会对带有@Bean方法的配置类生成CGLIB代理:代理类会继承你的Application类,所以代理对象会拥有自己的sockets实例变量副本。
你在构造函数里把static app赋值为原始的Application实例,但ApplicationRunner的run方法是在代理对象上执行的!也就是说,你往代理对象的sockets里塞了元素,而控制器里通过Application.app拿到的是原始对象的sockets,那自然是空的,size就是0了。
另外提个小细节:HashMap不是线程安全的,在多线程场景下(比如控制器请求和Bean初始化并发)容易出问题,这个后面一起解决。
解决方案
这里给你三个可行的方案,按推荐程度排序:
方案1:用独立的Bean管理Socket集合(最符合Spring规范)
把Socket集合放到一个单独的组件里,通过Spring的依赖注入来获取,彻底避开static引用和代理问题:
@Component public class SocketManager { // 用线程安全的ConcurrentHashMap替代HashMap private final ConcurrentHashMap<Integer, Socket> sockets = new ConcurrentHashMap<>(); public void addSocket(Socket socket) { sockets.put(Integer.valueOf(socket.hashCode()), socket); } public ConcurrentHashMap<Integer, Socket> getSockets() { return sockets; } }
修改Application类,注入SocketManager并使用:
@SpringBootApplication public class Application extends SpringBootServletInitializer { @Bean public ApplicationRunner runner(TaskExecutor exec, SocketManager socketManager) { return args -> { // ... 你的socket创建逻辑 socketManager.addSocket(socket); System.out.println("size: " + socketManager.getSockets().size()); // 这里会是1 }; } }
控制器里也注入SocketManager:
@Controller public class NotificationController { private final SocketManager socketManager; // 构造注入(推荐) public NotificationController(SocketManager socketManager) { this.socketManager = socketManager; } @RequestMapping(method = RequestMethod.POST, value = "/test") public ResponseEntity<?> test() { System.out.println("size: " + socketManager.getSockets().size()); // 现在会是1 return ResponseEntity.ok().build(); } }
方案2:把sockets改成static变量
如果不想新增类,可以把sockets改成static的,这样代理对象和原始对象共享同一个集合:
@SpringBootApplication public class Application extends SpringBootServletInitializer { public static Application app; // 改成static+线程安全的集合 public static ConcurrentHashMap<Integer, Socket> sockets = new ConcurrentHashMap<>(); public Application() { app = this; } public ConcurrentHashMap<Integer, Socket> getSockets() { return sockets; } @Bean public ApplicationRunner runner(TaskExecutor exec) { return args -> { // ... sockets.put(Integer.valueOf(socket.hashCode()), socket); System.out.println("size: " + sockets.size()); // 1 }; } }
控制器里的代码不用改,不过这种方式不太符合Spring的依赖注入思想,static变量在大型项目里不好维护,尽量少用。
方案3:禁用CGLIB代理
在@SpringBootApplication里加上proxyBeanMethods = false,告诉Spring不要生成代理类,直接用原始对象:
@SpringBootApplication(proxyBeanMethods = false) public class Application extends SpringBootServletInitializer { // 你的原有代码不变 }
这样ApplicationRunner和控制器拿到的就是同一个实例了,不过这种方式会关闭Spring的一些特性(比如@Bean方法的循环依赖处理、Bean的懒加载优化等),只适合简单场景。
内容的提问来源于stack exchange,提问作者ThanhQB

