如何使用 JSF 2.2、JPA 2.0 和依赖注入功能设置 Google AppEngine webapp?
How do I set up a Google AppEngine webapp with JSF 2.2, JPA 2.0 and Dependency Injection features?
几周前,我被要求在 Google Cloud Platform 上为 运行 创建一个 Web 应用程序(输入此 post 时,SDK v1.9.48) .配置 AppEngine 设置(创建帐户、项目、云数据库、源代码存储库等)后,我准备使用 GAE Eclipse 插件开发我的第一个 Web 应用程序。
当我发现 GAE 默认只支持 JSP 和 servlet 时,我感到非常失望。
然后我说:"May God help me! Back to J2EE's stone age again? I'm used to JSF and (C)DI for the UI. How am I going to integrate in a GAE webapp these 3 J2EE standards and make it run smoothly (if such a thing is possible)?":
- JSF 2.x
- (C)DI 1.x
- JPA2.x
只要继续阅读本文 post,您就会知道怎么做!
好吧,我决定不那么轻易放弃并陷入困境。经过几周的艰苦研究和 trial-error 编码,我找到了这个烂摊子的解决方案。
在开始 post 之前,我将为您提供一些可以帮助您将其整合在一起的重要资源:
- Configuring JSF 2.2 to run on the Google App Engine Using Eclipse
- A workaround for a session data loss bug
- List of libraries I used (at least half of them come bundled in GAE SDK)
框架:
- Datanucleus 3.1.1 (JPA 2.0)
- Oracle Mojarra 2.2.4 (JSF 2.2)。
- Google Guice 4.0 (DI 1.0)
我是这样工作的:
最重要的配置在web.xml。 JSF 初始化 必须 运行 FIRST :我发现 com.sun.faces.config.ConfigureListener
负责该步骤,它总是寻找 FacesServlet
声明。由于 JSF 请求 必须 由 Guice 使用 FacesHttpServlet
包装器(稍后我将 post 提供)以启用 DI,因此:
我声明了 FacesServlet
WITHOUT <servlet-mapping>
s(我通过 trial-error 编码弄清楚了那一步)。
它只是声明初始化FacesContextFactory
。这是 web.xml:
的 MUST-HAVE 结构
<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<display-name>BrochureForce</display-name>
<description>Purchase orders generator configured to run on the Google AppEngine.</description>
<context-param>
<description>Project stage (Development or Production)</description>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Development</param-value>
</context-param>
<context-param>
<description>
Designate client-side state saving, since GAE doesn't handle
server side (JSF default) state management.
</description>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>client</param-value>
</context-param>
<context-param>
<description>Sets the default suffix for JSF pages to .xhtml</description>
<param-name>javax.faces.DEFAULT_SUFFIX</param-name>
<param-value>.xhtml</param-value>
</context-param>
<context-param>
<description>
When enabled, the runtime initialization and default ResourceHandler
implementation will use threads to perform their functions. Set this
value to false if threads aren't desired (as in the case of single-threaded
environments such as the Google AppEngine).
Note that when this option is disabled, the ResourceHandler will not
pick up new versions of resources when ProjectStage is development.
</description>
<param-name>com.sun.faces.enableThreading</param-name>
<param-value>false</param-value>
</context-param>
<context-param>
<description>Allows dependency-injection into ManagedBeans</description>
<param-name>com.sun.faces.injectionProvider</param-name>
<param-value>mypackage.jsf.JsfInjectionProvider</param-value>
</context-param>
<context-param>
<description>Specify JBoss Expression Language Over Default</description>
<param-name>com.sun.faces.expressionFactory</param-name>
<param-value>org.jboss.el.ExpressionFactoryImpl</param-value>
</context-param>
<!-- JSF INITIALIZATION GOES FIRST!! -->
<servlet>
<description>
JSF 2 Servlet. There's NO servlet-mapping defined for this servlet because
it's declared here in order to enforce the FacesFactory to load properly
so that an instance of this servlet can be injected in the FacesHttpServlet
used by Guice to serve JSF requests and as injection provider at the same time.
Furthermore, the "load-on-startup" property is set to "0" to tell Jetty
that this servlet MUST be loaded first.
</description>
<servlet-name>JSF Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>0</load-on-startup>
</servlet>
<listener>
<description>JSF Initialization.</description>
<listener-class>com.sun.faces.config.ConfigureListener</listener-class>
</listener>
<!-- JSF INITIALIZATION GOES FIRST!! -->
<listener>
<description>PERSISTENCE ENGINE INITIALIZATION AND SHUTDOWN.</description>
<listener-class>mypackage.listener.PersistenceManagerSetupListener</listener-class>
</listener>
<!-- ***** Specify session timeout of thirty (30) minutes. ***** -->
<session-config>
<session-timeout>30</session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>index.jsf</welcome-file>
<welcome-file>index.xhtml</welcome-file>
</welcome-file-list>
<!-- **************************************************** -->
<!-- DI API initialization (Google Guice Implementation). -->
<!-- **************************************************** -->
<filter>
<description>Google Guice filter which enables DI.</description>
<filter-name>GuiceFilter</filter-name>
<filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>GuiceFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<description>
This listener initializes the Guice injector and wraps the JSF Servlet
into a HttpServlet in order to serve JSF requests via Guice Filter.
</description>
<listener-class>mypackage.listener.GuiceListener</listener-class>
</listener>
<!-- **************************************************** -->
</web-app>
其次,我不再尝试将托管 bean 实例注入另一个实例。相反,我将绑定的业务逻辑实例注入到 bean 中(换句话说,模拟 EJB 行为)。这就是我所做的:
我为业务逻辑实现定义了一个@BindingAnnotation
:
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import com.google.inject.BindingAnnotation;
@Documented
@BindingAnnotation
@Retention(RUNTIME)
@Target({ TYPE })
public @interface BusinessLogic {}
我定义了一个业务逻辑接口及其实现,并用 @BusinessLogic
注释对两者进行了注释(这是一个注册对页面的访问的示例。字段是:访问编号、源 IP 和时间戳):
import java.util.List;
import mypackage.annotation.BusinessLogic;
import mypackage.dataaccess.entity.Visit;
@BusinessLogic
public interface VisitsHandler {
public void insertVisit();
public List<Visit> getPageVisits();
// Propiedades
public String getCurrentVisit();
public void setCurrentVisit(String currentVisit);
}
及其实现:
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import mypackage.annotation.BusinessLogic;
import mypackage.jsf.logic.VisitsHandler;
import mypackage.dataaccess.PersistenceManager;
import mypackage.dataaccess.Queries;
import mypackage.dataaccess.entity.Visit;
@BusinessLogic
public class VisitsHandlerImpl implements VisitsHandler {
private String currentVisit;
public void insertVisit() {
PersistenceManager pMgr = PersistenceManager.getInstance();
Visit newVisit = new Visit();
newVisit.setUserIp("127.0.0.1");
newVisit.setTimestamp(new Date(System.currentTimeMillis()));
pMgr.insert(newVisit);
pMgr = null; // Dereference the singleton instance.
this.currentVisit = newVisit.toString();
}
@SuppressWarnings("rawtypes")
public List<Visit> getPageVisits() {
PersistenceManager pMgr = PersistenceManager.getInstance();
List<Visit> visitsList = new ArrayList<Visit>();
List visits = pMgr.executeJpqlQuery(Queries.JPQL_VISITS);
for (Object v : visits) {
visitsList.add((Visit) v);
}
pMgr = null; // Dereference the singleton instance.
return visitsList;
}
/**
* @return the currentVisit
*/
public String getCurrentVisit() {
return currentVisit;
}
/**
* @param currentVisit
* the currentVisit to set
*/
public void setCurrentVisit(String currentVisit) {
this.currentVisit = currentVisit;
}
}
为了避免重新实例化业务逻辑对象,我为 DI 绑定定义了一个实例:
import mypackage.jsf.logic.VisitsHandler;
import mypackage.jsf.logic.impl.VisitsHandlerImpl;
interface InjectorConstants {
// Url patterns for FacesServlet, as it would be defined in web.xml
static String[] JSF_SERVLET_URL_PATTERNS = new String[] { "*.jsf", "*.xhtml" };
// BUSINESS LOGIC OBJECTS.
static Class<VisitsHandler> VISITS_HANDLER = VisitsHandler.class;
static VisitsHandler VISITS_HANDLER_IMPL = new VisitsHandlerImpl();
}
现在,带有对象绑定的 Guice 模块:
import javax.faces.webapp.FacesServlet;
import javax.inject.Singleton;
import mypackage.cdi.annotation.ViewScoped;
import mypackage.cdi.annotation.ViewScopedImpl;
import mypackage.cdi.listener.PostConstructTypeListener;
import mypackage.jsf.FacesHttpServlet;
import com.google.inject.matcher.Matchers;
import com.google.inject.servlet.ServletModule;
public class JSFModule extends ServletModule {
private void businessLogicBindings() {
bind(InjectorConstants.VISITS_HANDLER).toInstance(InjectorConstants.VISITS_HANDLER_IMPL);
}
private void systemBindings() {
// Add support for the @PostConstruct annotation for Guice-injected
// objects.
bindListener(Matchers.any(), new PostConstructTypeListener(null));
// Binding a custom implementation of "@ViewScoped" scope.
bindScope(ViewScoped.class, new ViewScopedImpl());
}
private void jsfBindings() {
// Define and bind FacesServlet as singleton object
// so it can be injected in FacesHttpServlet's constructor.
bind(FacesServlet.class).in(Singleton.class);
// JSF patterns to be served by FacesHttpServlet.
for (String urlPattern : InjectorConstants.JSF_SERVLET_URL_PATTERNS) {
serve(urlPattern).with(FacesHttpServlet.class);
}
}
@Override
protected void configureServlets() {
// Guice injector bindings.
this.systemBindings();
this.businessLogicBindings();
this.jsfBindings();
}
}
businessLogicBindings()
方法将业务逻辑接口与实现实例相关联。另一方面,您可以在这一行看到:serve(urlPattern).with(FacesHttpServlet.class);
,Guice 会将 JSF 请求重新路由到带有注入 FacesServlet
实例的 HttpServlet 包装器:
import java.io.IOException;
import javax.faces.webapp.FacesServlet;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Singleton
public class FacesHttpServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final Servlet facesServlet;
@Inject
public FacesHttpServlet(FacesServlet facesServlet) {
this.facesServlet = facesServlet;
}
@Override
public void init(ServletConfig config) throws ServletException {
this.facesServlet.init(config);
}
@Override
public ServletConfig getServletConfig() {
return this.facesServlet.getServletConfig();
}
@Override
public String getServletInfo() {
return this.facesServlet.getServletInfo();
}
@Override
public void destroy() {
super.destroy();
this.facesServlet.destroy();
}
@Override
public void service(ServletRequest req, ServletResponse resp) throws ServletException, IOException {
HttpServletRequest httpReq = (HttpServletRequest) req;
String reqUrl = httpReq.getRequestURL().toString();
// A hack to redirect the index page. It's been throwing an error if the
// "/index.[xhtml|jsf]" is not explicitly specified in the request URL.
if(reqUrl.toLowerCase().endsWith("index.xhtml")) {
((HttpServletResponse) resp).sendRedirect(reqUrl.replace("index.xhtml", "index.jsf"));
} else {
this.facesServlet.service(req, resp);
}
}
}
现在,初始化注入器的侦听器:
import java.util.HashMap;
import mypackage.cdi.JSFModule;
import mypackage.cdi.JsfInjectionProvider;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.servlet.GuiceServletContextListener;
public class GuiceListener extends GuiceServletContextListener {
protected AbstractModule module;
protected static Injector injector;
private static HashMap<String, Object> instancesMap;
public GuiceListener() {
// Bean instance list to ensure that we inject a unique bean instance.
instancesMap = new HashMap<>();
// Create the injector.
injector = Guice.createInjector(new JSFModule());
}
@Override
public Injector getInjector() {
return injector;
}
/**
* given a class, generates an injected instance. Useful when an API call is
* needed internally.
*/
public static <T> T getInstance(Class<T> type) {
return injector.getInstance(type);
}
/**
* given an injectable instance, injects its dependencies and make sure to
* only inject one.
*/
public static void injectMembers(Object instance) {
Object obj = null;
if (JsfInjectionProvider.isBusinessLogicObject(obj)) {
String instanceClassName = instance.getClass().getName();
Object mappedInstance = instancesMap.get(instanceClassName);
if (mappedInstance == null) {
// It's a new bean instance. It's stored in the bean map
// to be able to reuse it.
instancesMap.put(instanceClassName, instance);
obj = instance;
} else {
// There's already a bean instance. Let's reuse it!.
obj = mappedInstance;
}
} else { // it should be a managed bean.
obj = instance;
}
injector.injectMembers(obj);
}
}
最后但同样重要的是,Mojarra 必须将我们的 DI 实现注册为它的 DI 提供者(参见 <context-param>
com.sun.faces.injectionProvider
值):
import javax.faces.bean.ManagedBean;
import mypackage.cdi.annotation.BusinessLogic;
import mypackage.cdi.listener.GuiceListener;
import com.sun.faces.spi.InjectionProviderException;
import com.sun.faces.vendor.WebContainerInjectionProvider;
public class JsfInjectionProvider extends WebContainerInjectionProvider {
@Override
public void inject(Object obj) throws InjectionProviderException {
if (isManagedBean(obj) || isBusinessLogicObject(obj)) {
GuiceListener.injectMembers(obj);
}
}
/**
* As an arbitrary choice, the choice here is to inject only into
* {@code @ManagedBean} instances, so that other classes - not written by us
* - wouldn't be injected too. This choice could be altered.
*
* @param obj
* A JSF bean instance (annotated with @ManagedBean).
* @return
*/
private boolean isManagedBean(Object obj) {
return obj != null && obj.getClass().getAnnotation(ManagedBean.class) != null;
}
public static boolean isBusinessLogicObject(Object obj) {
return obj != null && obj.getClass().getAnnotation(BusinessLogic.class) != null;
}
}
所有这些都可以正常工作(但省略了此时不相关的 JPA 部分):
ExampleBean
:
import java.io.Serializable;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.faces.bean.ManagedBean;
import javax.inject.Inject;
import mypackage.jsf.logic.VisitsHandler;
import mypackage.dataaccess.entity.Visit;
@ManagedBean(name="jsfbExample")
public class ExampleBean implements Serializable {
private static final long serialVersionUID = 1L;
@Inject
private VisitsHandler visitsHandler;
@PostConstruct
public void init() {
System.out.println("ExampleBean - Injection works! visitsHandler = " + visitsHandler); // It works.
}
/**
* Method to test EL engine processing with parameters.
* @param param
* @return
*/
public void insertVisit() {
this.visitsHandler.insertVisit();
}
public List<Visit> getPageVisits() {
return this.visitsHandler.getPageVisits();
}
/**
* @return the currentVisit
*/
public String getCurrentVisit() {
return this.visitsHandler.getCurrentVisit();
}
/**
* @param currentVisit
* the currentVisit to set
*/
public void setCurrentVisit(String currentVisit) {
this.visitsHandler.setCurrentVisit(currentVisit);
}
}
现在,您可以创建一个 *.xhtml 文件作为您的索引索引并将此测试代码放在上面:
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets">
<h:head id="head">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Welcome to JSF 2.1 on the Google AppEngine!</title>
</h:head>
<h:body>
<h:form>
<h:outputText id="lastVisit" value="#{jsfbExample.currentVisit}" /><br/>
<h:commandButton value="New visit!"
actionListener="#{jsfbExample.insertVisit()}">
<f:ajax execute="@this" render="pageVisitsList" />
</h:commandButton>
<h:commandButton value="Last inserted visit!">
<f:ajax execute="@this" render="lastVisit" />
</h:commandButton>
<h:panelGrid id="pageVisitsList">
<c:forEach var="visit" items="#{jsfbExample.pageVisits}">
<h:outputText value="#{visit.toString()}" />
</c:forEach>
</h:panelGrid>
</h:form>
</h:body>
</html>
JPA 功能更简单,因为它的配置既不依赖于 JSF 也不依赖于 DI。
PersistenceManagerSetupListener
:
package mypackage.listener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import mypackage.dataaccess.PersistenceManager;
import mypackage.utils.StringMap;
public class PersistenceManagerSetupListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent servletContextInitEvt) {
// This is only a wrapper over HashMap<String, String>
StringMap initProperties = new StringMap();
// Check the System properties to determine if we are running on cloud
// or not, and set up the JDBC driver accordingly.
String platform = System.getProperty("com.google.appengine.runtime.version").toLowerCase()
.contains("google app engine") ? "cloud" : "dev";
initProperties.put("datanucleus.ConnectionURL", System.getProperty(platform + ".db.url"));
initProperties.put("datanucleus.ConnectionDriverName", System.getProperty(platform + ".db.driver"));
initProperties.put("datanucleus.ConnectionUserName", System.getProperty(platform + ".db.user"));
initProperties.put("datanucleus.ConnectionPassword", System.getProperty(platform + ".db.password"));
// I implemented password encryption. See Datanucleus' "ConnectionEncryptionProvider" interface documentation.
initProperties.put("datanucleus.ConnectionPasswordDecrypter",
System.getProperty(platform + ".db.encryptionProvider"));
// ***********************************************************************************************************
// THESE 2 ARE A MUST-HAVE!!!
// ***********************************************************************************************************
initProperties.put("datanucleus.identifier.case", System.getProperty("persistencemanager.identifier.case"));
initProperties.put("datanucleus.storeManagerType", System.getProperty("persistencemanager.storeManagerType"));
// ***********************************************************************************************************
initProperties.put("datanucleus.NontransactionalRead",
System.getProperty("persistencemanager.NontransactionalRead"));
initProperties.put("datanucleus.NontransactionalRead",
System.getProperty("persistencemanager.NontransactionalRead"));
initProperties.put("datanucleus.NontransactionalWrite",
System.getProperty("persistencemanager.NontransactionalWrite"));
initProperties.put("datanucleus.singletonEMFForName",
System.getProperty("persistencemanager.singletonEMFForName"));
initProperties.put("javax.persistence.query.timeout", System.getProperty("persistencemanager.query.timeout"));
initProperties.put("datanucleus.datastoreWriteTimeout",
System.getProperty("persistencemanager.datastoreWriteTimeout"));
// Initialize persistence engine.
PersistenceManager.initialize(initProperties);
}
@Override
public void contextDestroyed(ServletContextEvent servletContextDestroyedEvt) {
PersistenceManager.shutdown();
}
}
所有持久性初始化属性都在 app-engine.xml
中定义。其基本结构:
<appengine-web-app ...>
<application>cloud-project-id</application>
<version>1</version>
<threadsafe>true</threadsafe>
<system-properties>
<!-- Cloud platform properties (their name starts with "cloud") -->
<property name="cloud.db.url"
value="jdbc:google:mysql://(cloud-connection-name)/(db-name)" />
<property name="cloud.db.driver"
value="com.google.appengine.api.rdbms.AppEngineDriver" />
<!-- ... -->
<!-- Dev platform properties (their name starts with "dev") -->
<property name="dev.db.url" value="jdbc:mysql://(db-server):(db-port)/(db-name)" />
<property name="dev.db.driver" value="com.mysql.jdbc.Driver" />
<!-- ... -->
<!-- Datanucleus properties -->
<!-- *********************************************** -->
<!-- THESE 2 ARE A MUST-HAVE!!! Others are optional -->
<!-- *********************************************** -->
<property name="persistencemanager.storeManagerType" value="rdbms" />
<!-- This means that all DB identifiers MUST be defined in lowercase. -->
<property name="persistencemanager.identifier.case" value="LowerCase" />
<!-- *********************************************** -->
<!-- ... -->
</system-properties>
<sessions-enabled>true</sessions-enabled>
<async-session-persistence enabled="false" />
<static-files>
<exclude path="/**.xhtml" />
</static-files>
</appengine-web-app>
您必须至少定义一个持久化单元(在"persistence.xml"中):
<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
version="1.0">
<persistence-unit name="MyPersistenceUnit">
<!-- DATANUCLEUS' JPA 2.0 PERSISTENCE PROVIDER CLASS -->
<provider>org.datanucleus.api.jpa.PersistenceProviderImpl</provider>
<!-- ENTITY CLASSES -->
<class>mypackage.dataaccess.entity.Visit</class>
<!-- DON'T PROCESS UNLISTED CLASSES AS ENTITY CLASSES. -->
<exclude-unlisted-classes>true</exclude-unlisted-classes>
</persistence-unit>
</persistence>
以及持久性管理器对象中的一些初始化和关闭方法,用于创建和销毁 EntityManagerFactory 和 EntityManager。像这样:
public static void initialize(Map properties) {
if (!isInitialized) {
if (properties == null) {
emfInstance = Persistence.createEntityManagerFactory("MyPersistenceUnit");
} else {
emfInstance = Persistence.createEntityManagerFactory("MyPersistenceUnit", properties);
}
emInstance = emfInstance.createEntityManager();
isInitialized = true;
}
}
public static void shutdown() {
try {
emInstance.close();
} catch (Exception e) {}
try {
emfInstance.close();
} catch (Exception e) {}
}
"Visit" class 只是一个实体 class,它映射了 3 个字段(访问次数、来源 IP 和时间戳),并在 "persistence.xml" 中注册文件。
我写这个 post 作为一个教程,显示 step-by-step,我是如何在 GAE 上 运行 这些技术的(当我输入这些行时,SDK 1.9.48在)。我花了数周的时间进行研究和 trial-error 编码,我希望本指南能够帮助其他 Java 程序员不要像我一样经历那些混乱。
希望本指南可以帮助其他人在 GAE 中创建出色的 J2EE 应用程序。
几周前,我被要求在 Google Cloud Platform 上为 运行 创建一个 Web 应用程序(输入此 post 时,SDK v1.9.48) .配置 AppEngine 设置(创建帐户、项目、云数据库、源代码存储库等)后,我准备使用 GAE Eclipse 插件开发我的第一个 Web 应用程序。
当我发现 GAE 默认只支持 JSP 和 servlet 时,我感到非常失望。
然后我说:"May God help me! Back to J2EE's stone age again? I'm used to JSF and (C)DI for the UI. How am I going to integrate in a GAE webapp these 3 J2EE standards and make it run smoothly (if such a thing is possible)?":
- JSF 2.x
- (C)DI 1.x
- JPA2.x
只要继续阅读本文 post,您就会知道怎么做!
好吧,我决定不那么轻易放弃并陷入困境。经过几周的艰苦研究和 trial-error 编码,我找到了这个烂摊子的解决方案。
在开始 post 之前,我将为您提供一些可以帮助您将其整合在一起的重要资源:
- Configuring JSF 2.2 to run on the Google App Engine Using Eclipse
- A workaround for a session data loss bug
- List of libraries I used (at least half of them come bundled in GAE SDK)
框架:
- Datanucleus 3.1.1 (JPA 2.0)
- Oracle Mojarra 2.2.4 (JSF 2.2)。
- Google Guice 4.0 (DI 1.0)
我是这样工作的:
最重要的配置在web.xml。 JSF 初始化 必须 运行 FIRST :我发现 com.sun.faces.config.ConfigureListener
负责该步骤,它总是寻找 FacesServlet
声明。由于 JSF 请求 必须 由 Guice 使用 FacesHttpServlet
包装器(稍后我将 post 提供)以启用 DI,因此:
我声明了 FacesServlet
WITHOUT <servlet-mapping>
s(我通过 trial-error 编码弄清楚了那一步)。
它只是声明初始化FacesContextFactory
。这是 web.xml:
<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<display-name>BrochureForce</display-name>
<description>Purchase orders generator configured to run on the Google AppEngine.</description>
<context-param>
<description>Project stage (Development or Production)</description>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Development</param-value>
</context-param>
<context-param>
<description>
Designate client-side state saving, since GAE doesn't handle
server side (JSF default) state management.
</description>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>client</param-value>
</context-param>
<context-param>
<description>Sets the default suffix for JSF pages to .xhtml</description>
<param-name>javax.faces.DEFAULT_SUFFIX</param-name>
<param-value>.xhtml</param-value>
</context-param>
<context-param>
<description>
When enabled, the runtime initialization and default ResourceHandler
implementation will use threads to perform their functions. Set this
value to false if threads aren't desired (as in the case of single-threaded
environments such as the Google AppEngine).
Note that when this option is disabled, the ResourceHandler will not
pick up new versions of resources when ProjectStage is development.
</description>
<param-name>com.sun.faces.enableThreading</param-name>
<param-value>false</param-value>
</context-param>
<context-param>
<description>Allows dependency-injection into ManagedBeans</description>
<param-name>com.sun.faces.injectionProvider</param-name>
<param-value>mypackage.jsf.JsfInjectionProvider</param-value>
</context-param>
<context-param>
<description>Specify JBoss Expression Language Over Default</description>
<param-name>com.sun.faces.expressionFactory</param-name>
<param-value>org.jboss.el.ExpressionFactoryImpl</param-value>
</context-param>
<!-- JSF INITIALIZATION GOES FIRST!! -->
<servlet>
<description>
JSF 2 Servlet. There's NO servlet-mapping defined for this servlet because
it's declared here in order to enforce the FacesFactory to load properly
so that an instance of this servlet can be injected in the FacesHttpServlet
used by Guice to serve JSF requests and as injection provider at the same time.
Furthermore, the "load-on-startup" property is set to "0" to tell Jetty
that this servlet MUST be loaded first.
</description>
<servlet-name>JSF Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>0</load-on-startup>
</servlet>
<listener>
<description>JSF Initialization.</description>
<listener-class>com.sun.faces.config.ConfigureListener</listener-class>
</listener>
<!-- JSF INITIALIZATION GOES FIRST!! -->
<listener>
<description>PERSISTENCE ENGINE INITIALIZATION AND SHUTDOWN.</description>
<listener-class>mypackage.listener.PersistenceManagerSetupListener</listener-class>
</listener>
<!-- ***** Specify session timeout of thirty (30) minutes. ***** -->
<session-config>
<session-timeout>30</session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>index.jsf</welcome-file>
<welcome-file>index.xhtml</welcome-file>
</welcome-file-list>
<!-- **************************************************** -->
<!-- DI API initialization (Google Guice Implementation). -->
<!-- **************************************************** -->
<filter>
<description>Google Guice filter which enables DI.</description>
<filter-name>GuiceFilter</filter-name>
<filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>GuiceFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<description>
This listener initializes the Guice injector and wraps the JSF Servlet
into a HttpServlet in order to serve JSF requests via Guice Filter.
</description>
<listener-class>mypackage.listener.GuiceListener</listener-class>
</listener>
<!-- **************************************************** -->
</web-app>
其次,我不再尝试将托管 bean 实例注入另一个实例。相反,我将绑定的业务逻辑实例注入到 bean 中(换句话说,模拟 EJB 行为)。这就是我所做的:
我为业务逻辑实现定义了一个
@BindingAnnotation
:import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import com.google.inject.BindingAnnotation; @Documented @BindingAnnotation @Retention(RUNTIME) @Target({ TYPE }) public @interface BusinessLogic {}
我定义了一个业务逻辑接口及其实现,并用
@BusinessLogic
注释对两者进行了注释(这是一个注册对页面的访问的示例。字段是:访问编号、源 IP 和时间戳):import java.util.List; import mypackage.annotation.BusinessLogic; import mypackage.dataaccess.entity.Visit; @BusinessLogic public interface VisitsHandler { public void insertVisit(); public List<Visit> getPageVisits(); // Propiedades public String getCurrentVisit(); public void setCurrentVisit(String currentVisit); }
及其实现:
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import mypackage.annotation.BusinessLogic;
import mypackage.jsf.logic.VisitsHandler;
import mypackage.dataaccess.PersistenceManager;
import mypackage.dataaccess.Queries;
import mypackage.dataaccess.entity.Visit;
@BusinessLogic
public class VisitsHandlerImpl implements VisitsHandler {
private String currentVisit;
public void insertVisit() {
PersistenceManager pMgr = PersistenceManager.getInstance();
Visit newVisit = new Visit();
newVisit.setUserIp("127.0.0.1");
newVisit.setTimestamp(new Date(System.currentTimeMillis()));
pMgr.insert(newVisit);
pMgr = null; // Dereference the singleton instance.
this.currentVisit = newVisit.toString();
}
@SuppressWarnings("rawtypes")
public List<Visit> getPageVisits() {
PersistenceManager pMgr = PersistenceManager.getInstance();
List<Visit> visitsList = new ArrayList<Visit>();
List visits = pMgr.executeJpqlQuery(Queries.JPQL_VISITS);
for (Object v : visits) {
visitsList.add((Visit) v);
}
pMgr = null; // Dereference the singleton instance.
return visitsList;
}
/**
* @return the currentVisit
*/
public String getCurrentVisit() {
return currentVisit;
}
/**
* @param currentVisit
* the currentVisit to set
*/
public void setCurrentVisit(String currentVisit) {
this.currentVisit = currentVisit;
}
}
为了避免重新实例化业务逻辑对象,我为 DI 绑定定义了一个实例:
import mypackage.jsf.logic.VisitsHandler;
import mypackage.jsf.logic.impl.VisitsHandlerImpl;
interface InjectorConstants {
// Url patterns for FacesServlet, as it would be defined in web.xml
static String[] JSF_SERVLET_URL_PATTERNS = new String[] { "*.jsf", "*.xhtml" };
// BUSINESS LOGIC OBJECTS.
static Class<VisitsHandler> VISITS_HANDLER = VisitsHandler.class;
static VisitsHandler VISITS_HANDLER_IMPL = new VisitsHandlerImpl();
}
现在,带有对象绑定的 Guice 模块:
import javax.faces.webapp.FacesServlet;
import javax.inject.Singleton;
import mypackage.cdi.annotation.ViewScoped;
import mypackage.cdi.annotation.ViewScopedImpl;
import mypackage.cdi.listener.PostConstructTypeListener;
import mypackage.jsf.FacesHttpServlet;
import com.google.inject.matcher.Matchers;
import com.google.inject.servlet.ServletModule;
public class JSFModule extends ServletModule {
private void businessLogicBindings() {
bind(InjectorConstants.VISITS_HANDLER).toInstance(InjectorConstants.VISITS_HANDLER_IMPL);
}
private void systemBindings() {
// Add support for the @PostConstruct annotation for Guice-injected
// objects.
bindListener(Matchers.any(), new PostConstructTypeListener(null));
// Binding a custom implementation of "@ViewScoped" scope.
bindScope(ViewScoped.class, new ViewScopedImpl());
}
private void jsfBindings() {
// Define and bind FacesServlet as singleton object
// so it can be injected in FacesHttpServlet's constructor.
bind(FacesServlet.class).in(Singleton.class);
// JSF patterns to be served by FacesHttpServlet.
for (String urlPattern : InjectorConstants.JSF_SERVLET_URL_PATTERNS) {
serve(urlPattern).with(FacesHttpServlet.class);
}
}
@Override
protected void configureServlets() {
// Guice injector bindings.
this.systemBindings();
this.businessLogicBindings();
this.jsfBindings();
}
}
businessLogicBindings()
方法将业务逻辑接口与实现实例相关联。另一方面,您可以在这一行看到:serve(urlPattern).with(FacesHttpServlet.class);
,Guice 会将 JSF 请求重新路由到带有注入 FacesServlet
实例的 HttpServlet 包装器:
import java.io.IOException;
import javax.faces.webapp.FacesServlet;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Singleton
public class FacesHttpServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final Servlet facesServlet;
@Inject
public FacesHttpServlet(FacesServlet facesServlet) {
this.facesServlet = facesServlet;
}
@Override
public void init(ServletConfig config) throws ServletException {
this.facesServlet.init(config);
}
@Override
public ServletConfig getServletConfig() {
return this.facesServlet.getServletConfig();
}
@Override
public String getServletInfo() {
return this.facesServlet.getServletInfo();
}
@Override
public void destroy() {
super.destroy();
this.facesServlet.destroy();
}
@Override
public void service(ServletRequest req, ServletResponse resp) throws ServletException, IOException {
HttpServletRequest httpReq = (HttpServletRequest) req;
String reqUrl = httpReq.getRequestURL().toString();
// A hack to redirect the index page. It's been throwing an error if the
// "/index.[xhtml|jsf]" is not explicitly specified in the request URL.
if(reqUrl.toLowerCase().endsWith("index.xhtml")) {
((HttpServletResponse) resp).sendRedirect(reqUrl.replace("index.xhtml", "index.jsf"));
} else {
this.facesServlet.service(req, resp);
}
}
}
现在,初始化注入器的侦听器:
import java.util.HashMap;
import mypackage.cdi.JSFModule;
import mypackage.cdi.JsfInjectionProvider;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.servlet.GuiceServletContextListener;
public class GuiceListener extends GuiceServletContextListener {
protected AbstractModule module;
protected static Injector injector;
private static HashMap<String, Object> instancesMap;
public GuiceListener() {
// Bean instance list to ensure that we inject a unique bean instance.
instancesMap = new HashMap<>();
// Create the injector.
injector = Guice.createInjector(new JSFModule());
}
@Override
public Injector getInjector() {
return injector;
}
/**
* given a class, generates an injected instance. Useful when an API call is
* needed internally.
*/
public static <T> T getInstance(Class<T> type) {
return injector.getInstance(type);
}
/**
* given an injectable instance, injects its dependencies and make sure to
* only inject one.
*/
public static void injectMembers(Object instance) {
Object obj = null;
if (JsfInjectionProvider.isBusinessLogicObject(obj)) {
String instanceClassName = instance.getClass().getName();
Object mappedInstance = instancesMap.get(instanceClassName);
if (mappedInstance == null) {
// It's a new bean instance. It's stored in the bean map
// to be able to reuse it.
instancesMap.put(instanceClassName, instance);
obj = instance;
} else {
// There's already a bean instance. Let's reuse it!.
obj = mappedInstance;
}
} else { // it should be a managed bean.
obj = instance;
}
injector.injectMembers(obj);
}
}
最后但同样重要的是,Mojarra 必须将我们的 DI 实现注册为它的 DI 提供者(参见 <context-param>
com.sun.faces.injectionProvider
值):
import javax.faces.bean.ManagedBean;
import mypackage.cdi.annotation.BusinessLogic;
import mypackage.cdi.listener.GuiceListener;
import com.sun.faces.spi.InjectionProviderException;
import com.sun.faces.vendor.WebContainerInjectionProvider;
public class JsfInjectionProvider extends WebContainerInjectionProvider {
@Override
public void inject(Object obj) throws InjectionProviderException {
if (isManagedBean(obj) || isBusinessLogicObject(obj)) {
GuiceListener.injectMembers(obj);
}
}
/**
* As an arbitrary choice, the choice here is to inject only into
* {@code @ManagedBean} instances, so that other classes - not written by us
* - wouldn't be injected too. This choice could be altered.
*
* @param obj
* A JSF bean instance (annotated with @ManagedBean).
* @return
*/
private boolean isManagedBean(Object obj) {
return obj != null && obj.getClass().getAnnotation(ManagedBean.class) != null;
}
public static boolean isBusinessLogicObject(Object obj) {
return obj != null && obj.getClass().getAnnotation(BusinessLogic.class) != null;
}
}
所有这些都可以正常工作(但省略了此时不相关的 JPA 部分):
ExampleBean
:
import java.io.Serializable;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.faces.bean.ManagedBean;
import javax.inject.Inject;
import mypackage.jsf.logic.VisitsHandler;
import mypackage.dataaccess.entity.Visit;
@ManagedBean(name="jsfbExample")
public class ExampleBean implements Serializable {
private static final long serialVersionUID = 1L;
@Inject
private VisitsHandler visitsHandler;
@PostConstruct
public void init() {
System.out.println("ExampleBean - Injection works! visitsHandler = " + visitsHandler); // It works.
}
/**
* Method to test EL engine processing with parameters.
* @param param
* @return
*/
public void insertVisit() {
this.visitsHandler.insertVisit();
}
public List<Visit> getPageVisits() {
return this.visitsHandler.getPageVisits();
}
/**
* @return the currentVisit
*/
public String getCurrentVisit() {
return this.visitsHandler.getCurrentVisit();
}
/**
* @param currentVisit
* the currentVisit to set
*/
public void setCurrentVisit(String currentVisit) {
this.visitsHandler.setCurrentVisit(currentVisit);
}
}
现在,您可以创建一个 *.xhtml 文件作为您的索引索引并将此测试代码放在上面:
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets">
<h:head id="head">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Welcome to JSF 2.1 on the Google AppEngine!</title>
</h:head>
<h:body>
<h:form>
<h:outputText id="lastVisit" value="#{jsfbExample.currentVisit}" /><br/>
<h:commandButton value="New visit!"
actionListener="#{jsfbExample.insertVisit()}">
<f:ajax execute="@this" render="pageVisitsList" />
</h:commandButton>
<h:commandButton value="Last inserted visit!">
<f:ajax execute="@this" render="lastVisit" />
</h:commandButton>
<h:panelGrid id="pageVisitsList">
<c:forEach var="visit" items="#{jsfbExample.pageVisits}">
<h:outputText value="#{visit.toString()}" />
</c:forEach>
</h:panelGrid>
</h:form>
</h:body>
</html>
JPA 功能更简单,因为它的配置既不依赖于 JSF 也不依赖于 DI。
PersistenceManagerSetupListener
:
package mypackage.listener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import mypackage.dataaccess.PersistenceManager;
import mypackage.utils.StringMap;
public class PersistenceManagerSetupListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent servletContextInitEvt) {
// This is only a wrapper over HashMap<String, String>
StringMap initProperties = new StringMap();
// Check the System properties to determine if we are running on cloud
// or not, and set up the JDBC driver accordingly.
String platform = System.getProperty("com.google.appengine.runtime.version").toLowerCase()
.contains("google app engine") ? "cloud" : "dev";
initProperties.put("datanucleus.ConnectionURL", System.getProperty(platform + ".db.url"));
initProperties.put("datanucleus.ConnectionDriverName", System.getProperty(platform + ".db.driver"));
initProperties.put("datanucleus.ConnectionUserName", System.getProperty(platform + ".db.user"));
initProperties.put("datanucleus.ConnectionPassword", System.getProperty(platform + ".db.password"));
// I implemented password encryption. See Datanucleus' "ConnectionEncryptionProvider" interface documentation.
initProperties.put("datanucleus.ConnectionPasswordDecrypter",
System.getProperty(platform + ".db.encryptionProvider"));
// ***********************************************************************************************************
// THESE 2 ARE A MUST-HAVE!!!
// ***********************************************************************************************************
initProperties.put("datanucleus.identifier.case", System.getProperty("persistencemanager.identifier.case"));
initProperties.put("datanucleus.storeManagerType", System.getProperty("persistencemanager.storeManagerType"));
// ***********************************************************************************************************
initProperties.put("datanucleus.NontransactionalRead",
System.getProperty("persistencemanager.NontransactionalRead"));
initProperties.put("datanucleus.NontransactionalRead",
System.getProperty("persistencemanager.NontransactionalRead"));
initProperties.put("datanucleus.NontransactionalWrite",
System.getProperty("persistencemanager.NontransactionalWrite"));
initProperties.put("datanucleus.singletonEMFForName",
System.getProperty("persistencemanager.singletonEMFForName"));
initProperties.put("javax.persistence.query.timeout", System.getProperty("persistencemanager.query.timeout"));
initProperties.put("datanucleus.datastoreWriteTimeout",
System.getProperty("persistencemanager.datastoreWriteTimeout"));
// Initialize persistence engine.
PersistenceManager.initialize(initProperties);
}
@Override
public void contextDestroyed(ServletContextEvent servletContextDestroyedEvt) {
PersistenceManager.shutdown();
}
}
所有持久性初始化属性都在 app-engine.xml
中定义。其基本结构:
<appengine-web-app ...>
<application>cloud-project-id</application>
<version>1</version>
<threadsafe>true</threadsafe>
<system-properties>
<!-- Cloud platform properties (their name starts with "cloud") -->
<property name="cloud.db.url"
value="jdbc:google:mysql://(cloud-connection-name)/(db-name)" />
<property name="cloud.db.driver"
value="com.google.appengine.api.rdbms.AppEngineDriver" />
<!-- ... -->
<!-- Dev platform properties (their name starts with "dev") -->
<property name="dev.db.url" value="jdbc:mysql://(db-server):(db-port)/(db-name)" />
<property name="dev.db.driver" value="com.mysql.jdbc.Driver" />
<!-- ... -->
<!-- Datanucleus properties -->
<!-- *********************************************** -->
<!-- THESE 2 ARE A MUST-HAVE!!! Others are optional -->
<!-- *********************************************** -->
<property name="persistencemanager.storeManagerType" value="rdbms" />
<!-- This means that all DB identifiers MUST be defined in lowercase. -->
<property name="persistencemanager.identifier.case" value="LowerCase" />
<!-- *********************************************** -->
<!-- ... -->
</system-properties>
<sessions-enabled>true</sessions-enabled>
<async-session-persistence enabled="false" />
<static-files>
<exclude path="/**.xhtml" />
</static-files>
</appengine-web-app>
您必须至少定义一个持久化单元(在"persistence.xml"中):
<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
version="1.0">
<persistence-unit name="MyPersistenceUnit">
<!-- DATANUCLEUS' JPA 2.0 PERSISTENCE PROVIDER CLASS -->
<provider>org.datanucleus.api.jpa.PersistenceProviderImpl</provider>
<!-- ENTITY CLASSES -->
<class>mypackage.dataaccess.entity.Visit</class>
<!-- DON'T PROCESS UNLISTED CLASSES AS ENTITY CLASSES. -->
<exclude-unlisted-classes>true</exclude-unlisted-classes>
</persistence-unit>
</persistence>
以及持久性管理器对象中的一些初始化和关闭方法,用于创建和销毁 EntityManagerFactory 和 EntityManager。像这样:
public static void initialize(Map properties) {
if (!isInitialized) {
if (properties == null) {
emfInstance = Persistence.createEntityManagerFactory("MyPersistenceUnit");
} else {
emfInstance = Persistence.createEntityManagerFactory("MyPersistenceUnit", properties);
}
emInstance = emfInstance.createEntityManager();
isInitialized = true;
}
}
public static void shutdown() {
try {
emInstance.close();
} catch (Exception e) {}
try {
emfInstance.close();
} catch (Exception e) {}
}
"Visit" class 只是一个实体 class,它映射了 3 个字段(访问次数、来源 IP 和时间戳),并在 "persistence.xml" 中注册文件。
我写这个 post 作为一个教程,显示 step-by-step,我是如何在 GAE 上 运行 这些技术的(当我输入这些行时,SDK 1.9.48在)。我花了数周的时间进行研究和 trial-error 编码,我希望本指南能够帮助其他 Java 程序员不要像我一样经历那些混乱。
希望本指南可以帮助其他人在 GAE 中创建出色的 J2EE 应用程序。