在Perl中使用Java类
最近,我开始了一份新工作,将我的职业生涯从系统管理转向Web开发。这次转变的一部分意味着我在工作中使用Java作为我的主要编程语言,并使用来自Java社区进程 (JCP) 的相对较新的技术,即Java内容库API (JCR),这是一个用于存储内容的分层数据库标准。然而,我不想让我在最喜欢语言中的技能荒废,所以我一直在用Perl尝试类似的技术。我决定直接将JCR移植到Perl,通过使用Inline::Java使Perl使用现有的Java实现。虽然我在路上遇到了一些困难,但令我非常惊讶的是,从Perl中使用Java类的过程竟然非常简单。
将JCR引入Perl
使用JCR从Perl的关键是Inline::Java
。这个库允许Perl程序以非常小的努力调用Java方法。对于对Inline::Java的介绍,我建议从Phil Crow的2003年将Java引入Perl文章开始,这篇文章发表在Perl.com上。我还大量参考了Inline::Java的文档,该文档非常完整,尽管可能不是详尽的。
要开始使用JCR,我使用了参考实现Jackrabbit。我下载了Jackrabbit JAR文件以及所有必需品,这些我都在Jackrabbit网站上找到了,在首次尝试部分。然后,我编写了一个小的脚本,使用Inline::Java
加载Jackrabbit的Java类,创建一个仓库,然后退出。我能够在Perl中像在Java中一样快或更快地使用Jackrabbit完成首次尝试。
#!/usr/bin/perl
use strict;
use warnings;
use Inline
Java => 'STUDY',
STUDY => [ qw(
org.apache.jackrabbit.core.TransientRepository
javax.jcr.Repository
) ],
AUTOSTUDY => 1;
my $repository = org::apache::jackrabbit::core::TransientRepository->new;
my $session = $repository->login;
eval {
my $user = $session->getUserID;
my $name = $repository
->getDescriptor($javax::jcr::Repository::REP_NAME_DESC);
print "Logged in as $user to a $name repository.\n";
};
if ($@) {
print STDERR "Exception: ", $@->getMessage, "n";
}
$session->logout;
此代码是Jackrabbit网站上的第一个教程的直接Perl移植。要运行此代码,您必须确保您的类路径正确。因为我最初将JCR文件放入了我的工作目录,所以我只是运行了以下命令来使其工作
% export CLASSPATH=$CLASSPATH:`echo *.jar | tr ' ' ':'`
% perl firsthop.pl
五分钟内,我编写了一个Perl脚本,该脚本可以访问Jackrabbit库,创建一个仓库,并以匿名身份登录。这回答了我的第一个问题:我可以将JCR移植到Perl吗?是的。
首次遇到的困难
在Jackrabbit教程中的“第二次尝试”之后,我遇到了第一个困难。要使用Jackrabbit创建节点和属性,您必须使用用户名和密码登录。然而,JCR使用字符数组作为密码参数。由于Inline::Java
有助于将Java字符串对象转换为Perl标量,我无法找到一种方法来实现这一点。
我还意识到,我不想在我的Perl代码中使用冗长的Java命名空间。写出org::apache::jackrabbit::core::TransientRepository
或javax::jcr::Repository
并不是我时间的高效利用,而且使Perl代码看起来很奇怪。
此外,我不想使用依赖于Jackrabbit的库。有几个其他的JCR实现要么已经编写,要么正在开发中。Day有CRX,还有一个名为Jaceira的开源实现正在进行中,eXo也创建了一个JCR实现,仅举几例。
考虑到这些困难以及我知道将会出现的其他问题,现在是时候将这个项目作为一个Perl模块来构建了。
创建包装器
为了创建我想要的抽象,很快就变得明显,我需要一种方法来围绕由 Inline::Java
生成的存根构建包装器。因此,我开始编写一个脚本,可以针对 JCR 中的每个库生成一个 Perl 包。除了帮助包装特殊情况外,每个包装包还会使用更符合 Perl 代码(尤其是我的 Perl 代码,类似于 Conway 的Perl 最佳实践中的约定)的命名约定来清理 Java 命名空间。
使用 Java 反射
首先,我需要发现要包装的类、方法和字段。JCR 规范中有超过 50 个类、接口和异常——我太懒了,不想全部输入。此外,JCR 目前正在通过 JSR 283 进行修订,我不想以后再更新类列表。最后,我想让我的包装器能够专门处理每个方法,因为使用 AUTOLOAD()
是邪恶的(有时有用,但仍然是邪恶的)。
我编写了一个 Java 程序,用于查找 JCR JAR 文件中的所有类,并将这些类名以及有关方法、构造函数和字段的其他信息写出来。我使用 YAML 格式化的文件来存储这些信息。我大量使用了 Java 反射 API 来实现这一点。您可以在 Java::JCR 发行版中查看 JCR 包生成器的完整源代码(inc/JCRPackageGenerator.java)。以下是 YAML JCR 包输出文件(inc/packages.yml)中的一个条目
javax.jcr.SimpleCredentials:
isa:
- java.lang.Object
- javax.jcr.Credentials
has_constructors: 1
methods:
instance:
getAttributeNames: Array:java.lang.String
getUserID: java.lang.String
toString: java.lang.String
getPassword: Array:char
getAttribute: java.lang.Object
setAttribute: void
removeAttribute: void
我选择放入 YAML 文件中的信息主要是通过实验 Perl 生成器脚本得出的结果。因为我编写了一个通用的处理程序来执行所需的解包操作,可以处理任何一组参数,所以我没有在这里记住它们。另一方面,知道返回类型,记录在每个方法名称之后,对我的实现有帮助。
使用 Perl 生成代码
接下来,我编写了一个 Perl 脚本来加载 YAML 文件中的信息并生成包。您还可以查看 inc/package-generator.pl
的完整源代码。这个脚本相当丑陋。我在 Perl 中使用嵌入式 here-documents 做了所有生成信息的工作。更好的做法是使用模板工具,比如 Andy Wardley 的 Template Toolkit,这是我最终想做的。
基本上,这个程序遍历从 YAML 文件中加载的所有条目,并为每个类生成一个包。它从 Java 包名创建一个 Perl 包名,并在发行版中的适当位置创建一个 Perl 包文件。
例如,javax.jcr.nodetype.ItemDefinition
获得了名为 Java::JCR::Nodetype::ItemDefinition
的 Perl 包名和文件位置 lib/Java/JCR/Nodetype/ItemDefinition.pm。
代码在包文件中注入了库存的头部和尾部。所有真正的魔法都发生在这两者之间。
处理静态字段
代码通过修改符号表来添加静态字段,使包装器指向自动生成的存根。例如,Java::JCR::PropertyType
获得了几个条目,如下所示
*STRING = *Java::JCR::javax::jcr::PropertyType::STRING;
*BINARY = *Java::JCR::javax::jcr::PropertyType::BINARY;
*LONG = *Java::JCR::javax::jcr::PropertyType::LONG;
对于那些可能不知道的人来说,第一行通过直接修改 符号表 使名称 Java::JCR::PropertyType::STRING
与使用较长的名称,Java::JCR::javax::jcr::Property::STRING
完全相同。
好的,看到这里,你可能想知道为什么所有的 Inline::Java
存根现在都在前面加了 Java::JCR
。原因是,在生成的代码中,我使用了 study_classes()
程序来导入 Java 代码,并指定导入的基本包应为 Java::JCR
study_classes(['javax.jcr.PropertyType'], 'Java::JCR');
为什么?这其实并不那么关键,但我想的是,因为我放在CPAN上的包名是Java::JCR
,我真的很不想在此时将包放入外部命名空间。因为包装器隐藏了所有长名称,所以内部名称的实际长度并不重要。
处理构造函数和方法
在字段之后,代码会检查Java类是否提供了构造函数(即,它是一个类而不是接口)。实际上,我从未真正使用处理构造函数的代码,原因有两个:
- 异常。由于我将稍后解释的原因,我不生成异常类。因此,这些构造函数未被使用。
SimpleCredentials
。唯一剩下的具有构造函数的类是java.jcr.SimpleCredentials
,这是我之前提到的特殊情况。因此,我只需要处理构造函数作为特殊情况。我稍后也会介绍特殊情况。
在构造函数之后,程序会遍历每个方法并生成静态和实例方法包装器。下面是来自Java::JCR::Repository
的典型方法包装器:
sub login {
my $self = shift;
my @args = Java::JCR::Base::_process_args(@_);
my $result = eval { $self->{obj}->login(@args) };
if ($@) { my $e = Java::JCR::Exception->new($@); croak $e }
return Java::JCR::Base::_process_return($result, "javax.jcr.Session", "Java::JCR::Session");
}
驼峰命名法
这个特定的例子没有显示,但我还把每个方法的Java驼峰命名法名称更改为全部小写加下划线,这是Perl中命名方法的更常见方式。我可能会在未来添加使用Java名称的别名,但我不喜欢Perl代码中的Java式命名约定。这个过程中最有趣的部分是处理包含全大写缩写的名称。这需要两行Perl代码:
my $perl_method_name = $method_name;
$perl_method_name =~ s/(p{IsLu}+)/_L$1E/g;
/(\p{IsLu}+)/
匹配任何大写字母或大写字母字符串。替换应用了\L
修饰符到正则表达式,将匹配的片段转换为全小写。我在前面添加一个下划线来完成转换。因此,名为getDescriptor
的方法变为get_descriptor
,而名为getNodeByUUID
的方法变为get_node_by_uuid
。顺便说一下,如果任何名称在末尾之前有缩写(例如,如果有一个getUUIDNode
,它将变为get_uuidnode
),这不会很好地工作。幸运的是,这种情况在JCR API中从未出现过。
方法包装器
Java::JCR::Base::_process_arg()
处理传递给每个方法的参数。这个函数在参数列表中寻找任何生成的包装器对象(任何isa
Java::JCR::Base
的对象)并解包生成的存根,通过从祝福的散列中提取obj
键。
sub _process_args {
my @args;
for my $arg (@_) {
if (UNIVERSAL::isa($arg, 'Java::JCR::Base')) {
push @args, $arg->{obj};
}
else {
push @args, $arg;
}
}
return @args;
}
然后,包装器通过传递解包的参数(就像包装器不存在一样)来执行包装方法。
我确保在eval
中包装每个调用,因为Inline::Java
将Java异常作为Perl异常对象传递。如果抛出异常,我将它包装在自定义类Java::JCR::Exception
中,这是我自己编写的。
最后,代码返回结果。如果返回类型有包装器,例如在login()
中),我使用Java::JCR::Base::_process_return()
来转换类并包装它。
sub _process_return {
my $result = shift;
my $java_package = shift;
my $perl_package = shift;
# Null is null
if (!defined $result) {
return $result;
}
# Process array results
elsif ($java_package =~ /^Array:(.*)$/) {
my $real_package = $1;
return [
map { bless { obj => cast($real_package, $_) }, $perl_package }
@{ $result }
];
}
# Process scalar results
else {
return bless {
obj => cast($java_package, $result),
}, $perl_package;
}
}
这提出了两个考虑:为什么有自定义异常类?为什么需要转换对象?在两种情况下,我这样做是为了处理Inline::Java
中的小问题。
就异常而言,生成的异常对象处理Perl字符串化不太好。由于许多异常处理程序假设异常是字符串或正确字符串化的,这可能(并且对我来说确实如此)是一个问题。我的异常类确保字符串化工作正确。
至于转换,Inline::Java
基于这样一个假设,即你想以最具体的形式使用类,但如果你没有研究那种形式,你将得到一个无法调用任何方法的一般对象。而不是启用可能昂贵的AUTOSTUDY
选项,以确保Inline::Java
研究一切,然后使包装器更智能,我选择将对象转换成预期的返回类型。这限制了某些灵活性。
加载包
除了定制组件外,我还需要一些额外的辅助工具来完成工作。我不想编写大量的use
语句来使用这个库。作为一个JAPH,我喜欢保持简单。因此,如果需要使用JCR和Jackrabbit,我只想说
use Java::JCR;
use Java::JCR::Jackrabbit;
我在主包中包含了一个包加载器,名为Java::JCR,它会处理这些细节,并为JCR中的每个子包创建一个包。加载器看起来像
sub import_my_packages {
my ($package_name, $package_file) = caller;
my %excludes = map { $_ => 1 } @_;
my $package_dir = $package_file;
$package_dir =~ s/.pm$//;
my $package_glob = File::Spec->catfile($package_dir, '*.pm');
for my $package (glob $package_glob) {
$package =~ s/^$package_dir///;
$package =~ s/.pm$//;
$package =~ s///::/g;
next if $excludes{$package};
eval "use ${package_name}::$package;";
if ($@) { carp "Error loading $package: $@" }
}
}
我确保在包加载完成后调用该方法,并传入排除项以防止它加载所有子包。这需要进一步的增强,以允许在Java::JCR
命名空间下进行未来扩展,以便不自动加载它们,但这是一个很好的起点。然后,我为每个子包构建了一个类,这些类继承自Java::JCR,然后调用此方法来加载这些类。
连接到Jackrabbit
显然,下一步是创建连接到Jackrabbit的代码。这是在Java::JCR::Jackrabbit中完成的。初始实现非常简单
use base qw( Java::JCR::Base Java::JCR::Repository );
use Inline (
Java => 'STUDY',
STUDY => [],
);
use Inline::Java qw( study_classes );
study_classes(['org.apache.jackrabbit.core.TransientRepository'], 'Java::JCR');
sub new {
my $class = shift;
return bless {
obj => Java::JCR::org::apache::jackrabbit::core::TransientRepository
->new(@_),
}, $class;
}
我扩展了Java::JCR::Repository以添加一个调用Jackrabbit构造函数的构造函数。完成。
处理特殊情况
尽管做了所有这些工作,但我仍然无法进行第二次跳跃,因为我还没有解决传递字符数组的问题。然而,有了我建立的基础设施,这个问题现在可以解决了。
我创建了一个额外的YAML配置文件,名为specials.yml。该文件包含适当使用的手动编码的替代方案。然后,我为新的构造函数编写了替代方案
javax.jcr.SimpleCredentials:
new: |-
sub new {
my $class = shift;
my $user = shift;
my $password = shift;
my $charArray = Java::JCR::PerlUtils->charArray($password);
return bless {
obj => Java::JCR::javax::jcr::SimpleCredentials->new($user, $charArray),
}, $class;
}
然后,我重新运行了生成脚本。幸运的是,我已经将其改进为使用任何已实现的函数或构造函数,而不是自动生成一个。
为了进行转换,我还需要嵌入一些额外的Java代码。我编写了一个非常小的Java类,名为PerlUtils
,用于处理转换
use Inline (
Java => <<'END_OF_JAVA',
class PerlUtils {
public static char[] charArray(String str) {
return str.toCharArray();
}
}
END_OF_JAVA
);
给定一个字符串,它返回一个字符数组,以传递回SimpleCredentials
构造函数。无需进行其他工作。我现在可以在Perl中执行JCR第二次跳跃(ex/secondhop.pl)。该脚本连接到Jackrabbit存储库,以“用户名”身份登录,密码为“密码”,然后创建一个节点。
将句柄用作InputStream
Jackrabbit教程的第三(也是最后)步演示了使用XML文件进行节点导入。然而,为了执行显示的导入,你必须将一个InputStream
传递给importXML()
方法。虽然Inline::Java
提供了使用Java InputStream
作为Perl文件句柄的能力,但它没有提供反向映射。因此,我需要另一个特殊处理程序和一组额外的辅助方法。
特殊的代码配置看起来像
javax.jcr.Session:
import_xml: |-
sub import_xml {
my $self = shift;
my $path = shift;
my $handle = shift;
my $behavior = shift;
my $input_stream = Java::JCR::JavaUtils::input_stream($handle);
$self->{obj}->importXML($path, $input_stream, $behavior);
}
这调用了一个名为input_stream()
的方法,这是一个Perl子程序。
sub input_stream {
my $glob = shift;
my $glob_val = $$glob;
$glob_val =~ s/^\*//;
my $glob_caller = Java::JCR::GlobCaller->new($glob_val);
return Java::JCR::GlobInputStream->new($glob_caller);
}
如你所见,这个子程序使用两个不同的Java类来提供从Perl文件句柄到Java InputStream
的接口。第一个类,Java::JCR::GlobCaller
,使用Inline::Java
提供的回调功能执行大部分实际工作。它被传递给Java::JCR::GlobInputStream
,当JCR从流中读取时,它会调用read()
public int read() throws InlineJavaException, InlineJavaPerlException {
String ch = (String) CallPerlSub(
"Java::JCR::JavaUtils::read_one_byte", new Object[] {
this.glob
});
return ch != null ? ch.charAt(0) : -1;
}
read_one_byte()
函数是对Perl内置的getc
的一个非常基本的包装。
sub read_one_byte {
my $glob = shift;
my $c = getc $glob;
return $c;
}
有了这些,你现在可以执行Perl中的第三次JCR跳跃。通过执行此脚本,您将连接到存储库,登录,然后从XML文件创建节点和属性。
准备分发
现在,实现部分基本完成。您可以使用Java::JCR
连接到Jackrabbit仓库,登录,创建节点和属性,以及从XML导入数据。还有很多未测试的功能,但基本功能已经具备。完成这些后,我开始为分发做准备。然而,由于一些Java库是使用库的先决条件,因此库在构建和安装方面有一些特殊需求。您只需运行以下命令即可安装它:
% cpan Java::JCR
我需要一种构建此库的方法。我最喜欢的构建工具是Ken Williams的Module::Build。它被广泛使用,与CPAN安装程序兼容,并与我最喜欢的Linux发行版的打包工具g-cpan.pl良好协作,该工具适用于Gentoo。最后,它易于扩展。
在自定义Module::Build
时,我更喜欢创建自定义构建模块,而不是将扩展直接内联在Build.PL文件中。在这种情况下,我将其命名为Java::JCR::Build。我将它放在名为inc/的目录中,其中包含我用于生成软件包的其他工具。
在创建扩展Module::Build
的基本模块后,我添加了一个名为get_jars
的自定义操作来获取JAR文件。我还添加了在构建过程中执行此操作的代码,通过扩展code
ACTION。
sub ACTION_get_jars {
my $self = shift;
eval "require LWP::UserAgent"
or die "Failed to load LWP::UserAgent: $@";
my $mirror_dir
= File::Spec->catdir($self->blib, 'lib', 'Java', 'JCR');
mkpath( $mirror_dir, 1);
my $ua = LWP::UserAgent->new;
print "Checking for needed jar files...n";
while (my ($file, $url) = each %jars) {
my $path = File::Spec->catfile($mirror_dir, $file);
$self->add_to_cleanup($path);
next if -f $path;
my $response = $ua->mirror($url, $path);
if ($response->is_success) {
print "Mirroring $url to $file.n";
}
elsif ($response->is_error) {
die "An error occurred fetching $url to $file: ",
$response->status_line, "n";
}
}
}
sub ACTION_code {
my $self = shift;
$self->ACTION_get_jars;
$self->SUPER::ACTION_code;
}
我使用Gisle Aas的LWP::UserAgent从公共Maven仓库获取JAR文件,并将它们放入构建库目录blib中。Module::Build
将在安装过程中将这些JAR文件复制到适当的位置,并处理其余部分。
我还需要在Java::JCR
中添加一些代码来提前设置CLASSPATH
。
my $classpath;
BEGIN {
my @classpath;
my $this_path = $INC{'Java/JCR.pm'};
$this_path =~ s/.pm$//;
my $jar_glob = File::Spec->catfile($this_path, "*.jar");
for my $jar_file (glob $jar_glob) {
push @classpath, $jar_file;
}
$classpath = join ':', @classpath, ($ENV{'CLASSPATH'} || '');
$ENV{'CLASSPATH'} = $classpath;
}
此段代码请求Perl库的位置,我假设这是JAR文件的安装位置。然后,我在该目录中找到所有以.jar结尾的文件,并将它们放入CLASSPATH
。不幸的是,当使用冒号作为路径分隔符时,我的代码假设了一个Unix环境。未来的版本可以确保它在其他系统上也能正常工作,但由于我只使用基于Unix的操作系统,我的动力不足。
有了这一切,您现在可以通过下载tarball并运行以下命令来部署它:
% perl Build.PL
% ./Build
% ./Build test
% ./Build install
它工作!
测试
我还没有提到这一点,但在构建此库的整个过程中,我还构建了一系列测试用例。您可以在分发的t/目录中找到这些测试用例。前几个测试实际上只是Jackrabbit教程的变体,以及一个确保POD文档没有错误的测试(每个模块作者都应该使用此测试;您只需将其复制并粘贴到任何项目中)。
结语
我喜欢Perl。从Java到Perl的移植比我想象的要容易。我希望分享我的成功,以激发他人的动力。向Ken Williams、Patrick LeBoutillier及其协助构建实现此功能的工具的其他人致以敬意。
干杯。
标签
反馈
这篇文章有什么问题吗?请通过在GitHub上打开问题或拉取请求来帮助我们。
- More commenting... maybe?
github.polettix.it - Perl Weekly Challenge 121: Invert Bit
blogs.perl.org - Web nostalgia: MojoX::Mechanize
github.polettix.it - On the eve of CPAN Testers
blogs.perl.org - PWC121 - The Travelling Salesman
github.polettix.it - PWC121 - Invert Bit
github.polettix.it - Floyd-Warshall algorithm implementations
github.polettix.it - Perl Weekly Challenge 120: Swap Odd/Even Bits and Clock Angle
blogs.perl.org - How I Uploaded a CPAN Module
blogs.perl.org - App::Easer released on CPAN
github.polettix.it