概述
交友项目用 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-interfaceRPC 契约、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 聚合工程(多模块)
- Spring Boot 自动装配与 spring.factories
- Apache Dubbo 注解
@DubboService/@DubboReference - MyBatis-Plus 字段自动填充
总结
- 交友项目用 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 写完整。