java+spark 应用构建(1)

java+spark 应用构建(1)

前言

需要做一个小型的后端
但是现在看到 springboot 有点犯恶心, 于是想着有没有什么东西能够替换掉 spring

软件需求:
简单的 CRUD + 用户登录

技术栈需求:
打包体量小 + 性能要求低

由以上两点分析出了基本的技术路线

  1. 简单的 CRUD
    sqlite3 + sparkframework
  2. 用户登录需求
    jsonwebtoken + jbcrypt
  3. 前端交互 + 软件测试需求
    swagger + junit + mockito
  4. 简易部署 + 方便迁移的开发环境
    Gradle + shadowjar

下面我将介绍具体的工程目录以及构建方法

主要内容

gradle 简介及初始化

为何使用 gradle

  1. 部署方便,不需要再安装一个 maven
  2. 在使用难度上比 maven 要低一些,使用 maven 需要清楚的分别软件的生命周期,但是 gradle 不需要

如何安装并初始化一个 gradle 工程

  1. gralde 官网 下载一个二进制包
  2. 安装 java (默认已经安装)
  3. 输入以下命令,完成 gradle 安装
❯ mkdir /opt/gradle
❯ unzip -d /opt/gradle gradle-0.0.0-bin.zip // 可能不是这个版本号,需要注意
❯ ls /opt/gradle/gradle-0.0.0
LICENSE  NOTICE  bin  README  init.d  lib  media
❯ echo `export PATH=$PATH:/opt/gradle/gradle-9.3.1/bin`  >> ~/.zshrc
❯ source ~/.zshrc
❯ gradle -v

------------------------------------------------------------
Gradle 9.0.0-rc-4
------------------------------------------------------------

Build time:    2025-07-28 15:10:02 UTC
Revision:      a0a6deb6f9937f9e9894a4460f4158267a13ad03

Kotlin:        2.2.0
Groovy:        4.0.27
Ant:           Apache Ant(TM) version 1.10.15 compiled on August 25 2024
Launcher JVM:  21.0.6 (Eclipse Adoptium 21.0.6+7-LTS)
Daemon JVM:    /${mypath}/jdk-21.0.6+7 (no JDK specified, using current Java home)
OS:            Linux 6.18.7-arch1-1 amd64

  1. 在需要的目录中初始化项目
❯ mkdir ${project name}
❯ cd ${the path where the project located}
❯ gradle init --type java-application

Enter target Java version (min: 7, default: 21): 21

Project name (default: example):

Select application structure:
  1: Single application project
  2: Application and library project
Enter selection (default: Single application project) [1..2] 1

Select build script DSL: 2
  1: Kotlin
  2: Groovy
Enter selection (default: Kotlin) [1..2] 2

Select test framework: 1
  1: JUnit 4
  2: TestNG
  3: Spock
  4: JUnit Jupiter
Enter selection (default: JUnit Jupiter) [1..4] 1

Generate build using new APIs and behavior (some features may change in the next minor release)? (default: no) [yes, no] no


> Task :init
Learn more about Gradle by exploring our Samples at https://docs.gradle.org/9.0.0-rc-4/samples/sample_building_java_applications.html

BUILD SUCCESSFUL in 13s
1 actionable task: 1 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0-rc-4/userguide/configuration_cache_enabling.html

❯ tree -L2 ./
./
├── app
│   ├── build.gradle
│   └── src
├── gradle
│   ├── libs.versions.toml
│   └── wrapper
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle

5 directories, 6 files

至此,已经完成了 gradle 的安装与项目的新建

gradle 简易用法

接下来简单列举一些 gradle 会用到的命令

❯ gradle clean // 清理 build 文件夹
❯ gradle build // 执行编译命令
❯ gradle run // 运行工程
❯ gradle clean build run // 清除编译缓存,重新编译并运行
❯ gradle assemble // 编译项目但不跑 test
❯ gradle test // 跑项目的测试
❯ gradle tasks // 列举 gradle 的任务

ℹ️: gradle 使用注意事项

