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 操作

DOMCategorygroovy.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"
}

appendNodetext() 等方法让 DOM 操作变得像操作 Groovy 对象一样自然。


4. 自定义 Category

光用内置的还不够?完全可以自己写一个 Category 来扩展任意类。

4.1. 基于静态方法的传统方式

要创建一个 Category 类,必须遵守两个约定:

  1. ✅ 方法必须是 static
  2. ✅ 第一个参数必须是目标类的实例(即 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 块,安全可控
  • ✅ 内置 TimeCategoryDOMCategory 等大幅提升开发效率
  • ✅ 支持两种写法:传统静态方法 + self 参数,或使用 @Category 注解

在处理日期、XML、或需要临时增强第三方类时,Category 能让你写出更简洁、更具表达力的代码。

示例代码已上传至 GitHub:https://github.com/baeldung/tutorials/tree/master/core-groovy-modules/core-groovy-2


原始标题:Categories in Groovy