Feign与项目多模块

本篇文章主要是记录了Feign的使用方式,并且重点讲述了使用Maven构建多模块项目,从而更好地适应微服务架构的软件开发模式。在服务调用的场景中,我们经常调用基于HTTP协议的服务,Feign封装了Http调用流程,更适合面向接口化的变成习惯。Feign底层使用了Ribbon作为负载均衡的客户端,而有关Ribbon的负载均衡的实现请见 《RestTemplate与负载均衡器》

一、Feign基本使用

1、引入依赖

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2、启动类注解

@EnableFeignClients

3、声明伪RPC调用接口(因为本质还是HTTP)

@FeignClient(name = "SHOP-CLIENT")
public interface ShopClient {
    @GetMapping("/shop/show")
    String getShop();
}

4、注入接口对象并使用

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private ShopClient shopClient;

    @GetMapping
    public String getOrder(){
       return shopClient.getShop();
    }
}

使用Feign可以让调用者无感知这是一个远程调用,获得与本地方法一致的体验

二、项目多模块改造

其实多模块改造主要就是为了复用,毕竟让调用者去声明Client端是不合理的,Client应该由服务端声明,也就是我自己知道自己提供了哪些服务,别人来使用这些服务即可,而不是别人看我的源代码才知道我提供哪些接口。另外像接口输入参数,输出参数也都是需要为调用者提供的。

商品服务有两个功能:查看所有商品、新增商品,商品在数据库中的定义如下:

@Data
@AllArgsConstructor
public class ShopInfo {
    private String shopId;
    private String shopName;
    private Integer shopStock;
}

分别是商品ID、商品名称、商品库存等字段,我们需要给外界展示的是ID和名称,库存多少是没有必要展示费消费者的。同样的对于商城的管理系统来说,如果要添加新的商品,那么主键ID是没有必要让用户手动传入的,所以对于这两种情况分别就是此服务模块的输入参数和输出参数,分别定义对应的JavaBean:

@Data
public class ShopInfoInput {
    private String shopName;
    private Integer shopStock;
}
 
@Data
@AllArgsConstructor
public class ShopInfoOutput {
    private String shopId;
    private String shopName;
}

并且将这两个类放到shop-common模块中,作为商品服务的通用模块。接下来是逻辑的编写,应该放到shop-serivce模块中,shop-service和我们平时的SpringBoot工程无区别,实现的都是主体业务逻辑:

@RestController
@RequestMapping("/shop")
public class ShopController {
  private static final List<ShopInfo> *list* = Arrays.*asList*(
      new ShopInfo(UUID.*randomUUID*().toString(), "ThinkPad X1", 10),
      new ShopInfo(UUID.*randomUUID*().toString(), "MacBook Air", 5),
      new ShopInfo(UUID.*randomUUID*().toString(), "MacBook Pro", 20)
      );
  private static final CopyOnWriteArrayList<ShopInfo> *collect* = new CopyOnWriteArrayList<>(*list*);

  @GetMapping("show")
  public List<ShopInfoOutput> getAllShop(){
    return *collect*.stream()
        .map(x -> new ShopInfoOutput(x.getShopId(), x.getShopName()))
        .collect(Collectors.*toList*());
  }

  @PostMapping("create")
  public List<ShopInfoOutput> addOneShop(@RequestBody ShopInfoInput shopInfoInput){
    *collect*.add(new ShopInfo(UUID.*randomUUID*().toString(), shopInfoInput.getShopName(), shopInfoInput.getShopStock()));
    return *collect*.stream()
        .map(x -> new ShopInfoOutput(x.getShopId(), x.getShopName()))
        .collect(Collectors.*toList*());
  }
 }
 
 @Data
 @AllArgsConstructor
 class ShopInfo {
   private String shopId;
   private String shopName;
   private Integer shopStock;
 }

同时配置好Eureka,在application.yml中:

server:
  port: 8080
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
spring:
  application:
    name: shop-client

最后需要把服务提供暴露给外界使用,所以直接使用Feign来完成shop-client模块的编写:

@FeignClient(name = "SHOP-CLIENT")
public interface ShopClient {

    @GetMapping("/shop/show")
    List<ShopInfoInput> getAllShop();

