托管Bean(ManagedBean)是JSF中的一种特殊类型的JavaBean,它充当了在JSF应用程序中视图和业务逻辑之间的桥梁,这篇笔记我们详细学习有关托管Bean的用法。
JSF中实际上我们有两种方式注册托管Bean,我们可以基于注解注册,也可以基于XML配置注册。
下面代码基于注解方式注册JSF托管Bean,这种方式写法简洁,推荐使用。
DemoManagedBean.java
package com.gacfox.demo.demojsf.controller;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
@Named("demo")
@RequestScoped
public class DemoManagedBean {
private String data = "Hello, JSF!";
public String getData() {
return data;
}
}
其中,@Named
是一个CDI注解,它标识了该类是托管Bean,以及该托管Bean的名字;@RequestScoped
标识了该托管Bean的作用域是请求作用域,其中的数据只会在一个请求流程中缓存。
除了使用注解,我们也完全可以使用XML方式配置托管Bean,下面是一个例子。
DemoManagedBean.java
package com.gacfox.demo.demojsf.controller;
public class DemoManagedBean {
private String data = "Hello, JSF!";
public String getData() {
return data;
}
}
WEB-INF/faces-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<faces-config xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_3.xsd"
version="2.3">
<managed-bean>
<managed-bean-name>demo</managed-bean-name>
<managed-bean-class>com.gacfox.demo.demojsf.controller.DemoManagedBean</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
</managed-bean>
</faces-config>
不过一般来说我们没必要使用XML方式配置托管Bean,注解方式更加简洁并且代码可读性更高。
在使用JSF框架时,实际上JSF本身提供了@ManagedBean
、@ManagedProperty
等注解用于托管Bean的注册和依赖注入,然而CDI实际上也提供了这些功能,在CDI中对应的注解是@Named
、@Inject
等,使用这些注解可以让JSF框架将托管Bean交给CDI容器管理,在之前的例子中,我们也一直使用的都是CDI注解。
总而言之,如果你的项目只使用JSF框架没有使用其他的JavaEE技术,那么使用JSF的注解是没问题的;然而通常我们的工程都还会用到许多其他框架,因此直接统一使用CDI是一种最佳实践;一般来说我们尽量不要混用两种注解,以避免潜在的冲突和混乱。
托管Bean的命名规则和JavaBean的命名规则类似,即遵循驼峰命名法,从经验角度看命名为XxxView
、XxxManagedBean
、XxxBean
、XxxMB
的工程都有,甚至有按不同托管Bean的功能采用不同命名后缀的都有,所以也没有一个固定的约束或“最佳”实践,我们选择一种遵循项目规范且可读性较好的即可。
当我们访问一个JSF视图时,例如/demo.faces
,根据我们在web.xml
中注册的FacesServlet
匹配规则,服务端就会展示demo.xhtml
视图,根据视图中定义的数据关联EL表达式如#{demo.data}
,JSF框架就会寻找名为demo
的托管Bean,并调用其GET方法读取data
属性。
DemoManagedBean.java
package com.gacfox.demo.demojsf.controller;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
@Named("demo")
@RequestScoped
public class DemoManagedBean {
private String data = "Hello, JSF!";
public String getData() {
return data;
}
}
demo.xhtml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "
http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<h:head>
<title>JSF Demo</title>
</h:head>
<h:body>
<h:outputText value="#{demo.data}" />
</h:body>
</html>
JSF中托管Bean有以下几种作用域:
Request Scope:每次HTTP请求都会创建一个新的实例,请求结束后实例销毁。
View Scope:实例与页面生命周期相同,适用于单个页面内的数据共享。以一个现代的Web开发者视角来看ViewScope
理解起来可能有些困难,具体来说,它在一次GET请求发生时生效,只要浏览器还停留在当前页面该作用域就一直生效,只有当调用的方法返回非null
(或void方法)时失效,因为此时触发了服务端的转发或重定向,浏览器将离开当前页面。ViewScope
用于缓存页面级的数据,你可以理解为类似于React中一个组件或页面的state
,缓存的数据服务于UI界面,只不过对于JSF这个后端框架来说数据缓存在Java服务端。
Session Scope:该作用域会关联会话创建多个实例,在用户会话期间保持实例存在,直到用户注销或会话过期。
Application Scope:整个应用程序生命周期内保持实例存在,直到应用停止,它是全局单例的。
托管Bean的作用域涉及托管Bean在服务端缓存的数据,我们需要在合适的场景选择合适的Scope,对于简单的查询请求我们应该使用RequestScope,当前页面有数据需要缓存时则需要使用ViewScope,对于类似购物车等功能则倾向于选择SessionScope,对于全局数据则可能采用ApplicationScope。
不过这里对于新手来说可能存在一个误区,一个常见的Web应用程序可能包含提交数据、回显数据两步,但这并不意味着我们要用SessionScope的托管Bean保存数据,我们仍应该使用RequestScope,因为数据通常存储在数据库中,此时托管Bean负责向数据库写入数据以及从数据库查询数据回显,托管Bean本身不缓存任何数据。
下面例子中我们继续改造第一章节的登录例子,这里我们使用SessionScope
将用户的登录信息缓存起来。
package com.gacfox.demo.demojsf.controller;
import com.gacfox.demo.demojsf.model.User;
import com.gacfox.demo.demojsf.service.UserService;
import javax.enterprise.context.SessionScoped;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.io.Serializable;
@Named("login")
@SessionScoped
public class LoginManagedBean implements Serializable {
@Inject
private UserService userService;
private String username;
private String password;
private boolean loggedIn = false;
private String errMessage;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public boolean getLoggedIn() {
return loggedIn;
}
public void setLoggedIn(boolean loggedIn) {
this.loggedIn = loggedIn;
}
public String getErrMessage() {
return errMessage;
}
public void setErrMessage(String errMessage) {
this.errMessage = errMessage;
}
public String login() {
User user = userService.queryUserByUsername(username);
if (user != null && user.getPassword().equals(password)) {
loggedIn = true;
return "success.faces?faces-redirect=true";
} else {
errMessage = "用户名或密码错误!";
return "login";
}
}
public void checkLoginStatus() {
FacesContext facesContext = FacesContext.getCurrentInstance();
ExternalContext externalContext = facesContext.getExternalContext();
if (!loggedIn) {
try {
externalContext.redirect(externalContext.getRequestContextPath() + "/login.faces");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
上面代码中,LoginManagedBean
是一个SessionScope
的托管Bean,它会在会话期间内缓存实例。当我们登录成功后,loggedIn
字段会被设置为true
供其它业务逻辑判定用户的登录状态,用户名字段也会保持缓存相应的信息。此外代码中我们还添加了一个checkLoginStatus
方法,它用于判断当前是否已登陆,如果未登录就跳回登录页。
此外还要注意,对于那些需要序列化来缓存的托管Bean需要实现Serializable
接口,SessionScope
的托管Bean即使如此。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "
http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<h:head>
<title>JSF Demo</title>
</h:head>
<h:body>
<f:metadata>
<f:event type="preRenderView" listener="#{login.checkLoginStatus()}"/>
</f:metadata>
你好:<h:outputText value="#{login.username}"/>
</h:body>
</html>
登陆成功后,浏览器被重定向到了success.xhtml
视图,这里我们引用了LoginManagedBean
中缓存的信息,由于它是会话期间内缓存的,因此我们无论刷新页面还是新打开标签页,用户名都能正确回显。然而如果我们清空Cookie中的SessionID,登录状态就会失效。
这里我们使用了preRenderView
在视图生成前先执行login.checkLoginStatus()
方法,如果用户未登录就跳转回到登录页。
托管Bean中,我们可以使用@PostConstruct
标注一个方法,该方法会在托管Bean实例化后立即执行,常用于初始化操作。
@Named("demo")
@RequestScoped
public class DemoManagedBean {
@PostConstruct
public void init() {
System.out.println("init");
}
}
JSF托管Bean默认是SesssionScope
的,即托管Bean会在会话期间内缓存实例。
@Named("demo")
@RequestScoped
public class DemoManagedBean {
@PostConstruct
public void init() {
System.out.println("init");
}
}
还是考虑上面登录的例子,如果我有另一个托管Bean,它需要从LoginManagedBean
中读取一些信息,此时我们可以直接通过@Inject
这个CDI注解来注入LoginManagedBean
。
package com.gacfox.demo.demojsf.controller;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;
@Named("demo")
@RequestScoped
public class DemoManagedBean {
@Inject
private LoginManagedBean loginManagedBean;
public String getDataFromSession() {
return loginManagedBean.getUsername();
}
}
虽然托管Bean之间是可以互相通过依赖注入引用数据的,但通常来说,我们只能注入相同或更长作用域的托管Bean,例如可以将一个SessionScoped的托管Bean注入到一个 RequestScoped的托管Bean中,反之则不行。
JSF中,FacesContext
和ExternalContext
是两个核心类,用于处理Web应用程序的请求和响应。
FacesContext
对象是通过FacesContext.getCurrentInstance()
方法获取的,它允许你与当前正在处理的JSF请求进行交互,例如访问请求参数、会话数据、应用程序范围的属性等。
FacesContext context = FacesContext.getCurrentInstance();
ExternalContext
是一个抽象类,提供了访问Servlet环境的方法,如获取请求和响应对象、获取Web应用程序的真实路径等。ExternalContext
对象通常通过FacesContext
的getExternalContext()
方法获取。
FacesContext context = FacesContext.getCurrentInstance();
ExternalContext externalContext = context.getExternalContext();
通过这两个类,你可以在JSF应用程序中访问并操作Servlet环境以及JSF请求和响应的信息从而实现更多功能,例如处理表单提交、管理会话、重定向等。
类似Servlet,JSF中也存在转发(Forward)和重定向(Redirect)的区别,前面其实我们已经使用过很多了。
转发是指在服务端将请求交给下一个组件处理,在浏览器端看到的URL不会发生改变;而重定向则是一个HTTP301或302响应,请求新页面是由浏览器发出的。
JSF中转发写法很简单,托管Bean中的方法返回一个视图名字符串,其实此时就是转发方式。而重定向则是一种固定的写法,形如success.faces?faces-redirect=true
,JSF托管Bean中方法返回该字符串时,将触发重定向。此外,我们也可以使用ExternalContext
实现重定向。
externalContext.redirect(externalContext.getRequestContextPath() + "/login.faces");