Java 项目实战: 交友项目 - 项目模块划分与依赖关系

概述

交友项目用 Maven 聚合工程把后端切成 7 个模块(其中 mf-dubbo 只是聚合目录,本身不写代码)。这篇文章逐个模块说清它放什么代码、依赖了谁、被谁依赖,再用一次真实的登录请求把跨模块调用串起来。

纲要

  • 工程坐标与版本基线:com.wb:mf:1.0-SNAPSHOT,Spring Boot 2.3.9.RELEASE,JDK 8
  • 模块清单与工程树(mf 根工程下 5 个子模块 + mf-dubbo 下 3 个 Dubbo 子模块)
  • 逐模块职责:
    • mf-model 公共实体 / mf-commons 通用工具
    • mf-autoconfig 自动装配组件(META-INF/spring.factories)
    • mf-dubbo 聚合目录、mf-dubbo-interface RPC 契约、mf-dubbo-db / mf-dubbo-mongo 域服务实现
    • mf-app-server 应用层入口(端口 18080)
  • 依赖关系图:单向、无循环,标出"被广泛依赖的基础模块"与"末端部署单元"
  • 一次跨模块调用:Controller → Service(@DubboReference)→ 接口 → 实现 → MySQL → 实体 → VO
  • 模块划分的工程收益,以及模块与后续章节的对应关系
  • 几个常见误解(mf-dubbo 不是代码模块、model 不能按业务拆散等)

模块划分要解决什么问题

交友项目不是单体工程,而是一套微服务骨架:App 层对外出 HTTP 接口,业务能力由独立的域服务提供,几个能力(短信、OSS、人脸识别)被抽成可复用的组件。这三类东西的生命周期完全不同------接口层天天改、组件半年不动、实体类全项目共用。如果全塞在一个模块里,改一个 Controller 要重新编译整个工程,组件也没法给别的项目引。

Maven 聚合工程就是干这个的:一个父工程 mf 只做版本和依赖的"统一口径",下面挂若干子模块,各自独立打包。父工程打包方式是 pom:

xml 复制代码
<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.mf</groupId>
    <artifactId>mf</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>mf-autoconfig</module>
        <module>mf-model</module>
        <module>mf-dubbo</module>
        <module>mf-commons</module>
        <module>mf-app-server</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.9.RELEASE</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <spring-cloud.version>Hoxton.SR10</spring-cloud.version>
        <spring-cloud-alibaba.version>2.2.5.RELEASE</spring-cloud-alibaba.version>
    </properties>
</project>

父工程自己不产生业务代码,它的价值是统一 <properties> 里的版本号,并用 <dependencyManagement> 锁住 netty、reactor、mysql、mybatis-plus 等一堆第三方包的版本。各子模块声明依赖时不写版本,由父工程兜底,避免同一个 jar 在 7 个模块里出现 7 个版本。

这里有个容易忽略的点:<modules> 里的 5 个模块是 Maven 的构建单元 ,但 mf-dubbo 进去之后还会再聚合 3 个子模块,所以实际参与编译的是 8 个 jar(7 个代码模块 + 1 个聚合 pom)。

工程结构与模块清单

讲义 3.2.2 给的模块表是权威依据,实读 01-代码/mf 后与之一致:

父工程 工程名称 说明
mf mf-autoconfig 自动装配组件(短信等)
mf mf-model 实体类模块
mf mf-dubbo Dubbo 子模块(可以理解为文件夹,管理 Dubbo 模块)
mf mf-commons 工具类
mf mf-app-server 与手机端交互的入口模块
mf-dubbo mf-dubbo-interface Dubbo 接口模块
mf-dubbo mf-dubbo-db Dubbo 服务模块(数据库部分)
mf-dubbo mf-dubbo-mongo Dubbo 服务模块(MongoDB 部分)

真实工程目录(已去掉 target、.idea、*.iml):

