本文是上文的延续.
我想先介绍一下JBoss Developer Framework, 这是JBoss 社区推出的一个全新的项目, 旨在帮助开发者更好的理解和使用JBoss的相关技术, 它提供了大量的实例教程(50+), 视频, 文章的内容, 迁移向导(从Java EE 5到EE 6, 从Spring到Java EE)等, 教你一点点的学会Java EE 6相关的各种技术, 并且涵盖了 REST, HTML5 等新的热点技术.
本文即以kitchensink, 一个 JBoss Developer Framework 提供的实例为基础展开.
首先, 先让我介绍一下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), 那么, 我们接下来就是看看如何做很少的更改, 让这个实例使用insinispan做为存储替换掉数据源中使用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 来看看具体的效果.
另外, 如果你尝试输入一个不合法的名字, 电话号码或者email地址的话, 你会看到错误提示, 这就是Bean Validation所自动帮你提供的输入校验, 来看看代码, 还记得 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;
Okay, 现在你已经有了一个使用Hibernate OGM + NO-SQL的程序跑在 JBoss AS7上面了
另外, 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
要使用这个工具的话, 你首先需要安装(gem install)如下的gem:
- gem install httparty
- gem install nokogiri
- gem install choice
然后就可以通过执行下面的命令来创建一些测试数据了.
ruby member-generator.rb -a http://localhost:8080/ogm-kitchensink -c 20
部属到 Openshift
openshift是Redhat所提供了一个Paas服务, 它背后的技术已经开源, 可以在这里找到其源代码和众多的实例项目.
openshift同时提供免费的服务和付费的服务, 对于我们简单的玩玩来讲, 免费的账户已经足够了, 但是如果你的项目是一个正式上线的项目, 还是推荐使用收费的带有支持的服务的.
openshift所支持的平台包括:
- Java
- Ruby
- Node.js
- Python
- PHP
- Perl
并且它还提供了开箱即用的数据库支持, 包括RMDBS和NO-SQL
- Mysql
- PostgreSQL
- Mongodb
例如我们可以在常见部属一个自己的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.