    @PostMapping("/shop/create")
    List<ShopInfoOutput> addOneShop(@RequestBody ShopInfoInput shopInfoInput);
}

通过上面的代码我们不难发现,项目被分成了三个模块,分别是shop-common、shop-service和shop-client,他们之间的依赖关系如下图所示:

所以,接下来介绍一下1个大工程的pom文件和3个小模块的pom文件,首先是父工程的pom文件:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <!-- Spring Boot版本 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
        <relativePath/>
    </parent>
    
    <groupId>xpu.edu</groupId>
    <artifactId>shop_server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>shop_server</name>
    <description>Demo project for Spring Boot</description>
    
    <!-- 共用的一些配置 -->
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR4</spring-cloud.version>
        <shop-common-version>0.0.1-SNAPSHOT</shop-common-version>
    </properties>

    <!-- 包含的子模块 -->
    <modules>
        <module>shop_client</module>
        <module>shop_common</module>
        <module>shop_service</module>
    </modules>
    <!-- 打包方式必须是pom -->
    <packaging>pom</packaging>

    <!-- 依赖管理 -->
    <dependencyManagement>
        <dependencies>
            <!-- 公用模块 -->
            <dependency>
                <groupId>xpu.edu</groupId>
                <artifactId>shop_common</artifactId>
                <version>${shop-common-version}</version>
            </dependency>
            <!-- SpringCloud -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

父工程的还是基于SpringBoot,另外包含了一些公用配置,包含SpringCloud的版本等信息,还包含了公用模块的依赖。接下来看看shop-service模块:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>xpu.edu</groupId>
        <artifactId>shop_server</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>shop_service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>shop_service</name>

    <dependencies>
        <dependency>
            <groupId>xpu.edu</groupId>
            <artifactId>shop_common</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

这个模块和我们之前写的工程一致,各种必要的依赖(不要忘记公用模块shop-common),编译插件、SpringBoot插件。最后需要看的是shop-client模块,因为这个模块相当于是整个系统的使用手册:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>xpu.edu</groupId>
        <artifactId>shop_server</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>shop_client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>shop_client</name>

    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-openfeign-core</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>

        <dependency>
            <groupId>xpu.edu</groupId>
            <artifactId>shop_common</artifactId>
        </dependency>
    </dependencies>
</project>

因为这个模块用到了@FeignClient(name = “SHOP-CLIENT”)、@GetMapping("/shop/show")、@PostMapping("/shop/create")等注解,所以需要引入spring-web、spring-cloud-openfeign-core等依赖,同样的公用模块需要引入,所以加上了shop-common这个模块的依赖。最后是shop-common的pom文件 :

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>xpu.edu</groupId>
        <artifactId>shop_server</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>shop_common</artifactId>
    <name>shop_common</name>
    <packaging>jar</packaging>
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

其实就是引入了一个lombok,没啥其他的东西。

那么别的模块如何使用shop-server这个工程提供的服务呢?下面是一个order-server即订单服务。它也是一个多模块的项目,分为order-common、order-client、order-service。只是为了测试所以,order-common与order-client模块都是空的,我们直接使用order-service模块测试一下即可:

1、引入依赖

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>edu.xpu</groupId>
        <artifactId>order_server</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>order_service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>order_service</name>
    <description>Demo project for Spring Boot</description>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>xpu.edu</groupId>
            <artifactId>shop_client</artifactId>
            <version>${shop_client.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.67</version>
        </dependency>
        <dependency>
            <groupId>edu.xpu</groupId>
            <artifactId>order_common</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2、添加注解,其实就是为了把FeignClient给添加到IOC容器中

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "xpu.edu.shop_client")
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

3、使用其他(shop-client)模块的FeignClient

@RestController
@RequestMapping("/")
public class TestShopClient {
    @Autowired
    private ShopClient shopClient;

    @GetMapping
    public String test(){
        ShopInfoInput infoInput = new ShopInfoInput();
        infoInput.setShopName("iPad Pro");
        infoInput.setShopStock(100);
        shopClient.addOneShop(infoInput);
        return JSON.toJSONString(shopClient.getAllShop());
    }
}

完整的代码请见:

https://github.com/zouchanglin/practic_code/tree/master/maven_test