这个教程演示如何使用Aries JPA2.0的声明式服务。
你可以在github上找到完整的源代码github Karaf-Tutorial/tasklist-ds
声明式服务
声明式服务(DS)是对Blueprint的最大的扩展。它是OSGi轻量的服务注入框架。 DS允许你提供和消费OSGi服务,与配置一起工作。
DS的核心部分是使用xml文件定义scr组件和它们的依赖。 它们通常位于 OSGI-INF目录,在Manifest中使用"Service-Component"头声明组件描述文件的路径。幸运地是,没必要直接使用这个xml文件,因为DS已经支持注解了。这些是由maven-bundle-plugin处理的。唯一的前置条件是必须在maven-bundle-plugin插件的configuration instruction中使能:<_dsannotations>*</_dsannotations>
更多细节请参阅 http://www.aqute.biz/Bnd/Components
DS和Blueprint比较
让我们比较一下DS和blueprint。主要有几个不同点:
- Blueprint总是工作在一个完整的blueprint上下文上。所以当所有的强制服务代表都存在的时候,上下文才会启动。然后它会发布所有提供的服务。 因此,blueprint上下文自己不能依赖于任何它提供的服务。DS工作在组件上。一个组件就是一个提供服务的类,它依赖于其他的服务和配置。在DS中,你可以分开管理每一个组件,如启动和停止组件。 一个bundle有可能提供了两个组件,但是只有一个组件启动了,而另一个组件的依赖还没有。
- 与blueprint相比,DS更好地支持OSGi动态服务。 让我们看一个例子:你有一个DS和blueprint模块组件,提供了服务A,强依赖于服务B。Blueprint会等待强制服务第一次启动。如果服务B启动失败了,那么服务A在超时后也会失败,并且不能恢复。一旦blueprint上下文启动了,它就会一直启动,即使强制的服务没有了。这被称为服务阻尼。目的就是避免blueprint上下文频繁重启。服务作为动态代理被注入到blueprint bean中。内部,代理处理服务的替换和不可用。这会引起的问题是调用一个不可用的服务会阻塞当前线程,知道超时或者抛出RuntimeException。
另一个方面,在DS中组件的生命周期直接绑定到依赖的服务上。所以组件只会在所有的强制服务存在时才会激活,只要有一个强制服务消失了,那么组件就会失效。这样的优势在于注入到组件中的服务不必被代理,调用服务总是有效的。
- 每一个DS组件必须是一个服务。Blueprint可以有内部bean,只是用于互相连接内部类,这在DS中是不可能的。所以DS不是一个完全的依赖注入框架,从这个角度看DS缺少很多blueprint提供的特性。
- DS不支持扩展命名空间。Aries blueprint使用扩展的命名空间支持很多其他的Apache的工程。例如Aries jpa、Aries transactions、Aries authz、CXF、 Camel。所以在DS中使用这些技术有点困难。
- DS不支持拦截器。在blueprint中,扩展命名空间可以在bean前后引入和拦截。例如,用于事务处理的安全性。因此,DS不支持JPA,一般的用法都是托管都有拦截器。下面会说明jpa如何在DS中工作。
所以,如果DS是否适合你的工程,依赖于你需要的服务的动态性,依赖于是否DS可以集成到其他的工程。
JEE和JPA
JPA规范基于JEE,它拥有非常特殊的线程和拦截器模型。在JEE中,你在EntityManager管理的容器中使用会话bean。为了操作JPA实体,它看起来是这样的:
@Stateless
class TaskServiceImpl implements TaskService {
@PersistenceContext(unitName="tasklist")
private EntityManager em;
public Task getTask(Integer id) {
return em.find(Task.class, id);
}
}
在JEE中,调用getTask方法默认参与或者启动一个事务。如果方法调用成功了,那么事务就会被提交。如果方法发生了异常,那么事务就会回滚。
这些调用会进入到TaskServiceImpl 实例池子中。每一个实例每次只能被一个线程使用。因此,EntityManager接口不是线程安全的!
所以这个模型的优势是它看来其很简单,且允许相当少的代码。另一个方面,在容器外面,测试这样的代码有点困难,所以你必须模拟容器的行为来测试这个类。它也很难访问,例如访问em,因为它是私有的并且没有set方法。
Blueprint支持类似于JEE的代码风格,可以使用特殊的jpa、tx命名空间、处理事务、em管理的拦截器实现。
DS和JPA
在DS中,每一个组件都是单例的。所有只有唯一一个组件实例需要处理多线程访问。所以对于JPA与普通的JEE概念工作在DS中是不可能的。
当前,可能会注入EntityManagerFactory,处理EntityManager声明周期和事务,但是这将导致相当冗长且错误百出的代码。
Aries JPA 2.0.0为框架(如不提供拦截器的DS)提供特殊支持的第一个版本。这里的解决方案是JPATemplate的概念, 同时支持Java 8中的闭包。要想知道代码长什么样,请看看persistence 的章节。
不是注入EntityManager,我们注入了一个线程安全的JpaTemplate到代码中。我们需要将jpa代码放在闭包中,用jpa.txEpr()或jpa.tx()方法运行它。 JPATemplate会保证在闭包中具有像JEE一样的环境。由于每一个闭包都在自己的实例中运行,每一个线程都有一个em。这个代码也会分享或创建事务,事务也会提交或回滚,就像JEE一样。
所以,这个需要更多的代码,但优势是你不需要集成一个特殊的框架。代码有容易测试。参见下面的TaskServiceImplTest。
工程结构
- features
- model
- persistence
- ui
Features
定义karaf feature,安装示例和所有的必要的依赖。
Model
这个模块定义了Task JPA实体,TaskService接口和persistence.xml。model的详细描述请参见tasklist-blueprint示例。模型与这里的完全一样。
Persistence
TaskServiceImpl
@Component publicclass TaskServiceImpl implements TaskService {
private JpaTemplate jpa;
public Task getTask(Integer id) { return jpa.txExpr(em -> em.find(Task.class, id)); }
@Reference(target = "(osgi.unit.name=tasklist)") publicvoid setJpa(JpaTemplate jpa) { this.jpa = jpa; } } |
我们定义了需要一个OSGi服务TaskService和具有属性值“tasklist”的属性“osgi.unit.name”。
InitHelper
@Component publicclass InitHelper { Logger LOG = LoggerFactory.getLogger(InitHelper.class); TaskService taskService;
@Activate publicvoid addDemoTasks() { try { Task task = new Task(1, "Just a sample task", "Some more info"); taskService.addTask(task); } catch (Exception e) { LOG.warn(e.getMessage(), e); } } @Reference publicvoid setTaskService(TaskService taskService) { this.taskService = taskService; } } |
InitHelper类创建和持久化第一个task,所以UI有东西可以显示。这个例子也说明了业务代码如何使用TaskService。
@Reference TaskService taskService注入TaskService到字段field taskService。@Activate确保addDemoTasks()方法在注入这个组件后被调用
另一个有趣的地方是测试类 TaskServiceImplTest。它运行在OSGi的外面,使用特殊的persistence.xml来为测试创建EntityManagerFactory。它也演示了如何实例化ResourceLocalJpaTemplate,来避免为测试安装JTA事务管理。测试代码演示了TaskServiceImpl确实可以被用于普通Java代码,而不需要任何特殊的技巧。
UI
tasklist-ui模块用TaskService作为OSGi服务,将一个Servlet作为OSGi service发布。Pax-web whiteboard bundle会获得这个导出的Servlet,并用HttpService服务发布它,所以可以通过http访问它。
TaskListServlet
@Component(immediate = true, service = { Servlet.class }, property = { "alias:String=/tasklist" }) publicclass TaskListServlet extends HttpServlet { private TaskService taskService; protectedvoid doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // Actual code omitted }
@Reference publicvoid setTaskService(TaskService taskService) { this.taskService = taskService; } } |
上面的代码片段演示了当到处服务时,如何指定使用哪个接口及定义服务属性。
TaskListServlet用接口javax.servlet.Servlet和服务属性alias="/tasklist"导出。所以可以通过 http://localhost:8181/tasklist访问。
构建
确保使用JDK8,运行mvn clean install
安装
确保使用JDK 8,下载并解压Karaf 4.0.0,启动karaf,执行下面的命令。
创建DataSource配置,安装示例工程
cat https://raw.githubusercontent.com/cschneider/Karaf-Tutorial/master/tasklist-blueprint-cdi/org.ops4j.datasource-tasklist.cfg | tac -f etc/org.ops4j.datasource-tasklist.cfg feature:repo-add mvn:net.lr.tasklist.ds/tasklist/1.0.0-SNAPSHOT/xml/features feature:install example-tasklist-ds-persistence example-tasklist-ds-ui |
验证安装
首先,我们要未持久化单元检查JpaTemplate服务已经存在。
service:list JpaTemplate
[org.apache.aries.jpa.template.JpaTemplate] ------------------------------------------- osgi.unit.name = tasklist transaction.type = JTA service.id = 164 service.bundleid = 57 service.scope = singleton Provided by : tasklist-model (57) Used by: tasklist-persistence (58) |
Aries JPA应该已经从我们的model bundle创建了这个服务。如果这个服务没有工作,那么检查来自Aries JPA的日志消息。它应该打印它尝试的东西和等待的东西。你也可以检查EntityManagerFactory的存在和JpaTemplate 使用的EmSupplier服务是否存在。
可能的问题是丢失了DataSource,所以让我们检查一下它:
service:list DataSource
[javax.sql.DataSource] ---------------------- dataSourceName = tasklist felix.fileinstall.filename = file:/home/cschneider/java/apache-karaf-4.0.0/etc/org.ops4j.datasource-tasklist.cfg osgi.jdbc.driver.name = H2-pool-xa osgi.jndi.service.name = tasklist service.factoryPid = org.ops4j.datasource service.pid = org.ops4j.datasource.cdc87e75-f024-4b8c-a318-687ff83257cf url = jdbc:h2:mem:test service.id = 156 service.bundleid = 113 service.scope = singleton Provided by : OPS4J Pax JDBC Config (113) Used by: Apache Aries JPA container (62) |
这就是它应该的样子。Pax-jdbc-config根据配置"etc/org.ops4j.datasource-tasklist.cfg"创建了DataSource。 通过使用DataSourceFactory wit 属性"osgi.jdbc.driver.name=H2-pool-xa",所以 DataSource应该是成池了,完全准备好了XA事务。
下一步就是检查启动的DS组件:
scr:list
ID | State | Component Name -------------------------------------------------------------- 1 | ACTIVE | net.lr.tasklist.persistence.impl.InitHelper 2 | ACTIVE | net.lr.tasklist.persistence.impl.TaskServiceImpl 3 | ACTIVE | net.lr.tasklist.ui.TaskListServlet |
如果任何组件没有活动,你都可以像下面这样检查它:
scr:details net.lr.tasklist.persistence.impl.TaskServiceImpl
Component Details Name : net.lr.tasklist.persistence.impl.TaskServiceImpl State : ACTIVE Properties : component.name=net.lr.tasklist.persistence.impl.TaskServiceImpl component.id=2 Jpa.target=(osgi.unit.name=tasklist) References Reference : Jpa State : satisfied Multiple : single Optional : mandatory Policy : static Service Reference : Bound Service ID 164 |
测试
在浏览器中访问http://localhost:8181/tasklist
你可以看到一个task的列表
http://localhost:8181/tasklist?add&taskId=2&title=Another Task