1.feign接口逻辑
传统的http调用需要不仅需要自己手动拼接URL,还需要自己设置好请求头,并且需要调用方自己处理相应体,例如:
String url = "http://account-service/api/exchange-rate";
HttpClient client = new HttpClient();
HttpPost post = new HttpPost(url);
post.setHeader("Content-Type", "application/json");
String json = objectMapper.writeValueAsString(request);
post.setEntity(new StringEntity(json));
HttpResponse response = client.execute(post);
String result = EntityUtils.toString(response.getEntity());
BigDecimal rate = objectMapper.readValue(result, BigDecimal.class);
但是利用如果被调用方使用提供了feign接口那就会像调用本地服务一样方便进行调用,接下来是作为被调用方,如何设计好一个feign接口,作为调用方怎么进行调用,接下来设置一个获取汇率的场景,被调用方的服务名叫u_finance,调用方需要获取当日汇率进行原币和人名币的转化逻辑
1.1调用方逻辑
创建好请求和响应实体类,此处省略,重点再创建声明式客户端
// 1. 定义 Feign Client 接口
@FeignClient(name = "u_finance") // ← 关键:指定服务名
public interface FinanceClient {
@PostMapping("/api/exchange-rate")
Result<BigDecimal> getRate(@RequestBody RateRequest request);
}
// 2. 直接注入使用
@Service
public class MyService {
@Resource
private FinanceClient financeClient; // ← Spring 自动创建代理对象
public void doSomething() {
Result<BigDecimal> result = financeClient.getRate(request);
}
}
1.2被调用方逻辑
创建好一个调用接口,就像平常创建的可被调用接口一样就行
// u_finance 服务中的 Controller
@RestController
@RequestMapping("/api")
public class ExchangeRateController {
@PostMapping("/exchange-rate") // ← 路径必须和 Feign Client 一致
public Result<BigDecimal> getRate(@RequestBody RateRequest request) {
// 业务逻辑
BigDecimal rate = rateService.getRate(request);
return Result.success(rate);
}
}
1.3两者如何关联起来的
feign接口功能的实现在于nacos服务中心被调用方注册到nacos中,调用方在必须申明nacos中存在的服务,并且路径和被调用方中的信息一致,见图:
调用方 Feign Client:
@FeignClient(name = "u_finance") ← 服务名
@PostMapping("/api/exchange-rate") ← 路径
↓
通过注册中心(Nacos)
↓
被调用方 Controller:
服务名: u_finance ← 在 application.yml 中配置
@PostMapping("/api/exchange-rate") ← 路径必须一致
2.可复盘的日志表和子日志表
为追踪每次上报情况和日后的复盘,设计记录每次上报情况的日志记录表,设计思路为主从表结构和批次追踪
2.1主日志表和从日志表字段结构说明
主日志表一对多从日志表,从日志表通过批次号关联到主表,同时程序本事针对多个租户进行了数据隔离,日志追踪就得带有标明不同客户的内容,同时注意子表详细记录每一次发起请求的情况(是每一次发起http请求或者SDK调用),数据全部上报完毕之后,统计子表中成功与失败的情况来更新主表状态,库表字段结构在这里不进行展示
3.策略模式和模板模式在处理多个上报主体的复用和可维护性
目前项目中甚至可以再抽象出一层逻辑,改内容在另一篇文章中阐述(此处添加url),项目内容可以抽象为两步,一步是数据归集阶段,一步是组装转换数据上报阶段
3.1模板模式和策略模式在数据归集阶段的作用
场景为多个业务场景的数据归集,前置处理流程相同,都为检查批次状态,生成主批次号(数据归集部分也分主表和子表),将归集到的数据用总批次号和主表关联,归集到的放到子表中。前几步的步骤相同,不同之处在于归集部分,会构造不同的请求体去请求不同的服务,因此模板模式可以到构造请求体这一步,再通过策略模式去构造不同场景的数据请求体和后置过滤条件,简单的策略实现将会在下文一并提及
3.2模板模式和策略模式在构建数据
场景为多个监管部门为其地区多个企业财政信息进行监督,那么细分到每个企业中的每个财政场景(如每日产生的流水,每日账户的余额,每月银行借款等内容),模板模式体现在数据构建过程中为保证健壮性的处理。
在上一步数据归集完成之后通过消息队列投递到mq中等待消费者消费,消费者会根据bean对象匹配找到对应的实现类进行处理(此为策略模式在处理不同企业不同场景上报的关键部分,会在后面详细提及,具体操作可见该文章,此处加一个URL),回到模板模式部分,我们将构建数据的步骤分为以下部分

