托管Bean

托管Bean(ManagedBean)是JSF中的一种特殊类型的JavaBean,它充当了在JSF应用程序中视图和业务逻辑之间的桥梁,这篇笔记我们详细学习有关托管Bean的用法。

定义和注册托管Bean

JSF中实际上我们有两种方式注册托管Bean,我们可以基于注解注册,也可以基于XML配置注册。

基础注解注册托管Bean

下面代码基于注解方式注册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

除了使用注解,我们也完全可以使用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和CDI的依赖注入

在使用JSF框架时,实际上JSF本身提供了@ManagedBean@ManagedProperty等注解用于托管Bean的注册和依赖注入,然而CDI实际上也提供了这些功能,在CDI中对应的注解是@Named@Inject等,使用这些注解可以让JSF框架将托管Bean交给CDI容器管理,在之前的例子中,我们也一直使用的都是CDI注解。

总而言之,如果你的项目只使用JSF框架没有使用其他的JavaEE技术,那么使用JSF的注解是没问题的;然而通常我们的工程都还会用到许多其他框架,因此直接统一使用CDI是一种最佳实践;一般来说我们尽量不要混用两种注解,以避免潜在的冲突和混乱。

关于托管Bean的命名

托管Bean的命名规则和JavaBean的命名规则类似,即遵循驼峰命名法,从经验角度看命名为XxxViewXxxManagedBeanXxxBeanXxxMB的工程都有,甚至有按不同托管Bean的功能采用不同命名后缀的都有,所以也没有一个固定的约束或“最佳”实践,我们选择一种遵循项目规范且可读性较好的即可。

从视图到托管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>

托管Bean的作用域

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()方法,如果用户未登录就跳转回到登录页。

@PostConstruct

托管Bean中,我们可以使用@PostConstruct标注一个方法,该方法会在托管Bean实例化后立即执行,常用于初始化操作。

@Named("demo")
@RequestScoped
public class DemoManagedBean {
    @PostConstruct
    public void init() {
        System.out.println("init");
    }
}

托管Bean的作用域

JSF托管Bean默认是SesssionScope的,即托管Bean会在会话期间内缓存实例。

@Named("demo")
@RequestScoped
public class DemoManagedBean {
    @PostConstruct
    public void init() {
        System.out.println("init");
    }
}

引用其它托管Bean

还是考虑上面登录的例子,如果我有另一个托管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中,反之则不行。

使用FacesContext和ExternalContext

JSF中,FacesContextExternalContext是两个核心类,用于处理Web应用程序的请求和响应。

FacesContext对象是通过FacesContext.getCurrentInstance()方法获取的,它允许你与当前正在处理的JSF请求进行交互,例如访问请求参数、会话数据、应用程序范围的属性等。

FacesContext context = FacesContext.getCurrentInstance();

ExternalContext是一个抽象类,提供了访问Servlet环境的方法,如获取请求和响应对象、获取Web应用程序的真实路径等。ExternalContext对象通常通过FacesContextgetExternalContext()方法获取。

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");
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap