再看OSGi模块层——从在OSGi容器中引入Thymeleaf说起

 由  Ruici 发布

引言

Thymeleaf是一个开源的XML/XHTML/HTML5模板引擎,它的主要优势在于创建的模板可以被浏览器良好的支持并正确显示,非常适合于直接用于原型(prototype)设计。一个示例模板如下:

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Good Thymes Virtual Grocery</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" type="text/css" media="all" href="../../css/gtvg.css" th:href="@{/css/gtvg.css}" />
</head>
<body>
    <p th:text="#{home.welcome}">Welcome to our grocery store!</p>
</body>
</html>

可以看到,它不包含任何非标准HTML标签,只是通过引入th:*这样的标签属性来进行模板渲染(浏览器在遇到不能识别的属性时会自动忽略)。这样做的好处是显而易见的:设计师/前端工程师可以直接在模板文件上工作并可以将它们直接放在应用服务器中。这篇文章比较了传统的JSP模板和Thymeleaf的开发流程,其中提到的好处十分具有吸引力,所以我们决定在我们的社区项目里使用Thymeleaf替代JSP。

使用Thymeleaf需要考虑的问题

Spring支持

我们使用的Web框架是SpringMVC,令人兴奋的是,Thymeleaf的一个子项目Thymeleaf-Spring完全支持Spring(并且thymeleaf-spring4随着spring4的发布立马推出了)。

取代JSP

我们在实用JSP的过程中,用JSP标签封装了大量的业务逻辑。Thymeleaf同样是一门灵活的模板语言,它良好的扩展机制——包括方言(Dialect)和处理器(Processor)能够完全替代JSP自定义标签。

OSGi环境支持

理论上来说,任何不包含OSGi元信息的jar包,都是可以放在OSGi环境中的,主要方法有三种:

  1. 嵌入Jar包——假设Bundle A依赖的jar包没有被bundle化,那么可以将其加入Bundle A的Bundle-Classpath并一起打包,即该jar包作为Bundle A的一部分。
  2. Virgo Bundlor工具——Bundlor会自动分析jar包中的类以及依赖关系,自动生成OSGi Manifest文件。
  3. 利用bndtools重新编译源码,bndtools是OSGi官方联盟推荐的一个工具,这里也非常推荐大家使用它来bundle化普通jar包。

尝试将thymeleaf jar包转化为bundle

在bundle化了thymeleaf和thymeleaf-spring两个jar包后,重写模板并本地运行网站,却发现一个问题:用到了表单功能(thymeleaf-spring支持)的页面,全部会抛出一个异常:

ClassNotFoundException:org.springframework.web.servlet.tags.form.ValueFormatterWrapper

看上去很像SpringMVC的Package没有引入,但当我打开SpringMVC中的org.springframework.web.servlet.tags.form一看,发现居然没有这个类!原来,这个类是thymeleaf-spring扩展的,藏在了thymeleaf-spring中。

为什么会找不到类

ValueFormatterWrapper做了什么

public final class ValueFormatterWrapper {

    public static String getDisplayString(final Object value, final boolean htmlEscape) {
        return ValueFormatter.getDisplayString(value, htmlEscape);
    }

    public static String getDisplayString(final Object value, final PropertyEditor propertyEditor, final boolean htmlEscape) {
        return ValueFormatter.getDisplayString(value, propertyEditor, htmlEscape);
    }

    private ValueFormatterWrapper() {
        super();
    }
}

原来这个类,是访问了同名包下的一个非public的ValueFormatter类(不包含任何限定符,仅包内可以访问,包外不能访问),而ValueFormatter是位于org.springframework.web.servlet这个bundle中的,这样一来改包名重新编译是不可行了。

OSGi环境下的类搜索顺序

由于抛出的是ClassNotFoundException,所以赶紧翻翻书,复习一下OSGi类加载机制吧,《OSGi in Action》中提到:

1.Requests for classes in java. packages are delegated to the parent class loader; searching stops with either a success or failure (section 2.5.4).

2.Requests for classes in an imported package are delegated to the exporting bundle; searching stops with either a success or failure (section 2.5.4).

3.The bundle class path is searched for the class; searching stops if found but con- tinues to the next step with a failure (section 2.5.4).

这是一个最基本的类搜索顺序,随着内容的展开,这个顺序还会进行完善。这里要注意的是第二点:如果Import-Package中引入包中的类在导出该包的bundle中没有找到,那么搜索失败并且停止,也就是说不会进入第三步。

Embbed jar方式

这种情况下,我们在Bundle A打入了thymeleaf和thymeleaf-spring两个jar包,并正确的设置了Bundle-Classpath。那么考虑一个问题:org.springframework.web.servlet.tags.form这个包,我们到底应不应该在Import-Package中写入呢?

  • 如果org.springframework.web.servlet.tags.formImport-Package中引入,那么很显然,位于Bundle-Classpath上的org.springframework.web.servlet.tags.form.ValueFormatterWrapper是找不到了,这种情况就是问题中出现的实际场景。
  • 如果org.springframework.web.servlet.tags.formImport-Package中不存在,那么org.springframework.web.servlet.tags.form.ValueFormatterWrapper可以在Bundle-Classpath上被找到,可麻烦的是这个类引用了同名包下的另外一个类org.springframework.web.servlet.tags.form.ValueFormatter,由于Import-Package中没有这个包,于是org.springframework.web.servlet.tags.form.ValueFormatter找不到了。

将Thymeleaf转换为Bundle

这种情况下,thymeleaf-spring的Import-Package中如果不引入 org.springframework.web.servlet.tags.form,那么还是会出现和Embbed jar方式同样的问题;可是如果引入了呢?这两个Bundle同时导出了org.springframework.web.servlet.tags.form这个包,Bundle A可不会这么智能,在使用同名包下不同的类时自动去找到合适的Bundle。

解决问题

问题找到了,本质上这是一个split packages问题,即同一个包的类分散在了不同的bundle里。这个问题在《OSGi in Action》中也有提到:

SPLIT PACKAGE A split package is a Java package whose classes aren’t contained in a single JAR but are split across multiple JAR files. In OSGi terms, it’s a package split across multiple bundles.

Require-Bundle

同样的,书中也提到了这个问题的解决方案——Require Bundle:

REQUIRE-BUNDLE This header consists of a comma-separated list of target bundle symbolic names on which a bundle depends, indicating the need to access all packages exported by the specifically mentioned target bundles.

Require-Bundle实际上,就是将一个bundle所有导出的包引入,那它和Import-Package有什么区别吗?

《OSGi in Action》在引入Require-Bundle后更新了类搜索顺序:

1.Requests for classes in java. packages are delegated to the parent class loader; searching stops with either a success or failure (section 2.5.4).

2.Requests for classes in an imported package are delegated to the exporting bundle; searching stops with either a success or failure (section 2.5.4).

3.Requests for classes in a package from a required bundle are delegated to the exporting bundle; searching stops if found but continues with the next required bundle or the next step with a failure.

4.The bundle class path is searched for the class; searching stops if found but con- tinues to the next step with a failure (section 2.5.4).

可以看出,Require-Bundle的特点在于,搜索一个类时,如果第一个导出了这个类所在包的bundle中没有找到的话,搜索不会停止,而是会继续搜索其他满足条件的bundle,正是这个特点,为解决split package问题提供了基础。试想,如果我的Manifest文件中有如下定义:

Require-Bundle: org.springframework.web.servlet;version="3.1", org.thymeleaf.spring3;version="2.1"

不是正好能够满足,要找的类可以在这两个包中轮流搜索,那就肯定能够找到了吗?问题似乎已经完美解决,可遗憾的是——还没有。