我们在本机安装了 gradle , 主要是用于初始化应用。实际上我们编译的时候都会使用工程内的 ./gradlew
Windows 使用 ./gradlew.bat

工程组织方法

在面向简单的 CRUD 项目来说,一般会采用单体或者分层架构进行工程组织


单体架构是指所有的功能模块集中在一个或者几个文件之中
在具体的操作层面指一个文件包含单个或几个功能模块

// UserController.java
@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    private UserService userService; // 直接注入UserService

    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.getUserById(id);
    }
}
// UserService.java
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository; // 直接注入UserRepository

    public User getUserById(Long id) {
        // 业务逻辑
        return userRepository.findById(id).orElse(null);
    }
}

// UserRepository.java
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // 数据访问方法
}

// User.java
@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    // ... getters and setters
}

如上文代码所示,一个文件写入一个功能模块,并没有进行分包

这样子减轻了代码的工程量


分层结构是指依据一定的原则对工程进行分层
基本的分层方式通常为 表示层 / 业务逻辑层 / 数据访问层

根据上述三层,还可以做出如下改变

  1. 在表示层和业务逻辑层中加入应用层, 该层主要用于在多个业务逻辑层之上进行进一步封装
    从而处理更复杂的业务逻辑
  2. 在数据访问层之下加入基础设施层,该层主要用于处理消息队列,日志等的技术细节

根据上文的需求,本文使用三层分层结构构建本项目

工程具体各层级作用

在实际的分层(三层)架构中,通常有 MVC , MVVM , MVP 等几种细分方法

但是我个人觉得没必要拘泥于怎么划分,就从最基本的工程实践进行处理即可

❯ tree -L4 ./
./
├── java
│   └── org
│       └── example
│           ├── App.java
│           ├── config // 对工程进行设置,比如 mybatis 以及 log 的设置
│           ├── controllers // 控制层,用于接受前端用户的操作
│           ├── filters // 过滤层,过滤登录用户权限用
│           ├── mappers // mybatis 的具体实践, 用于注册具体的 CRUD 操作
│           ├── models // 实体层
│           ├── services // 业务层
│           └── utils // 定义通用功能,比如 swagger , 数据库链接方式等
└ ...

其中各层级的功能如图所示

一般的各个层级的模板语法

本章节主要介绍各个层的具体工作,所需要的依赖并给出简易的示例代码

model 层

model 层主要用于定义与数据库链接的实体,通常来讲一张表对应一个实体

⚠️注意事项:
如果需要联合实体 (如数据表里可能有 join 表之类的存在), 则可以加入一个 DTO 层

在本工程中, 定义 model 层需要使用到 lombok 以及 mybatis-plus 库

  1. lombok 用于减少代码量,让我们只需要定义实体,而不需要写 getter / setter / 构造器 / tostring 等的方法
  2. mybatis-plus 使得 model 类的成员能够与具体的数据库表及属性名映射起来
package org.example.models;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor // 生成一个全参数构造器
@NoArgsConstructor // 生成一个无参数构造器
@TableName("work_info") // 将该模型类与 'work_info' 表进行对应
public class WorkInfo {

  @TableId(value = "id", type = IdType.AUTO) // 使用数据库的 autoincrement 作为 primary key
  private Integer id;

  @TableField("work_content") // 将 WorkInfo 类的 `worktontent` 成员与 `work_info` 表中的 `work_content` 列相对应
  private String workContent;
}
mapper 层

mapper 层主要用于定义 mybatis-plus 的 CRUD 方法

其主要原理是继承 mybatis-plus 的 basemapper 类, 该类封装了通用的数据库操作

而如果要定义一些更复杂的数据库操作,
在 mybatis-plus 中, 可以有以下途径

  1. 使用注解 (本文使用方法)
  2. 使用 XML 映射文件 (mybatis-plus-generate 插件)
  3. 使用 mybatis-plus 的条件构造器
package org.example.mappers;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select; // ibatis 的动态 sql 语句,用于取出数据库记录
import org.apache.ibatis.annotations.Update;
import org.example.models.User;

public interface UserMapper extends BaseMapper<User> {

