1. 概述
你有没有遇到过这种情况:想给某个 Java 或 Groovy 类加几个实用方法,但又没法修改它的源码?这时候,Groovy 的 Category 机制就能派上大用场了。
Groovy 作为 JVM 上一门动态性极强的语言,提供了丰富的元编程(Metaprogramming)能力。而 Category 就是其中一种简单粗暴但非常实用的技术,它允许我们在不改动原始类的前提下,临时“注入”新方法。
本文将深入讲解 Groovy 中的 Category 机制,包括内置常用 Category 的使用,以及如何自定义自己的 Category。
2. 什么是 Category?
Category 是 Groovy 提供的一种元编程特性,灵感来自 Objective-C。它的核心作用是:为现有类(包括 JDK 类或第三方库类)动态添加新方法。
⚠️ 但要注意,和 Groovy 的 Extension Methods 不同,Category 的增强功能不会全局生效。它的作用范围被严格限制在 use
代码块内。
✅ 简单说:只有在 use(SomeCategory)
块内部,你才能调用该 Category 提供的扩展方法。出了这个块,一切回归原样。
这种设计既灵活又安全,避免了污染全局命名空间的“踩坑”风险。
3. Groovy 内置的常用 Category
Groovy SDK 已经内置了一些非常实用的 Category,下面我们来看两个典型的例子。
3.1. TimeCategory:让时间操作更自然
TimeCategory
位于 groovy.time
包中,它极大地简化了 Date
和时间相关的操作。
它主要提供了两大能力:
- ✅ 将整数转换为时间单位,如
.seconds
、.minutes
、.days
、.months
- ✅ 为
Date
对象提供+
和-
操作符,支持直接加减时间间隔(Duration)
来看个例子:
def jan_1_2019 = new Date("01/01/2019")
use (TimeCategory) {
assert jan_1_2019 + 10.seconds == new Date("01/01/2019 00:00:10")
assert jan_1_2019 + 20.minutes == new Date("01/01/2019 00:20:00")
assert jan_1_2019 - 1.day == new Date("12/31/2018")
assert jan_1_2019 - 2.months == new Date("11/01/2018")
}
上面代码中:
10.seconds
返回一个TimeDuration
对象+
操作符会调用Date.plus(TimeDuration)
方法完成时间累加1.day
返回一个Duration
对象,用于日期减法
此外,TimeCategory
还支持更语义化的相对时间表达:
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy")
use (TimeCategory) {
assert sdf.format(5.days.from.now) == sdf.format(new Date() + 5.days)
sdf = new SimpleDateFormat("dd/MM/yyyy hh:mm:ss")
assert sdf.format(10.minutes.from.now) == sdf.format(new Date() + 10.minutes)
assert sdf.format(2.hours.ago) == sdf.format(new Date() - 2.hours)
}
像 5.days.from.now
这种写法,读起来就像自然语言,代码可读性直接拉满。
3.2. DOMCategory:简化 XML DOM 操作
DOMCategory
在 groovy.xml.dom
包中,它为 Java 的 DOM 节点提供了 GPath 支持,让 XML 遍历和修改变得更简单。
先准备一段 XML 并解析:
def baeldungArticlesText = """
<articles>
<article core-java="true">
<title>An Intro to the Java Debug Interface (JDI)</title>
<desc>A quick and practical overview of Java Debug Interface.</desc>
</article>
<article core-java="false">
<title>A Quick Guide to Working with Web Services in Groovy</title>
<desc>Learn how to work with Web Services in Groovy.</desc>
</article>
</articles>
"""
def baeldungArticlesDom = DOMBuilder.newInstance().parseText(baeldungArticlesText)
def root = baeldungArticlesDom.documentElement
使用 DOMCategory
遍历节点:
use (DOMCategory) {
assert root.article.size() == 2
def articles = root.article
assert articles[0].title.text() == "An Intro to the Java Debug Interface (JDI)"
assert articles[1].desc.text() == "Learn how to work with Web Services in Groovy."
}
这里 root.article
使用了 GPath 语法,可以直接通过点号访问子节点,非常直观。
还可以动态添加新节点:
use (DOMCategory) {
def articleNode3 = root.appendNode(new QName("article"), ["core-java": "false"])
articleNode3.appendNode("title", "Metaprogramming in Groovy")
articleNode3.appendNode("desc", "Explore the concept of metaprogramming in Groovy")
assert root.article.size() == 3
assert root.article[2].title.text() == "Metaprogramming in Groovy"
}
appendNode
和 text()
等方法让 DOM 操作变得像操作 Groovy 对象一样自然。
4. 自定义 Category
光用内置的还不够?完全可以自己写一个 Category 来扩展任意类。
4.1. 基于静态方法的传统方式
要创建一个 Category 类,必须遵守两个约定:
- ✅ 方法必须是
static
- ✅ 第一个参数必须是目标类的实例(即
self
)
比如,我们给 String
添加一个 capitalize
方法:
class BaeldungCategory {
public static String capitalize(String self) {
String capitalizedStr = self;
if (self.size() > 0) {
capitalizedStr = self.substring(0, 1).toUpperCase() + self.substring(1);
}
return capitalizedStr
}
}
使用时用 use
激活:
use (BaeldungCategory) {
assert "norman".capitalize() == "Norman"
}
再加个数学功能:计算幂:
public static double toThePower(Number self, Number exponent) {
return Math.pow(self, exponent);
}
测试:
use (BaeldungCategory) {
assert 50.toThePower(2) == 2500
assert 2.4.toThePower(4) == 33.1776
}
4.2. 使用 @Category 注解(推荐)
Groovy 还提供了 @groovy.lang.Category
注解,可以让 Category 写法更像实例方法,代码更清爽。
使用注解时需要指定目标类,方法体内用 this
指代当前实例,无需显式传 self
。
示例:为 Number
添加立方和向上取整除法:
@Category(Number)
class NumberCategory {
public Number cube() {
return this * this * this
}
public int divideWithRoundUp(BigDecimal divisor, boolean isRoundUp) {
def mathRound = isRoundUp ? BigDecimal.ROUND_UP : BigDecimal.ROUND_DOWN
return (int)new BigDecimal(this).divide(divisor, 0, mathRound)
}
}
divideWithRoundUp
方法支持按需向上或向下取整。
测试:
use (NumberCategory) {
assert 3.cube() == 27
assert 25.divideWithRoundUp(6, true) == 5
assert 120.23.divideWithRoundUp(6.1, true) == 20
assert 150.9.divideWithRoundUp(12.1, false) == 12
}
这种方式语法更简洁,推荐在新项目中使用。
5. 总结
Category 是 Groovy 元编程中一个轻量但极其实用的特性,主要优势在于:
- ✅ 可为任意类(包括 JDK 类)动态添加方法
- ✅ 作用域受限于
use
块,安全可控 - ✅ 内置
TimeCategory
、DOMCategory
等大幅提升开发效率 - ✅ 支持两种写法:传统静态方法 +
self
参数,或使用@Category
注解
在处理日期、XML、或需要临时增强第三方类时,Category 能让你写出更简洁、更具表达力的代码。
示例代码已上传至 GitHub:https://github.com/baeldung/tutorials/tree/master/core-groovy-modules/core-groovy-2