本文是上文的延续.
我想先介绍一下JBoss 开发者框架,这是JBoss 社区推出的一个全新的项目,旨在帮助开发者更好的理解和使用JBoss的相关技术,它提供了大量的实例教程(50+),视频,文章的内容,迁移向导(从Java EE 5到EE 6,从Spring到Java EE)等,教你一点点的学会Java EE 6相关的各种技术,并且涵盖了 REST,HTML5 等新的热点技术.
本文即以kitchensink,一个 JBoss 开发者框架 提供的实例为基础展开.
首先,先让我介绍一下kitchensink吧,最新的源代码可以在这里找到.
这个实例主要演示了如下几种技术
- Bean Validation 1.0
- EJB 3.1
- JAX-RS
- JPA 2.0
- JSF 2.0
- CDI 1.0
- Arquillian
想要运行这个实例的话,你需要 JDK 6/7,Maven 3 和 JBoss AS 7
注意,有一些依赖是只存在于JBoss 的maven 仓库中的,所以,可能需要对maven 的settings.xml文件做些配置,添加JBoss maven仓库,具体请参考这里 和这里
具体配置信息就不多说了,上面给出的链接很详细,这些也不是本文的重点,接下来就让我们看看代码
首先是persistence.xml,位于main/resources/META-INF/persistence.xml,都是标准的位置
<persistence version="2.0" 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_2_0.xsd"> <persistence-unit name="primary"> <jta-data-source>java:jboss/datasources/KitchensinkQuickstartDS</jta-data-source> <properties> <property name="hibernate.hbm2ddl.auto" value="create-drop" /> <property name="hibernate.show_sql" value="false" /> </properties> </persistence-unit> </persistence>
可以看到,这个文件定义的很简单,就是定义了一个数据源和两个属性,注意,hibernate.hbm2ddl.auto=creat-drop,意思是在创建session factory的时候自动创建表结构,关闭session factory的时候会自动drop掉表.
同时,还可以看到main/resources目录中有import.sql这个文件,当使用hibernate创建表结构的时候,创建完成之后,hibernate会自动的导入import.sql文件,这样就可以添加一些初始数据.
另外,引用的数据源是定义在main/webapp/WEB-INF/kitchensink-quickstart-ds.xml文件中的.
然后再来看看本文关注的另外一个方面,实体定义
@Entity @XmlRootElement @Table(uniqueConstraints = @UniqueConstraint(columnNames = "email")) public class Member implements Serializable { /** Default value included to remove warning. Remove or modify at will. **/ private static final long serialVersionUID = 1L; @Id @GeneratedValue private Long id; @NotNull @Size(min = 1, max = 25) @Pattern(regexp = "[A-Za-z ]*", message = "must contain only letters and spaces") private String name; @NotNull @NotEmpty @Email private String email; @NotNull @Size(min = 10, max = 12) @Digits(fraction = 0, integer = 12) @Column(name = "phone_number") private String phoneNumber; // getters / setters }
很简单的一个entity mapping,需要注意的是javax.xml.bind.annotation.XmlRootElement是JAXB里面的一个annotation,在这里可以把这个实体对象转化成xml表示
还有就是这个entity里面定义了一些BV的annotation,具体可以参考Hibernate Validator的文档.
Okay,本实例中用到的其它技术暂时不做介绍了,下面终于该进入正题了
上面介绍过了,这个实例使用的是JBoss AS7中的数据源(java:jboss/datasources/KitchensinkQuickstartDS), 那么,我们接下来就是看看如何做很少的更改,让这个实例使用Infinispan作为存储替换掉数据源中使用H2
首先,是添加依赖,因为我们这里是想要通过Hibernate OGM,把实体对象保存进Infinispan当中,所以只需要加入下面的依赖项即可
<dependency> <groupId>org.hibernate.ogm</groupId> <artifactId>hibernate-ogm-core</artifactId> <version>4.0.0-SNAPSHOT</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.6.6</version> </dependency> <dependency> <artifactId>infinispan-core</artifactId> <groupId>org.infinispan</groupId> <version>5.1.5.FINAL</version> <scope>provided</scope> </dependency>
接下来就是修改persistence.xml了,上文曾经提到过,Hibernate OGM本身也是一个JPA的实现,但是由于JBoss AS7默认集成的是Hibernate ORM 4,所以我们需要在persistence.xml中显式地声明我们希望使用Hibernate OGM作为JPA的实现。
<persistence version="2.0" 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_2_0.xsd"> <persistence-unit name="primary"> <provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider> <properties> <property name="hibernate.ogm.datastore.provider" value="org.hibernate.ogm.datastore.infinispan.impl.InfinispanDatastoreProvider"/> <property name="hibernate.ogm.infinispan.configuration_resourcename" value="infinispan-ogm-config.xml"/> </properties> <class>org.jboss.as.quickstarts.kitchensink.model.Member</class> </persistence-unit> </persistence>
在这个更新过的文件中我们可以看到如下的变化
- 使用org.hibernate.ogm.jpa.HibernateOgmPersistence作为JPA的实现
- 去掉了datasource的引用,Hibernate OGM不使用RMDBS
- 通过hibernate.ogm.datastore.provider指定使用infinispan作为data store
- 通过hibernate.ogm.infinispan.configuration_resourcename属性指定infinispan的配置文件
src/main/webapp/WEB-INF/kitchensink-quickstart-ds.xml这个文件已经没有用了,可以删掉。
至此,配置方面就需要这么多的改动,很简单吧
现在,我们已经通过使用Hibernate OGM,把底层的存储从RMDBS切换成了Infinispan,但是,由于RMDBS和NO-SQL本质的不同,我们还需要做一些修改。
使用uuid作为主键
在使用Hibernate ORM的时候,我们通常会使用@GeneratedValue来得到数据库自动生成的id,并且,通常我们建议把id设置成long类型的以得到更好的性能。
可是,如果使用的是NO-SQL的话,如果是K-V类型的NO-SQL的话,他们是没有一个主键的概念的 (mongodb等文档型数据库是会提供自动生成的id的),所以为了统一,并且保证全局唯一,在Hibernate OGM中我们建议使用UUID作为主键生成策略,并且,在Hibernate ORM中早已提供了此种策略,我们在这里可以直接使用。
org.jboss.as.quickstarts.kitchensink.model.Member#id需要修改成如下的样子。
@Id @GeneratedValue(generator = "uuid") @GenericGenerator(name = "uuid", strategy = "uuid2") private String id;
注意,改变了id的类型之后,我们还需要修改org.jboss.as.quickstarts.kitchensink.rest.MemberResourceRESTService#lookupMemberById
@GET @Path("/{id:[0-9][0-9,\\-]*}") @Produces(MediaType.APPLICATION_JSON) public Member lookupMemberById(@PathParam("id") String id) { Member member = repository.findById(id); if (member == null) { throw new WebApplicationException(Response.Status.NOT_FOUND); } return member; }
这个方法提供了一个通过REST接口来查找Member的功能,但是因为我们已经把id的类型改成了String,所以我们需要对@Path做一些修改,让它能够接受字符 --@Path("/{id:[0-9,a-z][0-9,a-z,\\-]*}")
查询
因为Hibernate OGM还是一个很年轻的项目,有一些功能还没有完全的实现,例如,我们在JPA/Hibernate中经常使用的Criteria查询,但是幸好,Hibernate OGM和Hibernate Search有很好的集成,我们可以使用Hibernate Search来完成这部分工作。
org.jboss.as.quickstarts.kitchensink.data.MemberRepository#findByEmail方法是通过email来查询一个Member,内部实现是通过Criteria来查询的。
org.jboss.as.quickstarts.kitchensink.data.MemberRepository#findAllOrderedByName方法是查询所有的Member并且按照name排序,同样是使用的Criteria。
这两个方法的实现很简单,相信大家都能看懂。
那么,我们现在就需要使用Hibernate Search替换掉这两个方法。
首先,把Hibernate Search相关的依赖添加进pom当中
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-search-orm</artifactId> <version>4.2.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-search-engine</artifactId> <version>4.2.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-search-analyzers</artifactId> <version>4.2.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-search-infinispan</artifactId> <version>4.2.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.infinispan</groupId> <artifactId>infinispan-lucene-directory</artifactId> <version>5.1.5.FINAL</version> </dependency>
这里所依赖的org.hibernate:hibernate-search-infinispan和org.infinispan:infinispan-lucene-directory提供了一个Lucene的Directory的实现,可以把Lucene的index保存在infinispan当中,从而实现比保存在文件系统当中更好的性能和可扩展性(因为Infinispan是一个分布式的数据网格系统)。
在persistence.xml当中,我们需要添加上两个Hibernate Search的属性 这里所依赖的org.hibernate:hibernate-search-infinispan和org.infinispan:infinispan-lucene-directory提供了一个Lucene的Directory的实现,可以把Lucene的index保存在infinispan当中,从而实现比保存在文件系统当中更好的性能和可扩展性(因为Infinispan是一个分布式的数据网格系统)。
在persistence.xml当中,我们需要添加上两个Hibernate Search的属性
<property name="hibernate.search.default.directory_provider" value="infinispan"/> <property name="hibernate.search.infinispan.configuration_resourcename" value="infinispan.xml"/>第一个hibernate.search.default.directory_provider告诉Hibernate Search使用Infinispan作为Lucene Index Directory,第二个指定了Hibernate Search所使用的Infinispan的配置文件。
接下来,我们需要对org.jboss.as.quickstarts.kitchensink.model.Member做一些改动,添加上Hibernate Search所需要的Annotation.
@Entity @XmlRootElement @Indexed @Table(uniqueConstraints = @UniqueConstraint(columnNames = "email")) @Proxy(lazy = false) public class Member implements Serializable { /** Default value included to remove warning. Remove or modify at will. **/ private static final long serialVersionUID = 1L; @Id @GeneratedValue(generator = "uuid") @GenericGenerator(name = "uuid", strategy = "uuid2") private String id; @NotNull @Size(min = 1, max = 25) @Pattern(regexp = "[A-Za-z ]*", message = "must contain only letters and spaces") @Fields({ @Field(analyze = Analyze.NO, norms = Norms.NO, store = Store.YES, name = "sortableStoredName"), @Field(analyze = Analyze.YES, norms = Norms.YES) }) private String name; @NotNull @NotEmpty @Email @Field(analyze = Analyze.NO) private String email; @NotNull @Size(min = 10, max = 12) @Digits(fraction = 0, integer = 12) @Column(name = "phone_number") @Field(analyze = Analyze.NO) private String phoneNumber;
第一个是@Indexed,它告诉Hibernate Search,这个实体类是需要被索引的,还有@Field (在name和phoneNumber属性上)告诉Hibernate Search这两个属性是需要被索引的,具体信息请参考Hibernate Search 文档。
想要在保存一个实体对象的时候,让Hibernate Search自动索引的话,我们需要使用org.hibernate.search.jpa.FullTextEntityManager来替换javax.persistence.EntityManager.
org.jboss.as.quickstarts.kitchensink.service.MemberRegistration#register这个方法调用了EntityManager#persist,那么我们需要做的就是把这个类中的@Inject private EntityManager em;换成@Inject private FullTextEntityManager em;这里,之前的EntityManager和现在的FullTextEntityManager都是由CDI负责自动注入的,可是,CDI并不知道如何创建Hibernate Search所特有的FullTextEntityManager,所以,为了让自动注入工作,我们需要一个Producer。
而这个Producer,在我们的实例当中就是org.jboss.as.quickstarts.kitchensink.util.Resources,它已经存在了,用来提供EntityManager的注入和Logger的注入(org.jboss.as.quickstarts.kitchensink.util.Resources#produceLog)
我们只需要添加Hibernate Search的内容 (CDI相关内容可以参考Weld文档)
@Produces public FullTextEntityManager getFullTextEntityManager() { return Search.getFullTextEntityManager( em ); } @Produces @ApplicationScoped public SearchFactory getSearchFactory() { return getFullTextEntityManager().getSearchFactory(); } @Produces @ApplicationScoped public QueryBuilder getMemberQueryBuilder() { return getSearchFactory().buildQueryBuilder().forEntity( Member.class ).get(); }
Okay,现在万事俱备,我们可以使用Hibernate Search来替换org.jboss.as.quickstarts.kitchensink.data.MemberRepository中那两个使用了Criteria的方法了
注意,org.jboss.as.quickstarts.kitchensink.data.CriteriaMemberRepository#findById是需要把参数的类型从long改成String的,除此之外,Hibernate OGM是支持直接使用id进行查询的,所以不需要修改。
org.jboss.as.quickstarts.kitchensink.data.MemberRepository#findByEmail方法是通过email来查询一个Member,内部实现是通过Criteria来查询的。
org.jboss.as.quickstarts.kitchensink.data.MemberRepository#findAllOrderedByName方法是查询所有的Member并且按照name排序,同样是使用的Criteria。
@Inject private QueryBuilder queryBuilder; @Inject private FullTextEntityManager em; @Override public Member findById(String id) { return em.find( Member.class, id ); } @Override public Member findByEmail(String email) { Query luceneQuery = queryBuilder .keyword() .onField( "email" ) .matching( email ) .createQuery(); List resultList = em.createFullTextQuery( luceneQuery ) .initializeObjectsWith( ObjectLookupMethod.SKIP, DatabaseRetrievalMethod.FIND_BY_ID ) .getResultList(); if ( resultList.size() > 0 ) { return (Member) resultList.get( 0 ); } else { return null; } } @Override public List<Member> findAllOrderedByName() { Query luceneQuery = queryBuilder .all() .createQuery(); List resultList = em.createFullTextQuery( luceneQuery ) .initializeObjectsWith( ObjectLookupMethod.SKIP, DatabaseRetrievalMethod.FIND_BY_ID ) .setSort( new Sort( new SortField( "sortableStoredName", SortField.STRING_VAL ) ) ) .getResultList(); return resultList; }
可以看到,在这个新的类中,我们首先使用CDI自动注入了Hibernate Search的QueryBuilder和FullTextEntityManager (参见上面修改后的org.jboss.as.quickstarts.kitchensink.util.Resources工厂类)
在findById方法中,FullTextEntityManager#find实际上是代理给Hibernate OGM来处理的。
而在其余两个方法中,则是完全使用Hibernate Search的Query API创建了查询条件,然后交过Lucene来搜索的,还记得我们上面修改了Member类,在它被保存的时候创建Lucene索引的吧 (另,上面提到过,这个索引也是保存在infinispan当中的)
部署到JBoss AS 7
方便的是,我的同事Hardy已经准备好了这样一个修改过的项目,你可以直接从前面的链接中下载到本文中所介绍到的项目的源代码。
这是一个maven项目,所以你需要先安装好maven,Hardy还很贴心的在pom里面使用了cargo插件,所以你不需要下载JBoss AS 7了(尽管我还是推荐你下载一个看看,很值得的),cargo会自动帮你下载,并且完成部署等事情。
- 编译$ mvn clean package
- 运行$ mvn cargo:run
- 测试(基于Arquillian)$mvn test
程序运行后,您可以访问http://127.0.0.1:8080/ogm-kitchensink来查看具体效果。
此外,如果您尝试输入一个不合法的名字、电话号码或电子邮件地址,您将看到错误提示。这就是Bean Validation自动为您提供的输入校验。看看代码,还记得类中属性上定义的Bean Validation Annotations吗?(下面代码中我只保留了这部分注解)org.jboss.as.quickstarts.kitchensink.model.Member类中的属性上定义的Bean Validation Annotations吗?(下面代码中我只保留了这部分的annotation)
@NotNull @Size(min = 1, max = 50) @Pattern(regexp = "[A-Za-z ]*", message = "must contain only letters and spaces") private String name; @NotNull @NotEmpty @Email private String email; @NotNull @Size(min = 10, max = 12) @Digits(fraction = 0, integer = 12) private String phoneNumber;
好的,现在您已经在JBoss AS7上运行了一个使用Hibernate OGM + NO-SQL的程序了。
另外,JBoss AS 7的启动速度非常快。
23:27:10,050 INFO [org.jboss.as] (Controller Boot Thread) JBAS015874: JBoss AS 7.1.1.Final "Brontes" started in 2009ms - Started 133 of 208 services (74 services are passive or on-demand)
上面的日志是在我的机器上启动的速度,应该和Tomcat也差不多吧,但JBoss AS7是一个完整的通过Java EE6认证的应用服务器,而Tomcat只是一个servlet container。
Hardy还非常贴心的提供了一个小工具来帮助我们自动插入数据,这个工具就在项目根目录下,名为member-generator.rb。member-generator.rb
要使用这个工具,您首先需要安装(gem install)以下gem:
- gem install httparty
- gem install nokogiri
- gem install choice
然后,您可以通过执行以下命令来创建一些测试数据。
ruby member-generator.rb -a https://#:8080/ogm-kitchensink -c 20
部署到OpenShift
OpenShift是Redhat提供的一个PaaS服务,其背后的技术已经开源,可以在这里找到源代码和众多的实例项目。
OpenShift同时提供免费的服务和付费的服务。对于我们简单的试玩来说,免费的账户已经足够了。但如果是正式上线项目,还是推荐使用带支持的付费服务。
OpenShift支持的平台包括
- Java
- Ruby
- Node.js
- Python
- PHP
- Perl
并且它还提供了开箱即用的数据库支持,包括RMDBS和NO-SQL
- Mysql
- PostgreSQL
- Mongodb
例如,我们可以在OpenShift上轻松部署一个自己的WordPress或Drupal程序。
对于我们Java程序员来说,OpenShift最重要的是它内置了JBoss AS7(及其商业版本JBoss EAP6)的支持。因此,对于想要尝试Java EE6的开发者来说,这是一个非常好的消息,您可以直接使用这个服务。
首先,第一步没有别的,先到这里创建一个免费的账户。
然后从这里下载客户端,Linux / Mac / Windows都有对应的客户端供下载。
接下来,您需要创建一个domain,命令为rhc domain create -n ${my domain name},之后,您所有部署到OpenShift上的应用都会是http://${app name}-${domain name}.rhcloud.com的格式(免费账户可以创建3个应用程序,并且您可以使用自己的域名)。
然后创建应用,我们想要把这个程序部署到JBoss AS7上,使用以下命令rhc-create-app -a ${your app name} -t jbossas-7 --nogit,命令完成后,您会从输出中看到有一个git repo的地址。现在,您可以把这个地址作为一个remote添加到之前clone出来的ogm-kitchensink项目当中。
git remote add openshift ${repo-url}
然后,把ogm-kitchensink 推送到OpenShift提供的git repo当中。
git push -f openshift master
最后,您就可以访问您的应用了,地址如同之前所说的,是
http://${your app name}-${your domain name}.rhcloud.com
我创建了一个demo,可以访问http://ogm-stliu.rhcloud.com。