Spring Boot基于AOP的本地/远程调用动态路由实践

01为什么我们要"无缝切换"

• MVP 阶段:单体最快,写完就可以上线。

• 流量暴涨后:微服务才能横向扩展,但把本地 userService.findById() 改成 feignClient.findById() 真要命------全链路改一遍,风险高、工期长。

• 理想状态:同一段业务代码,今天跑本地,明天改一行配置就跑远程,零代码侵入。

02核心设计一张图

java 复制代码
┌---------------┐
│  业务代码      │  ← 只认接口 UserService
└-------┬-------┘
        │ 注入
┌-------┴----------------┐
│  统一抽象层             │
│  根据配置动态选择实现    │
├------------┬-----------┤
│ UserServiceLocalImpl │ UserServiceRemoteImpl │
└------------┴-----------┘

03一步步落地

先画好契约------接口层

java 复制代码
// **统一接口** = 本地实现 + 远程 Feign 共用
public interface UserService {
    User getUserById(Long id);
    List<User> listAllUsers();
    User saveUser(User u);
    void updateUser(User u);
    void deleteUser(Long id);
}

本地实现

java 复制代码
@Service
@ConditionalOnProperty(name = "service.mode", havingValue = "local", matchIfMissing = true)
public class UserServiceLocalImpl implements UserService {

    @Autowired
    private UserRepository repo;   // 直连数据库,不走网络

    @Override
    public User getUserById(Long id) {
        return repo.findById(id).orElse(null);
    }

    @Override
    public List<User> listAllUsers() {
        return repo.findAll();
    }

    /* 其余方法省略 */
}

注解

  1. @ConditionalOnProperty:Spring Boot 的条件装配神器,配置文件里写 local 就激活。
  2. 直接依赖 DAO,零网络损耗,单元测试也能秒起。

远程实现------Feign

java 复制代码
// 1. 声明式 HTTP 客户端
@FeignClient(name = "user-service", fallback = UserServiceFallback.class)
public interface UserServiceFeignClient {

    @GetMapping("/api/users/{id}")
    User getUserById(@PathVariable("id") Long id);

    @GetMapping("/api/users")
    List<User> listAllUsers();

    @PostMapping("/api/users")
    User saveUser(@RequestBody User u);

    @PutMapping("/api/users")
    void updateUser(@RequestBody User u);

    @DeleteMapping("/api/users/{id}")
    void deleteUser(@PathVariable("id") Long id);
}
@Service
@ConditionalOnProperty(name = "service.mode", havingValue = "remote")
public class UserServiceRemoteImpl implements UserService {

    @Autowired
    private UserServiceFeignClient feignClient;   // 代理,真正发 HTTP

    @Override
    public User getUserById(Long id) {
        return feignClient.getUserById(id);
    }

    /* 其余方法省略 */
}

注解

  1. @FeignClient:Ribbon + Hystrix 自动集成,负载均衡、熔断降级开箱即用。
  2. fallback:远程挂了直接走兜底逻辑,雪崩不存在的。

自动配置

java 复制代码
@Configuration
@EnableFeignClients(basePackages = "com.example.feign")
public class ServiceAutoConfiguration {

    @Bean
    @ConditionalOnProperty(name = "service.mode", havingValue = "remote")
    public UserService userServiceRemote(UserServiceFeignClient client) {
        return new UserServiceRemoteImpl(client);
    }

    @Bean
    @ConditionalOnProperty(name = "service.mode", havingValue = "local", matchIfMissing = true)
    public UserService userServiceLocal(UserRepository repo) {
        return new UserServiceLocalImpl(repo);
    }
}

配置

java 复制代码
# application.yml
service:
  mode: local   # 改成 remote 秒变微服务

进阶玩法:细粒度路由 + 智能负载
按模块单独开关
service:
  user: local
  order: remote
  product: local

AOP 动态选路(伪代码)

java 复制代码
@Aspect
@Component
public class SmartRoutingAspect {
    @Around("@annotation(SmartRouting)")
    public Object route(ProceedingJoinPoint pjp) {
        // 统计 RT、错误率,实时计算 local vs remote 分值
        boolean goLocal = loadBalancingService.shouldGoLocal(pjp.getSignature());
        return goLocal ? pjp.proceed() : feignInvoke(pjp);
    }
}

05优缺点

相关推荐
lazy H几秒前
不同的消息队列有什么区别?Kafka、RabbitMQ、RocketMQ、Pulsar、ActiveMQ 选型对比
后端·中间件·kafka·rabbitmq·rocketmq
明月_清风2 分钟前
开发者写PPT自救指南:4类对接场景,把技术讲清楚
前端·后端·面试
明月_清风5 分钟前
从经典self-Attention 到 Flash Attention:为什么我们「不必算出每一个 âᵢ」
后端·ai编程
Irene19919 分钟前
Java 3 天入门:“最小必要知识”的功利性学法
java
m0_6406024414 分钟前
2026 年餐饮收银系统前后端技术实现——核心架构与原理详解
后端·微服务·云原生·架构
长谷深风11116 分钟前
Agent 何时该 Replan:五个关键判断
java·大数据·开发语言·ai agent·ai智能体·agent设计·clarify机制
Scene21617 分钟前
Flux 与 Mono:Project Reactor 核心响应式类型深度解析
后端
lingran__22 分钟前
C++ STL unordered系列(哈希) 底层剖析与模拟实现万字详解 | 基于哈希表,复刻 SGI-STL 泛型哈希容器架构
开发语言·c++·后端·哈希算法·哈希表·泛型编程·unordered系列
小林ixn33 分钟前
NestJS 入门实战:从 0 到 1 撸一个 Todo CRUD,感受装饰器与模块化的优雅
后端·mvc·nestjs