微服务中,如何优化接口调用

IIIIJDASHJF
发布于 2022-4-12 20:15
浏览
0收藏

在微服务的设计中,我们通常考虑到的是通过加密、熔断、限流等操作保证接口的安全性、健壮性等问题,但是在代码的编写中,你考虑过优化接口的调用方式吗?下面我们就来看一看,如何更优雅的调用接口。

假设在我们的系统中现在有两个微服务,订单服务和库存服务,业务流程是当使用订单服务创建订单时,先调用库存服务查询是否有库存,如果有库存才能完成订单的下单操作。

先从简单入手,以使用RestTemplate进行服务间调用为例。定义库存服务及其提供的对外调用接口:

@Service
public class StockService {
    public Integer query(String id){
        return 10;
    }
}

@RestController
public class StockController {
    @Autowired
    StockService stockService;
    @PostMapping("/query")
    public Integer queryStock(String id){
        return stockService.query(id);
    }
}

再定义订单服务及其提供的对外调用接口,在订单服务中,使用RestTemplate调用库存服务:

@Service
public class OrderService {
    @Autowired
    private RestTemplate restTemplate;
    public String createOrder(String id){
        Integer stock = restTemplate.postForObject("http://stockService/query", id, Integer.class);
        System.out.println(stock);
        return "create success";
    }
}

@RestController
public class OrderController {
    @Autowired
    OrderService orderService;
    @GetMapping("/create")
    public String createOrder(@RequestParam String id){
        return orderService.createOrder(id);
    }
}

这样写可以正常进行调用并返回结果,但存在一些的问题:

  • 在使用RestTemplate 进行服务间调用使用的是字符串,如果调用的路径填写错误,编译器在编译的时候不会进行提示,只有在真正调用服务时才会发现错误
  • 如果在订单服务中使用了多次库存服务,那么这个库存服务的接口地址就会出现多次,如果后期维护中接口的路径发生变化,那么需要修改所有出现调用的地方

针对这两个问题,如果将调用路径单独封装成常量,那么在调用的时候直接引用这个常量,可以避免字符串出现错误,并且在后续的修改中,只修改一个地方就可以了。在订单服务中创建一个类来维护接口字符串:

public class StockURL{
    public static final String PREFIX="http://stockService";
    public static final String STOCK_QUERY="/query";
}

并将调用改为:

public String createOrder(String id){
    Integer stock = restTemplate.postForObject(StockURL.PREFIX+StockURL.STOCK_QUERY, id, Integer.class);
    System.out.println(stock);
    return "create success";
}

这样做的确可以一定程度规避接口名称错误带来的风险,但是回头一看,这个接口名在库存服务中同样也可以被直接用到,也就是说如果直接由服务的提供方来维护接口名的话,是不是更好一些呢?

为了让这个常量在两个微服务中同时被调用,可以单独创建一个Module来维护它,将这个Module命名为stock-api,并将之前创建的StockURL类直接复制过来。在订单服务和库存服务的pom文件中引入我们创建的模块依赖:

<dependency>
  <groupId>com.cn.hydra</groupId>
  <artifactId>stock-api</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>

订单服务维持原样不动,库存服务可以修改接口:

@PostMapping(StockURL.STOCK_QUERY)
public Integer queryStock(String id){
    return stockService.query(id);
}

这样,由库存服务的提供者来维护stock-api模块,只需要修改常量就可以做到只需要修改一次,其他地方不再需要修改。

那么,是否还存在其他问题呢?仔细看一下发起请求调用:

restTemplate.postForObject(StockURL.PREFIX+ StockURL.STOCK_QUERY, id, Integer.class);

RestTemplate 发送请求时的参数和返回类型由发起调用方指明,那么这样仍然存在风险。虽然在微服务调用间一般都会提供比较详细的接口文档说明,但是如果接口发生变更但文档没有及时更新,那么仍然可能发生调用时的错误。同样,这样的错误是编译器不会提醒,只有在调用时才会被发现的。

那么,如果在调用远程方法时,希望能够像调用本地方法一样,给出参数和返回值的提示,不符合要求时能够及时报错,那么就要继续改造,由服务提供方进行接口的维护。

我们把OrderService 类拿到stock-api模块中加以改造,库存服务提供者来维护这个接口:

@Service
@ConditionalOnBean(RestTemplate.class)
public class StockServiceApi {
    @Autowired
    private RestTemplate restTemplate;
    public Integer query(String id){
        Integer stock = restTemplate.postForObject(StockURL.PREFIX+ StockURL.STOCK_QUERY, id, Integer.class);
        return stock;
    }
}

需要注意,这样提供的Service在订单服务中是无法通过@Autowired被直接注入的,因为Springboot的自动扫描是扫描不到这个Bean的,如果我们不希望再通过@Bean的方式手动注入的话,那么我们可以模仿starter的方式,来将这个Bean注入到容器中,在stock-api模块的最外层定义一个Configuration类进行扫描:

@Configuration
@ComponentScan
public class Config {
}

创建META-INF/spring.factories:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.cn.Config

在订单服务中重写方法调用,现在就可以直接调用接口,省去了还要翻阅接口文档看RestTemplate 调用需要判断参数和返回值的麻烦了:

@RestController
public class OrderController2 {
    @Autowired
    StockServiceApi stockServiceApi;
    @GetMapping("/create2")
    public String createOrder(@RequestParam String id){
        Integer stock = stockServiceApi.query(id);
        System.out.println(stock);
        return "create success";
    }
}

改造到这个程度,可以看到服务调用者的工作被大幅度简化了,但是服务提供者同时要对外提供Controller和ServiceApi两者,增添了一定的工作量。并且,Controller和Service的数量应该是一一对应的。那么为了更稳定的维护这个对应关系,其实可以创建一个接口来实现绑定:

public interface IStockService {
    Integer query(String id);
}

再分别让Controller和Service实现这个接口:

public class StockServiceApi implements IStockService{...
public class StockController implements IStockService {...

这样当修改接口时,就会提示我们去修改所有的实现类。

到这,对接口调用的优化进行一下总结:

  • 通过定义字符串常量避免接口地址调用出错
  • 服务提供方同时提供接口及接口的Api调用,消灭服务调用者调用时参数及返回值的潜在错误
  • 服务提供方通过接口的方式绑定Controller和Api

讨论完使用RestTemplate的调用方式,接下来看一下使用Feign调用时,应该如何优化接口调用。

先看一下正常方式下,订单服务使用Feign调用库存服务:

@FeignClient("stockService")
public interface IFeignOrderService {
    @PostMapping("/query")
    Integer query(String id);
}

我们知道,Feign中调用的接口路径和服务提供方的接口路径是一致的,那么也可以通过提供公共接口的方式进行优化。在stock-api模块中添加一个接口:

public interface IFeignStockService {
    @PostMapping("/query")
    Integer query(String id);
}

修改服务提供方的Controller类实现该接口,方法上不再需要加@PostMapping注解,会自动继承:

@RestController
public class FeignStockController implements IFeignStockService {
    @Autowired
    StockService stockService;
    @Override
    public Integer query(String id) {
        return stockService.query(id);
    }
}

修改服务调用方接口,同样继承stock-api模块中的接口,写一个空接口即可:

@FeignClient("stockService")
public interface IFeignOrderService extends IFeignStockService {
}

在使用Feign的情况下,本身就通过声明式的服务调用简化了接口的使用过程,通过上述的这一种方式,能够进而通过api模块完成了服务调用的优化,保证了代码的易维护性与稳定性。

本文转载自微信公众号「码农参上」

收藏
回复
举报
回复
    相关推荐