这是Spring Cloud上手系列的第四篇,代码放在GitHub上,随着本系列文章更新。

版本依赖的坑

在写前面几篇的时候都没感觉到SpringCloud的依赖关系处理必须使用io.spring.dependency-management来处理。在使用Feign进行服务消费时遇到很多错误:

  • Feign服务客户端的Bean无法实例化

  • java.lang.NoClassDefFoundError: feign/Feign$Builder

和其它很多错误。现在已经将第一篇中的构建依赖处理好。

配置模块依赖

consumer:service工程的build.gradle中添加以下配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
dependencies {
    compile project(':provider:api')
    compile libs.'eureka-client'  //Eureka客户端
}

jar {
    manifest {
        attributes "Manifest-Version": 1.0,
                'Main-Class': 'com.github.jamsa.sc.consumer.controller.ConsumerController'
    }
}

即这个工程有三个主要的依赖:

  • provider:api中的接口声明。

  • 它也是Eureka客户端工程,也依赖于eureka-client

  • feign的依赖则由全局的build.gradle中处理。

使用Feign在消费方编写API进行消费

consumer:service中添加消费接口,和对应的Fallback实现,fallback实现中不需要配置@RequestParam这类注解,因为它不是对远程方法的引用,它本身就是无法连接远程服务时的替代实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/**
 * 引用服务提供方提供的接口
 */
@FeignClient(name="sc-provider",fallback = FeignFallbackConsumerRemoteService.class)
public interface ConsumerRemoteService{
    @RequestMapping(value="/provider/hello",method= RequestMethod.GET)
    String hello(@RequestParam("name") String name);
}

@Component
public class FeignFallbackConsumerRemoteService implements ConsumerRemoteService {

    @Override
    public String hello(String name) {
        return "未连接远程服务";
    }
}

添加控制器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

/**
 * 服务消费方
 */
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = {"com.github.jamsa.sc.consumer.service"})
@RestController
@RequestMapping("/consumer")
@ComponentScan(basePackages={"com.github.jamsa.sc.consumer"})
public class ConsumerController{

    //注入服务接口
    @Autowired
    private ConsumerRemoteService consumerRemoteService;

    @RequestMapping("/hello")
    public String hello(@RequestParam String name) {
        return "Hello From Remote:"+consumerRemoteService.hello(name);
    }

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

在工程根目录使用gradle :consumer:service:build构建之后,执行java -jar consumer/service/build/libs/sc-consumer-service-0.0.1.jar。启动完毕后,就可以通过http://localhost:9011/consumer/hello?name=Jamsa直接访问就能看到从provider返回的信息。

使用Feign 和服务提供方的API进行消费

使用服务提供方的API,只是在消费端编写接口继承提供方的接口。所共享的代码也仅仅只是接口中的方法声明和各类注解了。

这里我们另外编写一个使用provider:api中的接口的服务:

1
2
3
4
5
@FeignClient(name="sc-provider",fallback = FeignFallbackConsumerRemoteService.class)
@RequestMapping("/provider")
public interface ConsumerRemoteApiService extends ProviderRemoteService {

}

如上所述,这只是一个空接口。

将它注入到ConsumerController中,并在helloByApi这个方法中调用:

1
2
3
4
5
6
7
    @Autowired
    private ConsumerRemoteApiService consumerRemoteApiService;

    @RequestMapping("/helloByApi")
    public String helloByApi(@RequestParam String name) {
        return "Hello From Remote By API:"+consumerRemoteApiService.hello(name);
    }

重新构建并运行之后,访问http://localhost:9011/consumer/helloByApi?name=Jamsa,结果报错了:

1
2
3
4
5
6
7
Whitelabel Error Page

This application has no explicit mapping for /error, so you are seeing this as a fallback.

Sun Jun 03 23:04:58 CST 2018
There was an unexpected error (type=Internal Server Error, status=500).
status 404 reading ConsumerRemoteApiService#hello(String); content: {"timestamp":1528038298528,"status":404,"error":"Not Found","message":"No message available","path":"/hello"}

这是因为我在api中写的RequestMapping并非最终的uri,我在ProviderController上添加了@RequestMapping("/provider")注解,最终hello方法被映射到了/provider/hello上。

在上面这种方式进行消费时,虽然我在ConsumerRemoteApiService中也添加了@RequestMapping("/provider")注解,但是这个注解好像被忽略掉了,估计是因为被注解的类上没有Controller注解。

如果要让这种方式调用成功,就不能在ProviderController上添加@RequestMapping注解。需要将它的内容合并到ProviderRemoteService@RequestMapping

ProviderController调整为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 *  服务提供方
 * Created by zhujie on 2018/5/29.
 */
@SpringBootApplication
@EnableEurekaClient
@RestController
@ComponentScan(basePackages={"com.github.jamsa.sc.provider"})
//@RequestMapping("/provider")
public class ProviderController implements ProviderRemoteService {

    @Override
    public String hello(String name) {
        return "Hello "+name;
    }

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

ProvicerRemoteServic调整为:

1
2
3
4
public interface ProviderRemoteService {
    @RequestMapping(value="/provider/hello",method= RequestMethod.GET)
    String hello(@RequestParam("name") String name);//这个name对服务消费方是必须的,否则调用时会报错
}

调整完毕后重新构建provider:serviceconsumer:service(因为它们都依赖于provider:api),重新运行这两个应用,就能在http://localhost:9011/consumer/helloByApi?name=Jamsa看到期望的结果了。

直接使用RestTempalte消费服务

除使用Feign外,我们也可以直接使用RestTemplate来进行服务消费。

首先,为了配置方便,我们在controller包下增加Config配置RestTemplate

1
2
3
4
5
6
7
8
9
@Configuration
public class Config {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }

}

然后,在ConsumerController中注入RestTemplate并添加这种调用方式的测试入口。

1
2
3
4
5
6
7
@Autowired
    private RestTemplate restTemplate;

    @RequestMapping("/helloByRest")
    public String helloByRest(@RequestParam String name) {
        return "Hello From Remote By RestTemplate: "+restTemplate.getForObject("http://SC-PROVIDER/provider/hello?name="+name,String.class);
    }

注意,这里的@LoadBalanced注解,如果不使用这个注解,我们在调用服务的时候就只能使用http://localhost:9010/provider/hello这种固定的URL。在这里我们使用的URL是通过服务名拼接的,http://SC-PROVIDER/provider/hello并非真实服务提供方的URL,而是由http://{Eureka服务名}/...构成的,为什么可以这样调用呢?还是因为我们在RestTempate这个bean定义的地方使用了@LoadBalanced注解。

如果不添加这个注解,RestTempalte将不具备负载均衡的能力,只能单点调用。添加这个注解后对RestTemplate的调用将被拦截,拦截器将使用Ribbon提供的负载均衡能力,从Eureka中获取服务节点,并挑选某个节点调用。

相关细节可参考 这篇文章