  // 自定义方法, 执行 select 方法
  @Select("SELECT * FROM user WHERE username = #{username}")
  User getUserByUsername(String username);

  @Update(
      "UPDATE USER SET password = #{password} ,created_at= #{createdAt}"
          + " ,updated_at=#{updatedAt}WHERE id = #{id}")
  boolean updatePassword(Integer id, String password, String createdAt, String updatedAt);
}
services 层

服务层用于定义具体的业务逻辑

在本文中定义了一个接口再定义一个服务实现层

服务层的职能是链接数据库并处理业务逻辑
所以在服务层中需要 ibatis / mybatis

package org.example.services;

import org.example.models.User;

public interface UserService {

  // 定义接口,由 impl 实现
  User getUserByUsername(String username);

  boolean verifyPassword(String rawPassword, String hashedPassword);

}
package org.example.services.impl;

import org.apache.ibatis.session.SqlSession;
import org.example.config.MyBatisPlusConfig;
import org.example.mappers.UserMapper;
import org.example.models.User;
import org.example.services.UserService;
import org.mindrot.jbcrypt.BCrypt;

public class UserServiceImpl implements UserService {

  private static UserServiceImpl instance;

  private UserServiceImpl() {}

  // 单例模式,如果在 spring 中就不需要处理服务层的单例,但本文使用的是 spark
  // 所以需要新建单例为 controller 提供
  public static synchronized UserServiceImpl getInstance() {
    if (instance == null) {
      instance = new UserServiceImpl();
    }
    return instance;
  }

  // 覆写 mybatis 的方法,在 spark 中, 与写删有关的方法都无法使用
  // 所以写删方法需要自己重新写
  @Override
  public User getUserByUsername(String username) {
    // 获取数据库会话,从而对数据库进行操作
    try (SqlSession sqlSession = MyBatisPlusConfig.getSqlSessionFactory().openSession(false)) {
      UserMapper mapper = sqlSession.getMapper(UserMapper.class);
      return mapper.getUserByUsername(username);
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }

  @Override
  public boolean verifyPassword(String rawPassword, String hashedPassword) {
    if (rawPassword == null || hashedPassword == null) {
      return false;
    }
    try {
      return BCrypt.checkpw(rawPassword, hashedPassword);
    } catch (Exception e) {
      return false;
    }
  }
}
controllers 层

controller 层, 主要处理从用户的操作逻辑

在 spark 框架中需要定义相关的路由以及动作

package org.example.controllers;

import java.util.List;

import org.example.models.MainWork;
import org.example.services.MainWorkService;
import org.example.services.impl.MainWorkServiceImpl;

// 数据 json 化
import com.google.gson.Gson;

// 引入 spark 依赖
import static spark.Spark.delete;
import static spark.Spark.get;
import static spark.Spark.post;

public class MainWorkController {
  private MainWorkService mainWorkService;

  private static final Gson gson = new Gson();

  public MainWorkController() {
    this.mainWorkService = MainWorkServiceImpl.getInstance();
    setupRoutes();
  }

  // 配置 spark 路由
  private void setupRoutes() {
    // 在 setupRoutes 这个方法中配置路由映射
    get(
        "/mainwork/:id",
        (req, res) -> {
          int id = Integer.parseInt(req.params(":id"));
          MainWork mainWork = mainWorkService.getById(id);
          if (mainWork != null) {
            res.type("application/json");
            return gson.toJson(mainWork);
          } else {
            res.status(404);
            return "DailyInfo not found";
          }
        });
  }
}

同时在 main 中设置这一段路由

public class Main {
      public static void main(String[] args) {
          Router router = Router.router(vertx);

          UserController userController = new UserController();
          userController.setupRoutes(router);  // 设置路由

          vertx.createHttpServer()
              .requestHandler(router)
              .listen(8080);
      }
  }

总结

本文章介绍了以下内容

  1. Gradle 项目以及其组织方法
  2. 各个层级的作用以及相应的模板语法
posted @ 2026-02-10 10:55  五花肉炒河粉  阅读(2)  评论(0)    收藏  举报