1. 概述

Groovy 作为 JVM 上的动态语言,凭借其简洁语法和强大元编程能力,在 Spring 生态中有着不错的集成表现。尤其适合快速构建原型或简化样板代码。

本文将带你用 Spring Boot + Groovy 实现一个简易的待办事项(Todo)应用,涵盖 REST 接口开发、JPA 数据持久化,并重点展示 Groovy 如何让 Java 开发变得更“丝滑”。


2. 待办事项应用设计

目标是实现一个基于 REST 的 CRUD 应用,功能包括:

✅ 创建任务
✅ 编辑任务
✅ 删除任务
✅ 查看单个任务
✅ 查看所有任务

构建工具选用 Maven,数据层使用 H2 内存数据库,便于快速启动和测试。

2.1. Maven 依赖配置

核心依赖如下,注意 Groovy 和 GMavenPlus 插件的引入:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>3.1.5</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.1.5</version>
</dependency>
<dependency>
    <groupId>org.apache.groovy</groupId>
    <artifactId>groovy</artifactId>
    <version>4.0.21</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>3.1.5</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.1.214</version>
    <scope>runtime</scope>
</dependency>

⚠️ 关键点:spring-boot-starter-web 用于构建 REST 接口,groovy 提供语言支持,spring-boot-starter-data-jpa + h2 构成轻量级持久化方案。

此外,必须添加 gmavenplus-plugin 插件,用于编译 Groovy 源码:

<build>
    <plugins>
        <plugin>
            <groupId>org.codehaus.gmavenplus</groupId>
            <artifactId>gmavenplus-plugin</artifactId>
            <version>3.0.2</version>
            <executions>
                <execution>
                    <goals>
                        <goal>addSources</goal>
                        <goal>addTestSources</goal>
                        <goal>generateStubs</goal>
                        <goal>compile</goal>
                        <goal>generateTestStubs</goal>
                        <goal>compileTests</goal>
                        <goal>removeStubs</goal>
                        <goal>removeTestStubs</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

这个插件负责生成存根(stubs)、编译 Groovy 类,并确保与 Java 代码的互操作性,漏配会导致编译失败

2.2. JPA 实体类(Groovy 版)

使用 Groovy 定义 Todo 实体,代码非常简洁:

@Entity
@Table(name = 'todo')
class Todo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Integer id
    
    @Column
    String task
    
    @Column
    Boolean isCompleted
}

✅ 踩坑提示:Groovy 默认将无访问修饰符的字段设为 private,并自动生成 getter/setter,无需 Lombok。这在 Spring Data JPA 中完全兼容。

2.3. 数据访问层

定义一个继承 JpaRepository 的 Groovy 接口,Spring Data 会自动提供实现:

@Repository
interface TodoRepository extends JpaRepository<Todo, Integer> {}

简单粗暴,零代码实现 CRUD。

2.4. 服务层

先定义接口:

interface TodoService {

    List<Todo> findAll()

    Todo findById(Integer todoId)

    Todo saveTodo(Todo todo)

    Todo updateTodo(Todo todo)

    Todo deleteTodo(Integer todoId)
}

再提供 Groovy 实现类:

@Service
class TodoServiceImpl implements TodoService {

    @Autowired
    TodoRepository todoRepository

    @Override
    List<Todo> findAll() {
        todoRepository.findAll()
    }

    @Override
    Todo findById(Integer todoId) {
        todoRepository.findById(todoId).get()
    }
    
    @Override
    Todo saveTodo(Todo todo){
        todoRepository.save(todo)
    }
    
    @Override
    Todo updateTodo(Todo todo){
        todoRepository.save(todo)
    }
    
    @Override
    Todo deleteTodo(Integer todoId){
        todoRepository.deleteById(todoId)
    }
}

注意:Groovy 中方法调用可省略括号和点号(dot),如 save todo,但为了可读性,建议保留括号,尤其是参数较多时。

2.5. 控制器层

使用 @RestController 暴露 REST 接口:

@RestController
@RequestMapping('/todo')
class TodoController {

    @Autowired
    TodoService todoService

    @GetMapping
    List<Todo> getAllTodoList() {
        todoService.findAll()
    }

    @PostMapping
    Todo saveTodo(@RequestBody Todo todo) {
        todoService.saveTodo(todo)
    }

    @PutMapping
    Todo updateTodo(@RequestBody Todo todo) {
        todoService.updateTodo(todo)
    }

    @DeleteMapping('/{todoId}')
    void deleteTodo(@PathVariable Integer todoId) {
        todoService.deleteTodo(todoId)
    }