在组装数据时我们先记录日志信息,日志分为主表和子表,主表标明一次上报记录,子表通过主表日志主键ID关联,记录一次主表上报记录中的 “每一次”请求的结果,上报第一个先记录主日志。之后进入关键步骤数据组装部分
策略模式对上报部分的作用
该部分为重点,现在我们将上报企业和上报场景作为一个注解,利用注解配合bean的初始化将该实现类交由spring管理(该内容涉及spring的生命周期,详情见这篇文章打好基础,此处之后加上url),这样代码就能满足不同监管部门对于不同企业不同财政场景上报的需求了,实现过程如下
// 首先创建自定义注解
@Retention(RetentionPolicy.RUNTIME) // ← 运行时保留,可以通过反射获取
@Target(ElementType.TYPE) // ← 只能标注在类上
@Documented // ← 生成JavaDoc时包含此注解
public @interface DataReportService {
String targetCode(); // ← 目标系统代码
String reportType(); // ← 上报类型
}
之后创建容器上下文
// 创建一个全局容器,存储"注解 → 实现类"的映射关系
@Component
public class DataReportHandlerContext {
// 核心:用 ConcurrentHashMap 存储策略映射
// Key: 注解对象(包含 targetCode 和 reportType)
// Value: 实现类的 Class 对象
private static final Map<DataReportService, Class<? extends DataReportDomainService>>
DATA_REPORT_HANDLER_SERVICE_MAP = new ConcurrentHashMap<>();
// 注册方法:将注解和实现类放入 Map
public static void registryDataReportStrategy(
DataReportService annotation,
Class<? extends DataReportDomainService> implClass) {
DATA_REPORT_HANDLER_SERVICE_MAP.put(annotation, implClass);
}
// 获取方法:返回整个 Map
public static Map<DataReportService, Class<? extends DataReportDomainService>>
getDataReportHandlerServiceMap() {
return DATA_REPORT_HANDLER_SERVICE_MAP;
}
}
运行例子如下,可由此衍生出拓展性,我以简单的例子举例,假如你是电脑维修师,我们将电脑的部件拆分为硬件和软件两种维度,同时我们的软件还包括系统光盘,bios启动设置,刷机等内容,此外还有硬件的内容,主板,显卡,电源等,如果抽象出一个维修系统,那么我们来想软件和硬件的维修步骤是不是前几步都是相同的,区别就在于对于具体软件硬件维修时候的具体操作罢了,这就是上面这种模式的体现,抛去业务理解之外需要下功夫的就是bean的生命周期了,点此链接跳转到对应实操部分(此处插入一个URL)
以上为基础内容,项目的运行逻辑基本如上所示,以下为另外的内容,TODO
1.mapStruct实现对象转化
1.1 实体对象VO,DTO,DO
在进入此模块之前,我们先了解数据模型的几种表示新式,更加详细的可见这篇文章
http://lsuccess.xyz/index.php/2025/07/15/%e5%ae%9e%e4%bd%93%e5%af%b9%e8%b1%a1vododtopobopojo%e5%88%b0%e5%ba%95%e6%98%af%e4%ba%9b%e4%bb%80%e4%b9%88/此处进行简单的复述,VO是前端视图展示数据时(或者说是后端想要传递给前端的数据内容)的数据实体,DTO可粗略理解为我们操作的所有实体对象,而PO则是我们数据库层面的实体对象
这些对象之间的简单转换可以利用mapStruct进行转化,通常我们回mapStruct来进行数据库对象转化,以下是实现细节和具体实现步骤
假如我们有这两个类
// 数据库实体(Entity)
class User {
private Long id;
private String userName;
private Integer userAge;
private String email;
private String password;
private Date createTime;
}
// 数据传输对象(DTO)- 用于接口返回
class UserDTO {
private Long id;
private String name;
private Integer age;
private String email;
private String createDate;
}
传统方式:
UserDTO dto = new UserDTO();
dto.setId(user.getId());
dto.setName(user.getName());
dto.setAge(user.getAge());
dto.setEmail(user.getEmail());
// ... 可能有几十个字段
// 或者使用BeanUtils
BeanUtils.copyProperties(user, dto); // 字段名不同就不行
当我们从数据库拿回数据对象想要转化成我们的DTO对象的时候要是一个一个set就得浪费很多时间了,更别说容易出错,但是如果使用了MapStruct的话
@Mapper
public interface UserMapper {
// 以此实例进行转化
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
// 字段名不同要指定映射关系
@Mapping(source = "userName", target = "name")
@Mapping(source = "userAge", target = "age")
@Mapping(source = "createTime", target = "createDate", dateFormat = "yyyy-MM-dd")
@Mapping(target = "password",)
UserDTO toDTO(User user);
// 反向转换
@Mapping(source = "name", target = "userName")
@Mapping(source = "age", target = "userAge")
User toEntity(UserDTO dto);
// 批量转换
List<UserDTO> toDTOList(List<User> users);
}
// 使用时
// 单个转换
User user = userRepository.findById(1L);
UserDTO dto = UserMapper.INSTANCE.toDTO(user);
// 批量转换
List<User> users = userRepository.findAll();
List<UserDTO> dtos = UserMapper.INSTANCE.toDTOList(users);









