通过前两节的示例实践,相信很多读者已经观察到,当使用Spring MVC的注解来绑定服务接口时,我们几乎完全可以从服务提供方的Controller中依靠复制操作,构建出相应的服务客户端绑定接口。既然存在这么多复制操作,我们自然需要考虑这部分内容是否可以得到进一步的抽象呢?在Spring Cloud Feign中,针对该问题提供了继承特性来帮助我们解决这些复制操作,以进一步减少编码量。下面,我们详细看看如何通过Spring Cloud Feign的继承特性来实现REST接口定义的复用。
(1)为了能够复用DTO与接口定义,我们先创建一个基础的Maven工程,命名为hello-service-api。
(2)由于在hello-service-api中需要定义可同时复用于服务端于客户端的接口,我们要使用到Spring MVC的注解,所以在pom.xml中引入spring-boot-starter-web依赖,具体内容如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.helloserviceapi</groupId>
<artifactId>hello-service-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>hello-service-api</name>
<description>Demo project for hello-service-api</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(3)将上一节中实现呃User对象复制到hello-service-api工程中,比如保存到com.helloserviceapi.dto.User。
(4)在hello-service-api工程中创建HelloService接口,内容如下:
package com.helloserviceapi.service;
import com.helloserviceapi.dto.User;
import org.springframework.web.bind.annotation.*;
@RequestMapping("/refactor")
public interface HelloService {
@RequestMapping(value = "/hello4", method = RequestMethod.GET)
String hello(@RequestParam("name") String name);
@RequestMapping(value = "/hello5", method = RequestMethod.GET)
public User hello(@RequestHeader("name") String name, @RequestHeader("age") Integer age);
@RequestMapping(value = "/hello6", method = RequestMethod.POST)
public String hello(@RequestBody User user);
}
因为后续还会通过之前的hello-service和feign-consumer来重构,所以为了避免接口混淆,在这里定义HelloService时,除了头部定义了/refactor前缀之外,同事将提供服务的三个接口更名为/hello4、/hello5、/hello6。对hello-service-api进行install,让其在本地生成一个hello-service-api-0.0.1-SNAPSHOT.jar。
(5)下面对hello-service进行重构,在pom.xml的dependency节点中,新增对hello-service-api的依赖。
<dependency>
<groupId>com.helloserviceapi</groupId>
<artifactId>hello-service-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
(6)创建RefactorHelloController类继承hello-service-api中定义的HelloService接口,并参考之前的HelloService来实现这三个接口,具体内容如下所示:
package com.maple.eurekaproducer.controller;
import com.helloserviceapi.dto.User;
import com.helloserviceapi.service.HelloService;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RefactorHelloController implements HelloService {
@Override
public String hello(@RequestParam("name") String name) {
return "Hello " + name;
}
@Override
public User hello(@RequestHeader("name") String name, @RequestHeader("age") Integer age) {
return new User(name, age);
}
@Override
public String hello(@RequestBody User user) {
return "Hello " + user.getName() + "," + user.getAge();
}
}
我们可以看到通过继承的方式,在Controller中不再包含以往会定义的请求映射注解@RequestMapping,而参数的注解定义在重写的时候会自动带过来。在这个类中,除了要实现接口逻辑之外,只需再增加@RestController注解使该类成为一个REST接口类就大功告成了。
(7)完成了服务提供者的重构,接下来在服务消费者feign-consumer的pom.xml文件中,如在服务提供者中一样,新增对hello-service-api的依赖。
(8)创建RefactorHelloService接口,并继承hello-service-api包中的HelloService接口,然后添加@FeignClient注解来绑定服务。
package com.feign.feignconsumer.service;
import com.feign.feignconsumer.model.User;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.*;
@FeignClient("hello-service")
public interface RefactorHelloService extends HelloService{
}
(9)最后,在ConsumerController中,注入RefactorHelloService的实例,并新增一个请求、feign-consumer3来触发对RefactorHelloService实例的调用。
package com.feign.feignconsumer.controller;
import com.feign.feignconsumer.service.RefactorHelloService;
import com.helloserviceapi.dto.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ConsumerController {
@Autowired
RefactorHelloService refactorHelloService;
@RequestMapping(value = "/feign-consumer3", method = RequestMethod.GET)
public String helloConsumer3() {
StringBuilder sb = new StringBuilder();
sb.append(refactorHelloService.hello("whb")).append("\n");
sb.append(refactorHelloService.hello("whb", 20)).append("\n");
sb.append(refactorHelloService.hello(new User("whb", 20))).append("\n");
return sb.toString();
}
}
测试验证
这次的验证需要注意几个工程的构建顺序,由于hello-service和feign-consumer都依赖hello-service-api工程中的接口和DTO定义,所以必须先构建hello-service-api工程,然后再构建hello-service和feign-consumer。接着我们分别启动服务注册中心,hello-service和feign-consumer。面板信息如下:
并访问http://localhost:9001/feign-consumer3,调用成功后可以获得如下输出:
优点与缺点
使用Spring Cloud Feign继承特性的有点很明显,可以将接口的定义从Controller中剥离,同时配合Maven私有仓库就可以轻易地实现接口定义的共享,实现在构建期的接口绑定,从而有效减少服务客户端的绑定配置。这么做虽然可以很方便的实现接口定义和依赖的共享,不用再复制粘贴接口进行绑定,但是这样的做法使用不当的话会带来副作用。由于接口在构建期间就建立起了依赖,那么接口变动就会对项目构建造成影响,可能服务提供方修改了一个接口定义,那么会直接导致客户端工程的构建失败。所以,如果开发团队通过此方法来实现接口共享的话,建议在开发评审期间严格遵守面向对象的开闭原则,尽可能地做好前后版本的兼容,防止牵一发而动全身的后果,增加团队不必要的维护工作量。