tree 复制代码
mf/                                  # 父工程 com.wb:mf:1.0-SNAPSHOT (packaging=pom)
├── pom.xml
├── mf-model/                        # 公共层:实体类
│   └── src/main/java/com/mf/model/domain/
│       ├── BasePojo.java
│       └── User.java
├── mf-commons/                      # 公共层:通用工具
│   └── src/main/java/com/mf/commons/utils/JwtUtils.java
├── mf-autoconfig/                   # 组件层:第三方能力自动装配
│   └── src/main/
│       ├── java/com/mf/autoconfig/
│       │   ├── mfAutoConfiguration.java
│       │   ├── properties/SmsProperties.java
│       │   └── template/SmsTemplate.java
│       └── resources/META-INF/spring.factories
├── mf-dubbo/                        # 聚合目录 (packaging=pom,本身不写代码)
│   ├── pom.xml
│   ├── mf-dubbo-interface/          # 域服务层:RPC 契约
│   │   └── src/main/java/com/mf/dubbo/api/UserApi.java
│   ├── mf-dubbo-db/                 # 域服务层:MySQL 实现 (端口 18081)
│   │   └── src/main/
│   │       ├── java/com/mf/dubbo/
│   │       │   ├── DubboDBApplication.java
│   │       │   ├── api/UserApiImpl.java
│   │       │   ├── mappers/UserMapper.java
│   │       │   └── handler/MyMetaObjectHandler.java
│   │       └── resources/application.yml
│   └── mf-dubbo-mongo/              # 域服务层:MongoDB 实现(本章只建骨架)
│       └── pom.xml
└── mf-app-server/                   # 应用层:App 入口 (端口 18080)
    └── src/main/
        ├── java/com/mf/server/
        │   ├── AppServerApplication.java
        │   ├── controller/LoginController.java
        │   └── service/UserService.java
        └── resources/application.yml

mf-dubbo-mongo 在本章结束的快照里只有一个 pom.xml,没有任何 Java 源码------MongoDB 侧的域服务(动态、位置等)在后面章节才会往里填代码,现在只是把架子搭好。

每个模块到底放什么

mf-model:全项目共用的实体类

mf-model 装的是数据模型(domain 实体),本章只有两个类:BasePojo 和 User。BasePojo 用 MyBatis-Plus 的自动填充注解统一了创建时间/更新时间:

java 复制代码
package com.mf.model.domain;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

@Data
public abstract class BasePojo implements Serializable {

    @TableField(fill = FieldFill.INSERT) // 插入时自动填充
    private Date created;

    @TableField(fill = FieldFill.INSERT_UPDATE) // 插入与更新时自动填充
    private Date updated;
}
java 复制代码
package com.mf.model.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor // 满参构造方法
@NoArgsConstructor  // 无参构造方法
public class User extends BasePojo {

    private Long id;
    private String mobile;
    private String password;
}

为什么要单独抽一个模块放实体?因为同一个 User 会被多处使用 :mf-dubbo-db 查出来返回它,mf-dubbo-interface 的接口签名里声明它,mf-app-server 接收后还要用它组装返回。如果每个模块各写一份,改一个字段要同步改三处,漏一处就编译不过或者字段对不上。集中定义后,谁用谁引依赖,永远只有一份定义。

它自己的依赖也很轻:只引 mybatis-plus(为了 @TableField 那一套注解)。实读的 pom 里还多了 spring-boot-starter-data-mongodb 和 mongodb-driver-sync,是为后面 Mongo 侧的实体(位置、动态)提前准备的。

mf-commons:与业务无关的通用工具

mf-commons 放"跟交友项目这个业务没关系"的通用能力,本章就一个 JwtUtils------生成/校验 JWT token:

java 复制代码
package com.mf.commons.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;

import java.util.Date;
import java.util.Map;

public class JwtUtils {

    // TOKEN 有效期 1 小时(秒)
    private static final int TOKEN_TIME_OUT = 3_600;

    // 加密 KEY
    private static final String TOKEN_SECRET = "mf";

    // 生成 Token
    public static String getToken(Map params) {
        long currentTime = System.currentTimeMillis();
        return Jwts.builder()
                .signWith(SignatureAlgorithm.HS512, TOKEN_SECRET) // 加密方式
                .setExpiration(new Date(currentTime + TOKEN_TIME_OUT * 1000)) // 过期时间戳
                .addClaims(params)
                .compact();
    }

    // 是否有效 true-有效,false-失效
    public static boolean verifyToken(String token) {
        if (StringUtils.isEmpty(token)) {
            return false;
        }
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(TOKEN_SECRET)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            return false;
        }
        return true;
    }
}

判断标准很简单:把 JwtUtils 拷贝到另一个项目也能用,它就是通用工具;如果某个类里出现了"用户、动态、圈子"这种字眼,那它就不属于这里。它依赖的四个包(commons-lang3、commons-codec、joda-time、jjwt)都是纯工具库。

mf-autoconfig:把第三方能力封成可复用的 starter

企业里多个工程都要发短信、存文件,做法是把这些能力抽成组件,业务工程只引依赖 + 加配置 就能用。mf-autoconfig 就是项目的这个组件模块。它的 pom 里堆着阿里云短信 SDK、OSS SDK、人脸识别 SDK、环信 IM SDK 等第三方包------业务模块一个都不用引。

关键在于自动装配三件套。第一件是 META-INF/spring.factories:

text 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.mf.autoconfig.mfAutoConfiguration

