JSF 2.3+CDI:登录会话刷新与会话作用域Bean处理疑问
这确实是CDI会话作用域和传统JSF托管Bean之间的常见坑点——当你失效HTTP会话后,CDI不会自动在当前请求里刷新会话Bean的实例,导致原来的代码拿不到新Bean。下面分享几个实用的解决方案:
方案一:通过CDI BeanManager手动获取新实例
如果你必须在当前请求里完成会话Bean的初始化,可以直接借助CDI的核心API BeanManager 来创建新的会话Bean实例。失效会话后,新的HTTP会话会在首次访问会话资源时被创建,我们可以通过BeanManager从新会话的上下文里获取Bean:
@Named("loginbean") @ViewScoped public class LoginBean implements Serializable { private String username; private String password; @Inject private SessionManager sessionManager; @Inject private PrivilegeManager privilegeManager; @Inject private BeanManager beanManager; // 注入BeanManager public String doLogin() { String destinationUrl; if (validate(username, password)) { FacesContext context = FacesContext.getCurrentInstance(); // 失效当前HTTP会话 context.getExternalContext().invalidateSession(); // 手动查找并创建新的SessionBean实例 Set<Bean<?>> beans = beanManager.getBeans(SessionBean.class); if (!beans.isEmpty()) { Bean<SessionBean> sessionBeanBean = (Bean<SessionBean>) beanManager.resolve(beans); CreationalContext<SessionBean> creationalContext = beanManager.createCreationalContext(sessionBeanBean); SessionBean newSession = sessionBeanBean.create(creationalContext); // 设置用户信息 newSession.setUsername(username); newSession.setSessionId(sessionManager.createNewSession(username)); newSession.setPrivileges(privilegeManager.getPrivileges(username)); } destinationUrl = createLandingPageUrl(); } else { destinationUrl = createFailureUrl("Unknown user or password"); } return destinationUrl; } }
这个方法的核心是绕过EL表达式的缓存,直接通过CDI容器获取绑定到新会话的Bean实例。CreationalContext 一般由CDI容器管理生命周期,这里手动创建后不需要额外销毁,因为会话Bean会和新HTTP会话绑定,容器会自动处理后续的销毁逻辑。
方案二:重定向到初始化请求(推荐)
更符合CDI设计理念的做法是把登录流程拆成两步:先失效会话,再通过重定向触发新请求,在新请求里初始化会话Bean。这样CDI会自动在新请求中创建绑定到新会话的Bean实例,完全符合上下文生命周期规则:
第一步:修改LoginBean,失效会话并重定向
@Named("loginbean") @ViewScoped public class LoginBean implements Serializable { private String username; private String password; @Inject private SessionManager sessionManager; @Inject private PrivilegeManager privilegeManager; public String doLogin() { String destinationUrl; if (validate(username, password)) { FacesContext context = FacesContext.getCurrentInstance(); // 失效当前HTTP会话 context.getExternalContext().invalidateSession(); // 用Flash作用域跨重定向传递用户信息(不会暴露在URL里) context.getExternalContext().getFlash().put("loginUsername", username); // 重定向到初始化会话的动作 destinationUrl = "/initializeSession?faces-redirect=true"; } else { destinationUrl = createFailureUrl("Unknown user or password"); } return destinationUrl; } }
第二步:创建会话初始化Bean
@Named @RequestScoped public class InitializeSessionBean { @Inject private SessionBean session; @Inject private SessionManager sessionManager; @Inject private PrivilegeManager privilegeManager; public String initialize() { FacesContext context = FacesContext.getCurrentInstance(); // 从Flash作用域取出用户名 String username = (String) context.getExternalContext().getFlash().get("loginUsername"); // 初始化会话Bean session.setUsername(username); session.setSessionId(sessionManager.createNewSession(username)); session.setPrivileges(privilegeManager.getPrivileges(username)); // 跳转到登录后的首页 return createLandingPageUrl(); } }
配置导航规则
在faces-config.xml里添加导航规则,让/initializeSession指向初始化方法:
<navigation-rule> <from-view-id>/initializeSession</from-view-id> <navigation-case> <from-outcome>success</from-outcome> <to-view-id>/landingPage.xhtml</to-view-id> <redirect/> </navigation-case> </navigation-rule>
这个方案的优势是完全遵循CDI的上下文管理机制,代码更简洁,也避免了手动操作BeanManager的潜在问题。
为什么原来的JSF表达式方法失效?
在CDI环境下,evaluateExpressionGet会优先从当前请求的EL上下文里查找Bean实例——即使你已经失效了HTTP会话,当前请求的EL上下文里还保留着旧SessionBean的引用,所以返回的还是原来的实例。而CDI的会话Bean是严格绑定到HTTP会话的,只有当新请求进来(或者手动通过BeanManager从新会话上下文获取),才能拿到新的实例。
内容的提问来源于stack exchange,提问作者Didjit