    @GetMapping('/{todoId}')
    Todo getTodoById(@PathVariable Integer todoId) {
        todoService.findById(todoId)
    }
}

每个接口对应一个 CRUD 操作,结构清晰。

2.6. 启动类

Spring Boot 启动类也用 Groovy 编写:

@SpringBootApplication
class SpringBootGroovyApplication {
    static void main(String[] args) {
        SpringApplication.run(SpringBootGroovyApplication, args)
    }
}

✅ Groovy 小技巧:

  • 方法调用可省略括号:run SpringBootGroovyApplication, args
  • 类名无需 .class 后缀

别忘了在 pom.xml 中指定启动类:

<properties>
    <start-class>com.example.demo.SpringBootGroovyApplication</start-class>
</properties>

3. 运行应用

执行以下任一命令即可启动:

mvn spring-boot:run

应用默认启动在 http://localhost:8080,可通过 Postman 或 curl 测试接口。


4. 应用测试

使用 RestAssured 编写集成测试,验证各接口行为。

4.1. 测试准备

定义测试常量和初始化数据:

class TodoAppTest {

    static final String API_ROOT = "http://localhost:8080/todo"
    static Integer readingTodoId
    static Integer writingTodoId

    @BeforeClass
    static void populateDummyData() {
        Todo readingTodo = new Todo(task: 'Reading', isCompleted: false)
        Todo writingTodo = new Todo(task: 'Writing', isCompleted: false)

        def readingResponse = RestAssured.given()
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .body(readingTodo)
            .post(API_ROOT)
        
        readingTodoId = readingResponse.as(Todo.class).id

        def writingResponse = RestAssured.given()
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .body(writingTodo)
            .post(API_ROOT)
        
        writingTodoId = writingResponse.as(Todo.class).id
    }
}

✅ Groovy 特性:

  • 使用 命名参数 初始化对象:new Todo(task: 'Reading', isCompleted: false)
  • 字符串插值:"$API_ROOT/$readingTodoId",比 Java 的 String.format 更直观

4.2. CRUD 接口测试

查询所有任务

@Test
void whenGetAllTodoList_thenOk() {
    def response = RestAssured.get(API_ROOT)
    
    assertEquals(HttpStatus.OK.value(), response.getStatusCode())
    assertTrue(response.jsonPath().getList(".").size() > 0)
}

查询单个任务

@Test
void whenGetTodoById_thenOk() {
    def response = RestAssured.get("$API_ROOT/$readingTodoId")
    
    assertEquals(HttpStatus.OK.value(), response.getStatusCode())
    Todo todo = response.as(Todo.class)
    assertEquals(readingTodoId, todo.id)
}

更新任务

@Test
void whenUpdateTodoById_thenOk() {
    def todo = new Todo(id: readingTodoId, isCompleted: true)
    def response = RestAssured.given()
        .contentType(MediaType.APPLICATION_JSON_VALUE)
        .body(todo)
        .put(API_ROOT)
    
    assertEquals(HttpStatus.OK.value(), response.getStatusCode())
    Todo updated = response.as(Todo.class)
    assertTrue(updated.isCompleted)
}

删除任务

@Test
void whenDeleteTodoById_thenOk() {
    def response = RestAssured.given()
        .delete("$API_ROOT/$writingTodoId")
    
    assertEquals(HttpStatus.OK.value(), response.getStatusCode())
}

创建新任务

@Test
void whenSaveTodo_thenOk() {
    def todo = new Todo(task: 'Blogging', isCompleted: false)
    def response = RestAssured.given()
        .contentType(MediaType.APPLICATION_JSON_VALUE)
        .body(todo)
        .post(API_ROOT)
    
    assertEquals(HttpStatus.OK.value(), response.getStatusCode())
}

测试覆盖完整,确保各接口行为符合预期。


5. 总结

通过这个小例子,我们展示了:

✅ Spring Boot 与 Groovy 的无缝集成
✅ Groovy 如何通过省略语法噪音(如分号、getter/setter)提升开发效率
✅ 使用命名参数、字符串插值等特性让代码更简洁
✅ GMavenPlus 插件在 Maven 项目中的关键作用

虽然 Groovy 在现代 Java 项目中使用频率不如 Kotlin,但在脚本化、DSL、测试等领域仍有其独特优势。对于已有 Spring Boot 项目,尝试用 Groovy 重写部分模块,可能会有意想不到的“丝滑”体验。

完整代码示例可参考 GitHub 仓库:https://github.com/example/spring-boot-groovy-demo


原始标题:Building a Simple Web Application with Spring Boot and Groovy