Spring Boot 启动时会扫描所有 jar 包里的这个文件,读到 mfAutoConfiguration 就自动加载它,不需要业务模块写 @ComponentScan 指到 com.mf.autoconfig 包 。这是自动装配最容易被问到的点:为什么我在 app-server 里能直接 @Autowired 一个远在别的模块的 SmsTemplate?答案就在这行 spring.factories。

第二件是配置类,把 SmsProperties 注册进容器并对外暴露 SmsTemplate:

java 复制代码
package com.mf.autoconfig;

import com.mf.autoconfig.properties.SmsProperties;
import com.mf.autoconfig.template.SmsTemplate;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

@EnableConfigurationProperties({
        SmsProperties.class
})
public class mfAutoConfiguration {

    @Bean
    public SmsTemplate smsTemplate(SmsProperties properties) {
        return new SmsTemplate(properties);
    }
}

第三件是属性类和模板类。属性类绑定 mf.sms.* 配置,模板类封装真正的发送逻辑:

java 复制代码
package com.mf.autoconfig.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data
@ConfigurationProperties(prefix = "mf.sms")
public class SmsProperties {
    private String signName;
    private String templateCode;
    private String accessKey;
    private String secret;
}
java 复制代码
package com.mf.autoconfig.template;

import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.dysmsapi20170525.models.SendSmsResponseBody;
import com.aliyun.teaopenapi.models.Config;
import com.mf.autoconfig.properties.SmsProperties;

public class SmsTemplate {

    private SmsProperties properties;

    public SmsTemplate(SmsProperties properties) {
        this.properties = properties;
    }

