Apache UIMA 快速入门与集成示例
以房间号抽取为例演示 Apache UIMA 3.6.1 快速入门:pom、TypeSystem、Annotator、描述符 XML、运行回显与 JCas 读回注解,扩展级联与 CPE 集成,输出均为实测。
Apache UIMA 快速入门与集成示例
一、项目结构总览
本章在 /opt/uima-demo 下构建一个完整的 UIMA 分析工程:从文本中抽取形如 9244-108 的房间号,聚合两个注解器,最后用 CPE 批量处理目录。实测环境:OpenJDK 17、uimaj-core 3.6.1(Maven Central 最新稳定版)。
/opt/uima-demo
├── pom.xml
├── descriptors/
│ ├── TypeSystem.xml # 类型系统描述符
│ ├── RoomNumberAnnotator.xml # 原语 AE 描述符(抽取)
│ ├── RoomNumberCounter.xml # 原语 AE 描述符(统计)
│ ├── AggregateAE.xml # 聚合 AE 描述符(级联)
│ ├── TextCollectionReader.xml # Collection Reader 描述符
│ └── RoomNumberCPE.xml # CPE 描述符(由代码生成)
├── data/in/room_notes.txt # 测试文本
└── src/main/java/com/example/uima/
├── RoomNumber.java # JCasGen 生成,勿手改
├── RoomNumberAnnotator.java # 注解器:正则抽取
├── RoomNumberCounter.java # 注解器:读回统计
├── TextCollectionReader.java # 文本目录读取器
├── RunPipeline.java # 运行入口:AE 管线
└── RunCPE.java # 运行入口:CPE
二、pom.xml 全文
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>uima-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
<uima.version>3.6.1</uima.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.apache.uima</groupId>
<artifactId>uimaj-core</artifactId>
<version>${uima.version}</version>
</dependency>
<dependency>
<groupId>org.apache.uima</groupId>
<artifactId>uimaj-cpe</artifactId>
<version>${uima.version}</version>
</dependency>
<dependency>
<groupId>org.apache.uima</groupId>
<artifactId>uimaj-tools</artifactId>
<version>${uima.version}</version>
</dependency>
<dependency>
<groupId>org.apache.uima</groupId>
<artifactId>uimaj-json</artifactId>
<version>${uima.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.36</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
四个 uimaj 构件的分工:uimaj-core 框架核心与 CAS/JCas;uimaj-cpe 集合处理引擎;uimaj-tools JCasGen 与 RunAE 工具;uimaj-json CAS-JSON 序列化(传递依赖 jackson-core 2.20.0)。
三、TypeSystem 与 JCas 代码生成
3.1 类型系统描述符
descriptors/TypeSystem.xml 声明自定义注解类型 com.example.uima.RoomNumber,继承 uima.tcas.Annotation,带一个 String 特征 pattern:
<?xml version="1.0" encoding="UTF-8"?>
<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
<name>RoomNumber TypeSystem</name>
<description>Demo type system declaring the RoomNumber annotation type</description>
<version>1.0</version>
<vendor>example</vendor>
<types>
<typeDescription>
<name>com.example.uima.RoomNumber</name>
<description>A room number such as 9244-108</description>
<supertypeName>uima.tcas.Annotation</supertypeName>
<features>
<featureDescription>
<name>pattern</name>
<description>The raw matched pattern text</description>
<rangeTypeName>uima.cas.String</rangeTypeName>
</featureDescription>
</features>
</typeDescription>
</types>
</typeSystemDescription>
3.2 生成 JCas 绑定类
cd /opt/uima-demo
mvn -B dependency:resolve
M2=~/.m2/repository/org/apache/uima
CP=$M2/uimaj-tools/3.6.1/uimaj-tools-3.6.1.jar:$M2/uimaj-core/3.6.1/uimaj-core-3.6.1.jar:$HOME/.m2/repository/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar
java -cp $CP org.apache.uima.tools.jcasgen.Jg -jcasgeninput descriptors/TypeSystem.xml -jcasgenoutput src/main/java
生成的 RoomNumber.java 提供 getPattern/setPattern/getBegin/getEnd/getCoveredText 等强类型访问器。UIMA 3 的生成类不再包含 UIMA 2 时代的 _Type 伴生类。
四、Annotator 实现
4.1 抽取注解器 RoomNumberAnnotator
package com.example.uima;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.uima.UimaContext;
import org.apache.uima.analysis_component.JCasAnnotator_ImplBase;
import org.apache.uima.analysis_engine.AnalysisEngineProcessException;
import org.apache.uima.jcas.JCas;
import org.apache.uima.resource.ResourceInitializationException;
import org.apache.uima.util.Level;
public class RoomNumberAnnotator extends JCasAnnotator_ImplBase {
private Pattern pattern;
@Override
public void initialize(UimaContext context) throws ResourceInitializationException {
super.initialize(context);
String regex = (String) context.getConfigParameterValue("Pattern");
if (regex == null || regex.isEmpty()) {
regex = "(\\d{3,4})\\s*-\\s*\\d{1,4}";
}
pattern = Pattern.compile(regex);
context.getLogger().log(Level.INFO, "RoomNumberAnnotator initialized, regex={0}", regex);
}
@Override
public void process(JCas jcas) throws AnalysisEngineProcessException {
Matcher matcher = pattern.matcher(jcas.getDocumentText());
while (matcher.find()) {
RoomNumber rn = new RoomNumber(jcas, matcher.start(), matcher.end());
rn.setPattern(matcher.group());
rn.addToIndexes();
}
}
}
要点:继承 JCasAnnotator_ImplBase;initialize 读取配置参数;process 里对每个命中创建 RoomNumber 并 addToIndexes()。
4.2 统计注解器 RoomNumberCounter
package com.example.uima;
import org.apache.uima.analysis_component.JCasAnnotator_ImplBase;
import org.apache.uima.analysis_engine.AnalysisEngineProcessException;
import org.apache.uima.jcas.JCas;
public class RoomNumberCounter extends JCasAnnotator_ImplBase {
@Override
public void process(JCas jcas) throws AnalysisEngineProcessException {
int count = 0;
StringBuilder values = new StringBuilder();
for (RoomNumber rn : jcas.select(RoomNumber.class)) {
count++;
if (count > 1) {
values.append(", ");
}
values.append(rn.getPattern());
}
System.out.printf("[counter] document length=%d, RoomNumber found=%d, values=[%s]%n",
jcas.getDocumentText().length(), count, values);
}
}
五、描述符 XML
5.1 原语 AE:RoomNumberAnnotator.xml
<?xml version="1.0" encoding="UTF-8"?>
<analysisEngineDescription xmlns="http://uima.apache.org/resourceSpecifier">
<frameworkImplementation>org.apache.uima.java</frameworkImplementation>
<primitive>true</primitive>
<annotatorImplementationName>com.example.uima.RoomNumberAnnotator</annotatorImplementationName>
<analysisEngineMetaData>
<name>RoomNumberAnnotator</name>
<description>Finds room numbers such as 9244-108 with a configurable regex</description>
<version>1.0</version>
<vendor>example</vendor>
<configurationParameters searchStrategy="language_fallback">
<configurationParameter>
<name>Pattern</name>
<description>Regular expression for room numbers</description>
<type>String</type>
<multiValued>false</multiValued>
<mandatory>false</mandatory>
</configurationParameter>
</configurationParameters>
<configurationParameterSettings>
<nameValuePair>
<name>Pattern</name>
<value><string>(\d{3,4})\s*-\s*\d{1,4}</string></value>
</nameValuePair>
</configurationParameterSettings>
<typeSystemDescription>
<imports><import location="TypeSystem.xml"/></imports>
</typeSystemDescription>
<capabilities>
<capability>
<inputs/>
<outputs>
<type allAnnotatorFeatures="true">com.example.uima.RoomNumber</type>
</outputs>
<languagesSupported><language>en</language></languagesSupported>
</capability>
</capabilities>
<operationalProperties>
<modifiesCas>true</modifiesCas>
<multipleDeploymentAllowed>true</multipleDeploymentAllowed>
<outputsNewCASes>false</outputsNewCASes>
</operationalProperties>
</analysisEngineMetaData>
</analysisEngineDescription>
RoomNumberCounter.xml 结构相同(implementationName 换为 com.example.uima.RoomNumberCounter,capability 中 RoomNumber 声明在 inputs 而非 outputs)。
5.2 聚合 AE:AggregateAE.xml
<?xml version="1.0" encoding="UTF-8"?>
<analysisEngineDescription xmlns="http://uima.apache.org/resourceSpecifier">
<frameworkImplementation>org.apache.uima.java</frameworkImplementation>
<primitive>false</primitive>
<delegateAnalysisEngineSpecifiers>
<delegateAnalysisEngine key="RoomNumberAnnotator">
<import location="RoomNumberAnnotator.xml"/>
</delegateAnalysisEngine>
<delegateAnalysisEngine key="RoomNumberCounter">
<import location="RoomNumberCounter.xml"/>
</delegateAnalysisEngine>
</delegateAnalysisEngineSpecifiers>
<analysisEngineMetaData>
<name>RoomNumber Aggregate AE</name>
<description>Aggregate AE: RoomNumberAnnotator followed by RoomNumberCounter</description>
<version>1.0</version>
<vendor>example</vendor>
<configurationParameters searchStrategy="language_fallback"/>
<configurationParameterSettings/>
<capabilities>
<capability>
<inputs/>
<outputs>
<type allAnnotatorFeatures="true">com.example.uima.RoomNumber</type>
</outputs>
<languagesSupported><language>en</language></languagesSupported>
</capability>
</capabilities>
<operationalProperties>
<modifiesCas>true</modifiesCas>
<multipleDeploymentAllowed>true</multipleDeploymentAllowed>
<outputsNewCASes>false</outputsNewCASes>
</operationalProperties>
<flowConstraints>
<fixedFlow>
<node>RoomNumberAnnotator</node>
<node>RoomNumberCounter</node>
</fixedFlow>
</flowConstraints>
</analysisEngineMetaData>
</analysisEngineDescription>
三条实测得出的硬规则:flowConstraints 必须在 analysisEngineMetaData 内;<node> 用文本承载 key(不是 key 属性);聚合 AE 不得写 <typeSystemDescription>。
六、运行与回显
6.1 运行入口 RunPipeline
package com.example.uima;
import java.io.File;
import org.apache.uima.UIMAFramework;
import org.apache.uima.analysis_engine.AnalysisEngine;
import org.apache.uima.jcas.JCas;
import org.apache.uima.json.JsonCasSerializer;
import org.apache.uima.json.impl.JsonContentHandlerJacksonWrapper;
import org.apache.uima.resource.ResourceSpecifier;
import org.apache.uima.util.XMLInputSource;
public class RunPipeline {
public static void main(String[] args) throws Exception {
System.out.printf("[heap] effective max heap = %.0f MB%n",
Runtime.getRuntime().maxMemory() / 1048576.0);
String aePath = args.length > 0 ? args[0] : "descriptors/AggregateAE.xml";
XMLInputSource xmlInput = new XMLInputSource(new File(aePath));
ResourceSpecifier specifier = UIMAFramework.getXMLParser().parseResourceSpecifier(xmlInput);
AnalysisEngine ae = UIMAFramework.produceAnalysisEngine(specifier);
JCas jcas = ae.newJCas();
jcas.setDocumentText("The seminar is in room 9244-108 and the workshop is in 3116-2. "
+ "Overflow seating is available in 2244-3A. The lab moved to 5117-C.");
ae.process(jcas);
int count = 0;
for (RoomNumber rn : jcas.select(RoomNumber.class)) {
count++;
System.out.printf("[jcas] RoomNumber #%d begin=%d end=%d covered=\"%s\" pattern=\"%s\"%n",
count, rn.getBegin(), rn.getEnd(), rn.getCoveredText(), rn.getPattern());
}
System.out.println("[jcas] total RoomNumber = " + count);
StringWriter sw = new StringWriter();
new JsonCasSerializer().serialize(jcas.getCas(), new JsonContentHandlerJacksonWrapper(sw));
System.out.println("[json] " + sw);
ae.collectionProcessComplete();
ae.destroy();
System.out.println("[done] pipeline finished");
}
}
(StringWriter 需 import java.io.StringWriter;)
6.2 构建并运行
mvn -B package
mvn -B dependency:build-classpath -Dmdep.outputFile=target/cp.txt -q
java -Xms256m -Xmx512m -cp "target/classes:$(cat target/cp.txt)" com.example.uima.RunPipeline
实测回显([json] 行仅摘录首尾,中段以 ... 略):
[heap] effective max heap = 512 MB
Oct 12, 2026 2:46:35 PM com.example.uima.RoomNumberAnnotator initialize(29)
INFO: RoomNumberAnnotator initialized, regex=(\d{3,4})\s*-\s*\d{1,4}
[ae] produced: RoomNumber Aggregate AE (version 1.0)
[cas] document length = 129
[counter] document length=129, RoomNumber found=3, values=[9244-108, 3116-2, 2244-3]
[jcas] RoomNumber #1 begin=23 end=31 covered="9244-108" pattern="9244-108"
[jcas] RoomNumber #2 begin=55 end=61 covered="3116-2" pattern="3116-2"
[jcas] RoomNumber #3 begin=96 end=102 covered="2244-3" pattern="2244-3"
[jcas] total RoomNumber = 3
[json] {"_context":{"_types":{"DocumentAnnotation":{...},"RoomNumber":{"_id":"com.example.uima.RoomNumber",...},...}},
"_views":{"_InitialView":{"RoomNumber":[{"sofa":1,"begin":23,"end":31,"pattern":"9244-108"},
{"sofa":1,"begin":55,"end":61,"pattern":"3116-2"},{"sofa":1,"begin":96,"end":102,"pattern":"2244-3"}]}},...}
[done] pipeline finished
结果解读:级联顺序正确(先抽取后统计);2244-3A 按默认正则命中前缀 2244-3,5117-C 因 - 后无数字不命中——正则行为符合预期;CAS-JSON 由 uimaj-json 输出,含类型系统上下文与视图数据。
6.3 JCas 读回注解
读回即 6.1 中的 select 循环:
for (RoomNumber rn : jcas.select(RoomNumber.class)) {
rn.getBegin(); rn.getEnd(); rn.getCoveredText(); rn.getPattern();
}
jcas.select(Class) 返回 SelectFSs 流式集合(UIMA 3 内置于 uimaj-core,无需 uimaFIT),支持 .count()、.at(typePriority) 等链式操作。
6.4 配置参数运行期覆盖(实测)
ae.setConfigParameterValue("Pattern", "2244-3A");
ae.reconfigure();
实测输出(只命中字面量):
[ae] parameter override applied: Pattern="2244-3A"
[jcas] RoomNumber #1 begin=96 end=103 covered="2244-3A" pattern="2244-3A"
[jcas] total RoomNumber = 1
注意:produceAnalysisEngine(specifier, params) 的 Map 实测不会注入配置参数;聚合 AE 上直接 setConfigParameterValue 实测也不会下发到委托组件,覆盖以委托描述符或聚合声明同名参数为准。
七、扩展:多 Annotator 级联与 CPE 集成
7.1 文本目录读取器 TextCollectionReader
package com.example.uima;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.uima.cas.CAS;
import org.apache.uima.collection.CollectionException;
import org.apache.uima.collection.CollectionReader_ImplBase;
import org.apache.uima.resource.ResourceInitializationException;
import org.apache.uima.util.Progress;
import org.apache.uima.util.ProgressImpl;
public class TextCollectionReader extends CollectionReader_ImplBase {
public static final String PARAM_INPUT_DIRECTORY = "InputDirectory";
private File[] files;
private int index;
@Override
public void initialize() throws ResourceInitializationException {
String dirName = (String) getConfigParameterValue(PARAM_INPUT_DIRECTORY);
if (dirName == null) {
throw new ResourceInitializationException(
ResourceInitializationException.CONFIG_SETTING_ABSENT,
new Object[] { PARAM_INPUT_DIRECTORY });
}
File dir = new File(dirName);
File[] all = dir.listFiles();
List<File> txtFiles = new ArrayList<>();
if (all != null) {
Arrays.sort(all);
for (File f : all) {
if (f.isFile() && f.getName().endsWith(".txt")) {
txtFiles.add(f);
}
}
}
files = txtFiles.toArray(new File[0]);
index = 0;
}
@Override
public void getNext(CAS cas) throws IOException, CollectionException {
File file = files[index++];
cas.setDocumentText(new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8));
cas.setDocumentLanguage("en");
}
@Override
public boolean hasNext() throws IOException, CollectionException {
return index < files.length;
}
@Override
public Progress[] getProgress() {
return new Progress[] { new ProgressImpl(index, files.length, Progress.ENTITIES) };
}
@Override
public void close() throws IOException {
}
}
注意 getNext 的参数类型是 CAS(实测按 JCas 覆写会编译失败)。配套描述符 TextCollectionReader.xml 与 5.1 节同构,根元素为 collectionReaderDescription,声明 mandatory 的 InputDirectory 参数并给默认值 data/in。
7.2 CPE 运行入口 RunCPE
package com.example.uima;
import java.io.File;
import java.io.FileOutputStream;
import java.util.Collections;
import org.apache.uima.UIMAFramework;
import org.apache.uima.cas.CAS;
import org.apache.uima.collection.CollectionProcessingEngine;
import org.apache.uima.collection.EntityProcessStatus;
import org.apache.uima.collection.StatusCallbackListener;
import org.apache.uima.collection.impl.metadata.cpe.CpeDescriptorFactory;
import org.apache.uima.collection.metadata.CpeCasProcessor;
import org.apache.uima.collection.metadata.CpeDescription;
public class RunCPE {
public static void main(String[] args) throws Exception {
CpeDescription cpeDesc = CpeDescriptorFactory.produceDescriptor();
cpeDesc.addCollectionReader("descriptors/TextCollectionReader.xml");
CpeCasProcessor annotator = CpeDescriptorFactory.produceCasProcessor("RoomNumberAnnotator");
annotator.setDescriptor("descriptors/RoomNumberAnnotator.xml");
cpeDesc.addCasProcessor(annotator);
cpeDesc.setNumToProcess(-1);
File cpeFile = new File("descriptors/RoomNumberCPE.xml");
try (FileOutputStream out = new FileOutputStream(cpeFile)) {
cpeDesc.toXML(out);
}
System.out.println("[cpe] generated CPE descriptor: " + cpeFile.getAbsolutePath());
CollectionProcessingEngine engine =
UIMAFramework.produceCollectionProcessingEngine(cpeDesc, Collections.emptyMap());
engine.addStatusCallbackListener(new StatusCallbackListener() {
@Override
public void entityProcessComplete(CAS cas, EntityProcessStatus status) {
int errors = status.isException() ? status.getExceptions().size() : 0;
System.out.printf("[cpe] document processed, length=%d, errors=%d%n",
cas.getDocumentText().length(), errors);
}
@Override
public void collectionProcessComplete() {
System.out.println("[cpe] collectionProcessComplete - finished");
System.exit(0);
}
@Override
public void initializationComplete() {
System.out.println("[cpe] initializationComplete - CPE is running");
}
@Override
public void batchProcessComplete() {
}
@Override
public void paused() {
}
@Override
public void resumed() {
}
@Override
public void aborted() {
System.exit(1);
}
});
engine.process();
}
}
运行:
java -Xms256m -Xmx512m -cp "target/classes:$(cat target/cp.txt)" com.example.uima.RunCPE
实测回显:
[cpe] generated CPE descriptor: /opt/uima-demo/descriptors/RoomNumberCPE.xml
INFO: RoomNumberAnnotator initialized, regex=(\d{3,4})\s*-\s*\d{1,4}
[reader] input directory=/opt/uima-demo/data/in, files found=1
[cpe] initializationComplete - CPE is running
[cpe] document processed, length=130, errors=0
[cpe] collectionProcessComplete - finished
代码同时把规范化的 CPE 描述符落盘为 descriptors/RoomNumberCPE.xml(含 casProcessors 的 errorHandling/checkpoint 与 cpeConfig 的 numToProcess/deployAs 等),后续可手工编辑后用 UIMAFramework.getXMLParser().parseCpeDescription(new XMLInputSource(...)) 加载复用。
八、JVM 堆限定与验证
启动脚本统一限定堆 256-512 MB:
java -Xms256m -Xmx512m -cp "target/classes:$(cat target/cp.txt)" com.example.uima.RunPipeline
三层实测验证:
- 程序内打印(RunPipeline 第一行输出):
[heap] effective max heap = 512 MB
- 运行期
jps -v(实测回显):
7236 RunAE -Xms256m -Xmx512m
- Maven 构建阶段的堆由
MAVEN_OPTS控制(写入 /etc/profile.d/uima-build.sh),构建期间jps -v实测:
4887 Launcher -Xms256m -Xmx512m -Dclassworlds.conf=/usr/share/maven/bin/m2.conf ...
本工程实测峰值远低于 512 MB,256-512 MB 区间可稳定完成构建与运行。
九、常见问题
Q1:编译报 cannot find symbol: class RoomNumber?
实测原因:跳过了 JCasGen,工程里没有生成的 JCas 类。解决:先执行 3.2 节的 Jg 命令生成 RoomNumber.java,再 mvn -B package。
Q2:初始化报 No configuration parameter with name "Pattern" is declared?
实测原因:描述符里 configurationParameter 用属性(name=、type=)声明被解析器忽略。解决:改为 <name>/<type>/<multiValued>/<mandatory> 子元素写法。
Q3:聚合 AE 报 The key "" was used in the flow 或 unknown element type: flowConstraints?
实测两条对应修法:flowConstraints 移入 analysisEngineMetaData 内部;fixedFlow 节点用 <node>RoomNumberAnnotator</node> 文本形式而非 key 属性。
Q4:produceAnalysisEngine 传 Map 覆盖参数不生效?
实测确认该 Map 不用于配置参数注入。解决:produce 之后再 ae.setConfigParameterValue("Pattern", ...) 加 ae.reconfigure()。
Q5:日志抛 unknown format type: 4?
实测原因:UIMA Logger 把消息串当 MessageFormat 模板,正则中的 {3,4} 触发解析错误。解决:用 log(Level.INFO, "...{0}", regex) 参数化写法。
Q6:getNext 覆写报"method does not override"?
实测确认 CollectionReader 接口签名是 getNext(CAS cas),不是 getNext(JCas)。解决:参数改为 CAS,需要 JCas 视图时再 cas.getJCas()。
Q7:CPE 回调编译报 does not override abstract method aborted()?
实测确认 StatusCallbackListener 除 entityProcessComplete 外还必须实现 initializationComplete/batchProcessComplete/collectionProcessComplete/paused/resumed/aborted。补齐全部七个方法即可。
十、参考链接
- Apache UIMA 官方网站:https://uima.apache.org/
- UIMA 概览与文档索引:https://uima.apache.org/doc-uima-overview.html
- uimaj-core Maven Central 页面:https://central.sonatype.com/artifact/org.apache.uima/uimaj-core
- GitHub 仓库 apache/uima-uimaj:https://github.com/apache/uima-uimaj
- UIMA Javadoc(uimaj-current):https://uima.apache.org/d/uimaj-current/api/
- uimaFIT(上层便捷 API,可选):https://uima.apache.org/uimafit.html