问题出在哪里呢?让我们再回想一下org.springframework.web.servlet.tags.form.ValueFormatterWrapper,他引用了org.springframework.web.servlet.tags.form.ValueFormatter这个非公开类。

我们不要忘记,OSGi环境里,每一个bundle都有自己类加载器(Class Loader),所以这两个类位于不同的类加载器中:

Java only allows package-private access to classes loaded by the same class loader. Two classes in the same package, but loaded by two dif- ferent class loaders, can’t access each others’ package-private members.

《OSGi in Action》中提到的上面这段文字,正好讲清楚了问题所在——类加载器的限制导致的类访问权限问题!道路真是曲折啊。

终极解决方案

既然是类加载器不同导致的问题,那可不可以把这两个bundle放在一个类加载器里呢?原来《OSGi in Action》的作者,早就帮我们想到了这种情况:

FRAGMENT-HOST This header specifies the single symbolic name of the host bundle on which the fragment depends, along with an optional bundle version range.

Fragment正是能够帮助我们解决这一问题的终极武器,因为Fragment只有当Host bundle启动之后才能运行,他们共用一个类加载器,所以我们只需要把thymeleaf-spring作为SpringMVC的fragment bundle就好了:

Fragment-Host: org.springframework.web.servlet; bundle-version="3.1"
Export-Package: org.springframework.web.servlet.tags.form;version="3.1"

利用bnd工具重新编译thymeleaf-spring3,加入这段Manifest文件后,问题终于得到了解决。现在Thymeleaf以及Thymeleaf-Spring已经很好的在OSGi环境下运行了!Congratulations~

类搜索顺序(最终版)

在引入了Fragment后,OSGi模块层也进入了尾声,类的搜索顺序也终于完整了:

1.Requests for classes in java. packages are delegated to the parent class loader; searching stops with either a success or failure (section 2.5.4).

2.Requests for classes in an imported package are delegated to the exporting bundle; searching stops with either a success or failure (section 2.5.4).

3.Requests for classes in a package from a required bundle are delegated to the exporting bundle; searching stops if found but continues with the next required bundle or the next step with a failure.

4.The host bundle class path is searched for the class; searching continues with a failure (section 2.5.4).

5.Fragment bundle class paths are searched for the class in the order in which the fragments were installed. Searching stops if found but continues through all fragment class paths and then to the next step with a failure.

6.The bundle class path is searched for the class; searching stops if found but con- tinues to the next step with a failure (section 2.5.4).

7.If the package in question isn’t exported or required, requests matching any dynamically import package are delegated to an exporting bundle if one is found. Searching stops with either a success or a failure (section 5.2.2).

7是5.2.2节讲完Dynamic Import和Optional Import后加上的,由于本文不涉及,故仅在最后补上。

总结

《OSGi in Action》这本书,以前也曾读过一遍,包括第五章——Delving deeper in modularity,当时读到Require-Bundle、Dynamic/Option Import、Fragment等等概念时,虽然结合书上提供的小例子(3个Bundle互相依赖,Import/Export:org.foo.bar之类的包)也能够看懂大体意思,但绝对没有想到这些东西的背景。如今在实际工作中遇到了此类问题,结合书本知识加以解决,对书上的场景理解的更为深刻,对于之前不太能够的理解的问题背景、解决方案以及上文几次提到的类搜索顺序,如今结合例子印证,真是醍醐灌顶!

最后,再次强烈推荐《OSGi in Action》这本OSGi开发者枕边必备的好书!

参考文献

  • OSGi in Action : Creating Modular Applications in Java, Richard S. Hall, Karl Pauls, Stuart McCulloch, and David Savage

wmz 2015-06-23 19:37

OSGi的企业级快速开发平台,首推:http://osgi.jxtech.net

顶(10) 踩(0) 回复

罗俊杰 2014-04-01 21:33

将OSGi模块层理论和实践结合得极好的一篇文章

顶(2) 踩(0) 回复
查看评论