    public void sendSms(String mobile, String code) {
        try {
            // 配置阿里云
            Config config = new Config()
                    .setAccessKeyId(properties.getAccessKey())     // AccessKey ID
                    .setAccessKeySecret(properties.getSecret());   // AccessKey Secret
            config.endpoint = "dysmsapi.aliyuncs.com";             // 访问域名

            com.aliyun.dysmsapi20170525.Client client = new com.aliyun.dysmsapi20170525.Client(config);

            SendSmsRequest sendSmsRequest = new SendSmsRequest()
                    .setPhoneNumbers(mobile)
                    .setSignName(properties.getSignName())
                    .setTemplateCode(properties.getTemplateCode())
                    .setTemplateParam("{\"code\":\"" + code + "\"}");

            SendSmsResponse response = client.sendSms(sendSmsRequest);
            SendSmsResponseBody body = response.getBody();
            System.out.println(body.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

配套的 app-server/application.yml 里只要写:

yaml 复制代码
mf:
  sms:
    signName: 物流云商
    templateCode: SMS_106590012
    accessKey: 你的AccessKeyId
    secret: 你的AccessKeySecret

SmsProperties 上的 @ConfigurationProperties(prefix = "mf.sms") 负责把这几行绑成对象,SmsTemplate 拿到的就是这个对象。整个链路里,业务模块一行 SDK 代码都没碰。

mf-dubbo:只是个聚合目录

mf-dubbo 的 packaging 是 pom,目录下没有一个 .java 文件,只有:

xml 复制代码
<artifactId>mf-dubbo</artifactId>
<packaging>pom</packaging>
<modules>
    <module>mf-dubbo-interface</module>
    <module>mf-dubbo-db</module>
    <module>mf-dubbo-mongo</module>
</modules>

它存在的唯一理由是分类:把 Dubbo 相关的三个模块归到一处,让人一眼看出"这三个是一组"。把它当成一个写代码的模块是最常见的误解。真正的代码在它的三个子模块里。

mf-dubbo-interface:RPC 契约

这个模块只放接口,本章只有一个 UserApi:

java 复制代码
package com.mf.dubbo.api;

import com.mf.model.domain.User;

public interface UserApi {

    // 根据手机号码查询用户
    User findByMobile(String mobile);

    // 保存用户,返回用户id
    Long save(User user);
}

它只依赖 mf-model,因为接口签名里用到了 User。注意接口一旦定下来就是契约:应用层按这个接口调用,实现层按这个接口实现,两边都引这个模块,谁也不能私自改签名。这就是 Dubbo 前置课里"抽取公共接口"那一步在项目项目里的落地------把接口和实现分到两个模块,是为了让调用方不必依赖实现方(也就不会顺带引进来 MySQL 驱动、MyBatis 这些它根本不该关心的东西)。

mf-dubbo-db:MySQL 域的提供者

这个模块提供"基于 MySQL 的业务能力",是服务提供者 。它按契约实现 UserApi:

java 复制代码
package com.mf.dubbo.api;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.mf.dubbo.mappers.UserMapper;
import com.mf.model.domain.User;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;

@DubboService
public class UserApiImpl implements UserApi {

    @Autowired
    private UserMapper userMapper;

    // 根据手机号码查询用户
    public User findByMobile(String mobile) {
        QueryWrapper<User> qw = new QueryWrapper<>();
        qw.eq("mobile", mobile);
        return userMapper.selectOne(qw);
    }

    @Override
    public Long save(User user) {
        userMapper.insert(user);
        return user.getId();
    }
}

@DubboService(老版本叫 @Service,和 Spring 的 @Service 同名容易冲突,所以新版 Dubbo 单独给了这个名字)把这个类注册成 Dubbo 服务,注册到注册中心后应用层就能远程调到它。启动类上加了 @MapperScan:

java 复制代码
package com.mf.dubbo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.mf.dubbo.mappers")
public class DubboDBApplication {

    public static void main(String[] args) {
        SpringApplication.run(DubboDBApplication.class, args);
    }
}

它的依赖里除了 MyBatis-Plus、MySQL 驱动、Druid 这些数据访问的东西,还有 spring-cloud-starter-dubbo、Nacos 注册与配置中心,以及 mf-dubbo-interface。application.yml 里端口是 18081,Dubbo 协议端口 20881:

yaml 复制代码
server:
  port: 18081
spring:
  application:
    name: mf-dubbo-db
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/mf?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false
    username: root
    password: root
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.136.160:8848
dubbo:
  protocol:
    name: dubbo
    port: 20881
  registry:
    address: spring-cloud://localhost
  scan:
    base-packages: com.mf.dubbo.api   # Dubbo 服务扫描包
mybatis-plus:
  global-config:
    db-config:
      table-prefix: tb_   # 表名前缀
      id-type: auto       # id 策略为自增长

dubbo.scan.base-packages 要写对,否则 @DubboService 不会被扫到,注册中心里就没这个服务,应用层调用时直接报 No provider available------这是实战里排第一的报错。

mf-dubbo-mongo:MongoDB 域的提供者

跟 mf-dubbo-db 平级,职责是提供基于 MongoDB 的业务能力(动态、位置这类文档型数据)。本章只有 pom,没有实现类,依赖比 db 多了 mf-commons。

mf-app-server:面向 App 的应用层

这是整个工程唯一对外暴露 HTTP 的模块,端口 18080,负责接收 App 请求、编排域服务、把实体转成前端要的 VO。启动类在 com.mf.server 包下:

java 复制代码
package com.mf.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AppServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(AppServerApplication.class, args);
    }
}

它的 pom 是 7 个模块里依赖最多的:Web、Redis、JJWT、fastdfs 客户端、Dubbo、Nacos、RabbitMQ,加上三个内部模块------mf-dubbo-interface、mf-commons、mf-autoconfig。application.yml 里能看到它作为消费者的配置:

yaml 复制代码
server:
  port: 18080
spring:
  application:
    name: mf-app-server
  redis:
    port: 6379
    host: 192.168.136.160
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.136.160:8848
dubbo:
  registry:
    address: spring-cloud://localhost
  consumer:
    check: false   # 启动时不校验提供者是否已就绪

dubbo.consumer.check: false 意味着本地起 app-server 时即使 provider 还没启动也不会报错,方便单独调试。

模块之间的依赖关系

依赖是 Maven 声明的一边倒关系,方向必须单向,否则就是循环依赖,Maven 直接构建失败。实读各模块 pom 后,真实的依赖方向如下:
#mermaid-svg-1fYtKWhwaGnG08R0{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1fYtKWhwaGnG08R0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1fYtKWhwaGnG08R0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1fYtKWhwaGnG08R0 .error-icon{fill:#552222;}#mermaid-svg-1fYtKWhwaGnG08R0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1fYtKWhwaGnG08R0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1fYtKWhwaGnG08R0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1fYtKWhwaGnG08R0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1fYtKWhwaGnG08R0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1fYtKWhwaGnG08R0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1fYtKWhwaGnG08R0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1fYtKWhwaGnG08R0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1fYtKWhwaGnG08R0 .marker.cross{stroke:#333333;}#mermaid-svg-1fYtKWhwaGnG08R0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1fYtKWhwaGnG08R0 p{margin:0;}#mermaid-svg-1fYtKWhwaGnG08R0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1fYtKWhwaGnG08R0 .cluster-label text{fill:#333;}#mermaid-svg-1fYtKWhwaGnG08R0 .cluster-label span{color:#333;}#mermaid-svg-1fYtKWhwaGnG08R0 .cluster-label span p{background-color:transparent;}#mermaid-svg-1fYtKWhwaGnG08R0 .label text,#mermaid-svg-1fYtKWhwaGnG08R0 span{fill:#333;color:#333;}#mermaid-svg-1fYtKWhwaGnG08R0 .node rect,#mermaid-svg-1fYtKWhwaGnG08R0 .node circle,#mermaid-svg-1fYtKWhwaGnG08R0 .node ellipse,#mermaid-svg-1fYtKWhwaGnG08R0 .node polygon,#mermaid-svg-1fYtKWhwaGnG08R0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1fYtKWhwaGnG08R0 .rough-node .label text,#mermaid-svg-1fYtKWhwaGnG08R0 .node .label text,#mermaid-svg-1fYtKWhwaGnG08R0 .image-shape .label,#mermaid-svg-1fYtKWhwaGnG08R0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-1fYtKWhwaGnG08R0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1fYtKWhwaGnG08R0 .rough-node .label,#mermaid-svg-1fYtKWhwaGnG08R0 .node .label,#mermaid-svg-1fYtKWhwaGnG08R0 .image-shape .label,#mermaid-svg-1fYtKWhwaGnG08R0 .icon-shape .label{text-align:center;}#mermaid-svg-1fYtKWhwaGnG08R0 .node.clickable{cursor:pointer;}#mermaid-svg-1fYtKWhwaGnG08R0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1fYtKWhwaGnG08R0 .arrowheadPath{fill:#333333;}#mermaid-svg-1fYtKWhwaGnG08R0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1fYtKWhwaGnG08R0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1fYtKWhwaGnG08R0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1fYtKWhwaGnG08R0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1fYtKWhwaGnG08R0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1fYtKWhwaGnG08R0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1fYtKWhwaGnG08R0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1fYtKWhwaGnG08R0 .cluster text{fill:#333;}#mermaid-svg-1fYtKWhwaGnG08R0 .cluster span{color:#333;}#mermaid-svg-1fYtKWhwaGnG08R0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1fYtKWhwaGnG08R0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1fYtKWhwaGnG08R0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-1fYtKWhwaGnG08R0 .icon-shape,#mermaid-svg-1fYtKWhwaGnG08R0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1fYtKWhwaGnG08R0 .icon-shape p,#mermaid-svg-1fYtKWhwaGnG08R0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1fYtKWhwaGnG08R0 .icon-shape .label rect,#mermaid-svg-1fYtKWhwaGnG08R0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1fYtKWhwaGnG08R0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1fYtKWhwaGnG08R0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1fYtKWhwaGnG08R0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 应用层
域服务层
组件层
公共层
mf-model

实体类
mf-commons

通用工具
mf-autoconfig

自动装配组件
mf-dubbo-interface

RPC 契约
mf-dubbo-db

MySQL 实现
mf-dubbo-mongo

MongoDB 实现
mf-app-server

端口 18080

几个从图里能直接读出来的结论:

  • mf-model、mf-commons、mf-dubbo-interface 是被广泛依赖的基础模块 。它们只被指向、指不出去(interface 还多指一条到 model),改它们风险最高,因为一改全工程都要重新编译。
  • mf-app-server、mf-dubbo-db、mf-dubbo-mongo 是末端部署单元:各自有启动类、各自可独立打包成可执行 jar、各自占端口。三个进程独立启动,谁挂了只影响自己的那部分能力。
  • 依赖严格单向:从"上层"流向"下层",没有一条反向边,也就没有循环依赖。

依赖关系落到 pom 上就是下面这张表("内部依赖"一列只列本工程的模块):

模块 内部依赖 关键第三方依赖 打包/部署
mf-model 无 mybatis-plus、mongodb 驱动 jar,被依赖
mf-commons 无 commons-lang3、commons-codec、joda-time、jjwt 0.9.1 jar,被依赖
mf-autoconfig 无 阿里云短信/OSS/人脸/内容安全 SDK、百度 AI、环信 IM jar,被依赖
mf-dubbo 无(聚合) 无 pom,不产出 jar
mf-dubbo-interface mf-model 无 jar,被依赖
mf-dubbo-db mf-dubbo-interface mybatis-plus、mysql、druid、dubbo、nacos 可执行 jar,端口 18081
mf-dubbo-mongo mf-dubbo-interface、mf-commons dubbo、nacos、mongodb 可执行 jar
mf-app-server mf-dubbo-interface、mf-commons、mf-autoconfig web、redis、jjwt、fastdfs、dubbo、nacos、amqp 可执行 jar,端口 18080

依赖表里有个细节值得单独指出来:mf-app-server 和两个域服务模块都没有直接声明 mf-model ,但代码里都在用 com.mf.model.domain.User。原因是通过 mf-dubbo-interface → mf-model 传递过来的。这属于 Maven 的传递性依赖,能跑通,但做法不推荐------一旦哪天 interface 不再依赖 model,这三个模块会瞬间编译失败,而报错位置和真正的原因隔了好几层。稳妥写法是在用到 User 的模块里自己显式声明一次 mf-model。

一次调用如何穿过这些模块

光看依赖图还不够直觉,把"获取登录验证码 + 校验登录"这条链路拆开看,就能明白模块是怎么协作的。
MySQL UserApiImpl (dubbo-db) UserApi 接口 (dubbo-interface) UserService (app-server) LoginController (app-server) App 客户端 MySQL UserApiImpl (dubbo-db) UserApi 接口 (dubbo-interface) UserService (app-server) LoginController (app-server) App 客户端 #mermaid-svg-E0guiC3HJ0YEajqU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-E0guiC3HJ0YEajqU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-E0guiC3HJ0YEajqU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-E0guiC3HJ0YEajqU .error-icon{fill:#552222;}#mermaid-svg-E0guiC3HJ0YEajqU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-E0guiC3HJ0YEajqU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-E0guiC3HJ0YEajqU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-E0guiC3HJ0YEajqU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-E0guiC3HJ0YEajqU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-E0guiC3HJ0YEajqU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-E0guiC3HJ0YEajqU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-E0guiC3HJ0YEajqU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-E0guiC3HJ0YEajqU .marker.cross{stroke:#333333;}#mermaid-svg-E0guiC3HJ0YEajqU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-E0guiC3HJ0YEajqU p{margin:0;}#mermaid-svg-E0guiC3HJ0YEajqU .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-E0guiC3HJ0YEajqU text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-E0guiC3HJ0YEajqU .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-E0guiC3HJ0YEajqU .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-E0guiC3HJ0YEajqU .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-E0guiC3HJ0YEajqU .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-E0guiC3HJ0YEajqU #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-E0guiC3HJ0YEajqU .sequenceNumber{fill:white;}#mermaid-svg-E0guiC3HJ0YEajqU #sequencenumber{fill:#333;}#mermaid-svg-E0guiC3HJ0YEajqU #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-E0guiC3HJ0YEajqU .messageText{fill:#333;stroke:none;}#mermaid-svg-E0guiC3HJ0YEajqU .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-E0guiC3HJ0YEajqU .labelText,#mermaid-svg-E0guiC3HJ0YEajqU .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-E0guiC3HJ0YEajqU .loopText,#mermaid-svg-E0guiC3HJ0YEajqU .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-E0guiC3HJ0YEajqU .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-E0guiC3HJ0YEajqU .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-E0guiC3HJ0YEajqU .noteText,#mermaid-svg-E0guiC3HJ0YEajqU .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-E0guiC3HJ0YEajqU .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-E0guiC3HJ0YEajqU .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-E0guiC3HJ0YEajqU .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-E0guiC3HJ0YEajqU .actorPopupMenu{position:absolute;}#mermaid-svg-E0guiC3HJ0YEajqU .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-E0guiC3HJ0YEajqU .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-E0guiC3HJ0YEajqU .actor-man circle,#mermaid-svg-E0guiC3HJ0YEajqU line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-E0guiC3HJ0YEajqU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} POST /user/loginVerification {phone, verificationCode} loginVerification(phone, code) 校验 Redis 里的验证码 userApi.findByMobile(phone) Dubbo 远程调用(Nacos 找到提供者) selectOne(mobile=?) 用户行 User(mf-model 的实体) userApi.save(user)(用户不存在时) JwtUtils.getToken() 生成 token Map{token, isNew} 200 + JSON

代码层面,第一跳在 app-server 的 Controller 里。它只做参数提取和调用,不碰任何域逻辑:

java 复制代码
package com.mf.server.controller;

import com.mf.server.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/user")
public class LoginController {

    @Autowired
    private UserService userService;

    /**
     * 获取登录验证码
     * 请求参数:phone(Map)
     * 响应:void
     */
    @PostMapping("/login")
    public ResponseEntity login(@RequestBody Map map) {
        String phone = (String) map.get("phone");
        userService.sendMsg(phone);
        return ResponseEntity.ok(null); // 正常返回状态码 200
    }

    /**
     * 校验登录
     */
    @PostMapping("/loginVerification")
    public ResponseEntity loginVerification(@RequestBody Map map) {
        // 1、调用 map 集合获取请求参数
        String phone = (String) map.get("phone");
        String code = (String) map.get("verificationCode");
        // 2、调用 userService 完成用户登录
        Map retMap = userService.loginVerification(phone, code);
        // 3、构造返回
        return ResponseEntity.ok(retMap);
    }
}

第二跳在同一个模块的 UserService。这里第一次出现跨模块引用------SmsTemplate 来自 autoconfig,JwtUtils 来自 commons,UserApi 来自 dubbo-interface,User 来自 model:

java 复制代码
package com.mf.server.service;

import com.mf.autoconfig.template.SmsTemplate;
import com.mf.commons.utils.JwtUtils;
import com.mf.dubbo.api.UserApi;
import com.mf.model.domain.User;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Service
public class UserService {

    @Autowired
    private SmsTemplate template;                 // 来自 mf-autoconfig

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @DubboReference
    private UserApi userApi;                      // 来自 mf-dubbo-interface,远程代理

    /**
     * 发送短信验证码
     */
    public void sendMsg(String phone) {
        // 1、随机生成 6 位数字(示例里写死便于测试)
        String code = "123456";
        // 2、调用 template 对象发送手机短信(示例中先注释掉)
        // template.sendSms(phone, code);
        // 3、将验证码存入 Redis
        redisTemplate.opsForValue().set("CHECK_CODE_" + phone, code, Duration.ofMinutes(5));
    }

    /**
     * 验证登录
     */
    public Map loginVerification(String phone, String code) {
        // 1、从 Redis 中获取下发的验证码
        String redisCode = redisTemplate.opsForValue().get("CHECK_CODE_" + phone);
        // 2、校验验证码(是否存在、是否一致)
        if (StringUtils.isEmpty(redisCode) || !redisCode.equals(code)) {
            throw new RuntimeException("验证码错误");
        }
        // 3、删除 Redis 中的验证码
        redisTemplate.delete("CHECK_CODE_" + phone);
        // 4、通过手机号码查询用户(跨模块 Dubbo 调用)
        User user = userApi.findByMobile(phone);
        boolean isNew = false;
        // 5、用户不存在则创建并入库
        if (user == null) {
            user = new User();
            user.setMobile(phone);
            user.setPassword(DigestUtils.md5Hex("123456"));
            Long userId = userApi.save(user);
            user.setId(userId);
            isNew = true;
        }
        // 6、通过 JWT 生成 token(存入 id 和手机号码)
        Map tokenMap = new HashMap();
        tokenMap.put("id", user.getId());
        tokenMap.put("mobile", phone);
        String token = JwtUtils.getToken(tokenMap);
        // 7、构造返回值
        Map retMap = new HashMap();
        retMap.put("token", token);
        retMap.put("isNew", isNew);
        return retMap;
    }
}

@DubboReference 注入的 userApi 是个代理对象 ,方法体不在本进程。调用 findByMobile(phone) 时,Dubbo 序列化参数、通过 Nacos 找到 mf-dubbo-db 的地址、走网络把请求发过去,第三跳就落到了另一个模块里那个 UserApiImpl(上一节贴过),由它访问 MySQL。返回的 User 是 mf-model 里的同一个类------因为两边引的是同一个实体模块,序列化/反序列化才能对得上。

每一跳归属哪个模块,梳理成一张表:

跳 执行位置(代码在哪个模块) 关键动作
1 mf-app-server / com.mf.server.controller.LoginController 解析 HTTP 请求,提取 phone、code
2 mf-app-server / com.mf.server.service.UserService 校验验证码、生成 token、组装返回值
3 mf-dubbo-interface / com.mf.dubbo.api.UserApi RPC 契约的方法签名(只有接口,无实现)
4 mf-dubbo-db / com.mf.dubbo.api.UserApiImpl 通过 UserMapper 访问 MySQL
5 mf-model / com.mf.model.domain.User 跨模块传递的数据载体
6 mf-commons / com.mf.commons.utils.JwtUtils 应用层生成 token
7 mf-autoconfig / com.mf.autoconfig.template.SmsTemplate 应用层发送短信

第 3、4 跳之间隔着网络,是模块划分里"契约与实现分离"最直接的体现:应用层从头到尾只见过 UserApi 这个接口,压根不知道背后连的是 MySQL 还是别的存储。

这样划分的工程收益

收益 具体体现
职责清晰、便于分工 实体/工具/组件/契约/域服务/应用各归其位,多个人可并行改不同模块,互不冲突
公共能力复用 mf-model、mf-commons、mf-autoconfig 一处定义,多处引入,字段和逻辑不会出现多份
域服务可独立部署扩容 mf-dubbo-db、mf-dubbo-mongo 各自有启动类,压力大时单独多起几个实例即可
契约集中、便于版本管理 接口只在 mf-dubbo-interface 改,调用方与实现方同时生效,不会各改各的
存储可扩展 要加一个 ES 域服务,只需再挂一个模块实现同一套接口,应用层代码不用动
组件可外借 mf-autoconfig 是标准 starter,换个项目引依赖 + 配 mf.sms 就能用

接下来会在哪个模块动手

模块清单不是背完就完了,它的作用是让你对后续章节有预期。按讲义后续内容,代码基本按下面的方向往里加:

后续内容(讲义章节) 主要落点模块
短信验证码自动装配(第 4 章) mf-autoconfig(spring.factories、SmsProperties、SmsTemplate)
登录发送验证码(第 4 章) mf-app-server(LoginController、UserService)
用户登录 JWT(第 5 章) mf-app-server + mf-commons(JwtUtils)
Dubbo 基础环境与用户查询(第 5 章) mf-dubbo-interface(UserApi)+ mf-dubbo-db(UserApiImpl)+ mf-model(User、BasePojo)
完善用户信息、圈子、动态等 mf-app-server 加 Controller/Service,mf-dubbo-mongo 填 MongoDB 实现,mf-model 加对应实体

看这张表就知道:新增功能时,先想清楚它属于哪一层,再决定代码写进哪个模块 ------域服务实现进 dubbo-db/dubbo-mongo,对外接口进 app-server,实体进 model。想错了地方,分层就白做了。

几个常见误解

把 mf-dubbo 当成一个代码模块。 它 packaging=pom、目录里没有 Java 文件,只是个分类用的聚合目录。真正写代码的是它下面三个子模块。

按业务把 model 拆成好几份。 有人觉得"用户一个 model、动态一个 model"更清晰,但这会让同一个业务概念在多个模块里各有一份定义,字段迟早对不上。实体集中放,正是为了跨模块复用时只有一个真相。

让 app-server 直接连数据库。 应用层如果自己写 Mapper 查 MySQL,那 mf-dubbo-db 就多余了,分层也失去意义。应用层要数据,只走 mf-dubbo-interface。

以为每个域服务必须独立部署。 开发阶段在一台机器上同时起三个进程(18080/18081/...)很常见,也可以把多个模块塞进同一个 Spring Boot 进程跑;模块化是代码组织层面的约束,与部署粒度是两件事。

在应用层 new 一个 UserApiImpl。 那属于实现类,应用层根本不该依赖 dubbo-db。应用层只能通过 @DubboReference 拿到接口代理。

API 速览

接口 方法 所属模块 说明
UserApi User findByMobile(String mobile) mf-dubbo-interface 按手机号查用户,Dubbo 契约
UserApi Long save(User user) mf-dubbo-interface 保存用户并返回自增 id
SmsTemplate void sendSms(String mobile, String code) mf-autoconfig 调阿里云短信发送验证码
JwtUtils String getToken(Map params) mf-commons 生成 JWT,有效期 1 小时
JwtUtils boolean verifyToken(String token) mf-commons 校验 token 是否有效
LoginController POST /user/login mf-app-server 获取登录验证码
LoginController POST /user/loginVerification mf-app-server 校验验证码并返回 token

官方文档

总结

  • 交友项目用 Maven 聚合工程切出 7 个代码模块,其中 mf-dubbo 只是聚合 pom,实际产出 8 个构建单元。
  • 依赖方向严格单向:app-server 与两个域服务模块指向 interface/commons/autoconfig,interface 再指向 model,model 与 commons 是链条末端的基础模块。
  • 一次登录请求横跨五个模块:app-server 接请求与编排、commons 生成 token、interface 提供契约、dubbo-db 落库、model 传递数据。
  • 划分的意义不在目录好看,而在于改动能被限定在一个模块里------加存储只加模块,换组件只改 autoconfig,改契约只动 interface。

下一篇进入《配置依赖》,逐个模块把上面这张依赖表的 pom 写完整。

相关推荐
用户0942485680344 分钟前
第37章:JVM极端场景调优与生产故障复盘方法论
java·jvm
知守观44 分钟前
同事的 SNAPSHOT 更新了,我三天后才拉到:Maven 更新检查被 _remote.repositories 静默跳过
java·后端·maven
努力进修1 小时前
时序数据长期承载难题:从压缩、批量写入到数据组织优化
java·开发语言·数据库
星夜离尘561 小时前
MATLAB 5 点滑动平均:filter 与 conv 的输出对齐,以及 2 样本延时的含义
开发语言·算法·matlab·滤波·fir
Dovis(誓平步青云)1 小时前
总用电下降了,为什么家庭能源看板仍然提示异常
java·开发语言·数据库·人工智能·算法·电脑·能源
程序猿乐锅1 小时前
【从0-1学习Python | 第四篇】Python 字符串和元组
java·开发语言·笔记·后端·python·学习
开开心心就好1 小时前
Windows视频播放器推荐,精简版体积更小
java·服务器·开发语言·eclipse·pdf·ocr·音视频
开开心心就好1 小时前
A3的PDF怎么切成A4?拖一下分割线就行
服务器·开发语言·python·算法·pdf·ocr·tornado
小黄人软件1 小时前
python 模块(Module)与包(Package)的区别,python xxx.py、python -m xxx 和 import xxx 三者的区别
开发语言·python