1、openfeign简介
在谈openfeign之前,我们先说下另一个框架:feign。feign是一个声明式的web服务客户端框架,它提供了一些注解,只要将它们加到某个接口和接口内部的方法上,那就可以很方便地直接用这个接口来开发http客户端代码。再细化来说,就是能让我们写更少的代码来访问服务端的http接口。拿我们熟知的jdk自带的HttpURLConnection类来说,我们要是使用它来访问http接口,加上解析http接口响应的数据,写个十几二十几行代码都是很正常的,就算是用apache的httpClient框架,也是如此。而通过使用feign,我们只需要调用接口的某个方法,就能完成http接口调用。
我们再来说下openfeign,它是Spring Cloud官方提供的,是在feign框架的基础上加了一些对spring mvc注解的支持,同时也支持我们开发spring web应用时所使用的一系列http消息转换器(当然这些基本都是spring框架内置的功能)。而且作为Spring Cloud官方的产品,它天然支持使用eureka、Spring Cloud CircuitBreaker和Spring Cloud LoadBalancer等组件,能使feign接口具备负载均衡的能力,就像咱们之前聊的restTemplate那样。
当然,feign和openfeign都是用来开发客户端代码的,意思就是这部分代码只能给客户端用,客户端通过这些代码来访问相应的服务端接口,而且无论是使用feign,还是使用openfeign,这部分代码基本都是要由服务端来完成开发,然后提供给客户端使用。就像我们开发dubbo服务一样,会提供相应的包含服务接口的jar包给客户端使用(后续会写文章来聊聊dubbo相关的知识)。
2、openfeign实战
和以前一样,针对技术讲解,我们会先聊聊技术的实际应用,然后再说原理。毕竟我们先会用,才更容易去理解它的原理。那么本部分我们来说下openfeign在实际工程中的应用。
2.1、创建openfeign工程
通过idea工具创建一个名为openfeign的Maven工程,工程的pom文件内容如下:
4.0.0
com.xk
openfeign
1.0
pom
openfeign
http://www.example.com
springcloud-openfeign-provider
springcloud-openfeign-consumer
UTF-8
8
8
junit
junit
4.11
test
org.springframework.cloud
spring-cloud-dependencies
2021.0.3
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
2021.0.4.0
pom
import
org.springframework.boot
spring-boot-starter-parent
2.7.0
pom
import
然后我们再在该工程下面创建两个子模块:
模块 | 备注 |
---|---|
springcloud-openfeign-provider | 服务提供者,采用springboot开发,用来提供http接口,供消费者调用 |
springcloud-openfeign-consumer | 服务消费者,采用springboot开发,用来通过openfeign调用服务提供者的http接口 |
2.2、创建工程子模块
2.2.1、创建springcloud-openfeign-provider模块
provider的pom文件如下:
4.0.0
com.xk
springcloud-openfeign-provider
1.0
openfeign-provider-api
8
8
UTF-8
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
1.18.24
我们在该模块下面创建以下三个子模块:
模块 | 备注 |
---|---|
openfeign-provider-api | api模块,提供一些公共的dto类、util类和过滤器供client模块和web模块使用 |
openfeign-provider-client | client模块,需要依赖api模块,通过openfeign封装web模块提供的http接口,然后供客户端调用 |
openfeign-provider-web | web模块,需要依赖api模块,是个完整的springboot web应用,对外提供http服务 |
下面我们分开讲解各个子模块的写法。
2.2.1.1、openfeign-provider-api
api模块作为一个公共的模块,主要提供一些dto类、util工具类和过滤器,供client模块和web模块使用。
image-20230715151449950
api模块的主要类定义如下:
(1)OrderDTO类
package com.xk.openfeign.springcloud.provider.api.dto;
public class OrderDTO {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
(2)TokenFilter类
TokenFilter主要是用来获取请求头中的token,然后将token设置到ThreadLocal对象中,方便后面直接从ThreadLocal中获取到token。ThreadLocal对象是和当前处理任务的线程绑定的,该线程可以在任何代码位置获取到和本线程相关的ThreadLocal对象中存储的信息,而不必将相关信息通过方法参数一步一步地往下传递。
package com.xk.openfeign.springcloud.provider.api.filter;
import com.xk.openfeign.springcloud.provider.api.util.WebThreadLocalUtil;
import org.springframework.util.StringUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* token过滤器
* 用于往线程本地变量工具类中设置token
* 同时也可以借助请求头是否存在token来完成对用户是否登录的校验
* @author xk
* @date 2023-07-07 8:16
*/
public class TokenFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
//这里假设用户登录后,前端将用户token放到了请求头中
String token = request.getHeader("token");
//如果token为空,可以做相应的处理
if(!StringUtils.hasText(token)){
}
//为了实验,此处暂时不考虑token为空
WebThreadLocalUtil.setToken(token);
filterChain.doFilter(servletRequest, servletResponse);
}
}
(3)WebThreadLocalUtil类
package com.xk.openfeign.springcloud.provider.api.util;
import java.util.HashMap;
import java.util.Map;
/**
* 本地线程工具类
* @author xk
* @date 2023-07-07 8:14
*/
public class WebThreadLocalUtil {
private static final String TOKEN = "token";
private static final String USER = "user";
protected static ThreadLocal localMap = new ThreadLocal(){
@Override
protected Map initialValue() {
return new HashMap();
}
};
/**
* 根据key获取对应的值对象
* @param key key
* @return
*/
public static Object getValue(String key) {
return localMap.get().get(key);
}
/**
* 设置键值对
* @param key key
* @param value value
*/
public static void setValue(String key,String value){
localMap.get().put(key,value);
}
/**
* 获取当前登录用户的token
* @return
*/
public static String getToken() {
return (String) localMap.get().get(TOKEN);
}
/**
* 设置token
* @param value token值
* @return
*/
public static void setToken(String value) {
Map objectMap = localMap.get();
objectMap.put(TOKEN,value);
}
}
2.2.1.2、openfeign-provider-web模块
web模块依赖api模块,它就是一个普通的采用springboot开发的web应用,它本身和openfeign没什么关系,也不需要依赖openfeign相关的jar包,所以我们就像开发正常的web应用一样,来进行本模块的开发。另外,web模块的server.context-path被设置为/。
image-20230715151528601
web模块的pom文件内容如下:
4.0.0
com.xk
springcloud-openfeign-provider
1.0
openfeign-provider-web
8
8
UTF-8
com.xk
openfeign-provider-api
1.0
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
8.0.29
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.2.2
org.projectlombok
lombok
1.18.24
org.springframework.boot
spring-boot-maven-plugin
可以看到,pom文件中依赖了openfeign-provider-api模块,并没有别的特别的东西。因为这次我们只关心http接口层的调用,所以我们就只看下controller层,关于mybatis的使用,这里就先不介绍了。
(1)OrderController类
package com.xk.openfeign.springcloud.provider.core.controller;
import com.xk.openfeign.springcloud.provider.api.dto.OrderDTO;
import com.xk.openfeign.springcloud.provider.core.entity.Order;
import com.xk.openfeign.springcloud.provider.core.service.OrderService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author xk
* @since 2023.04.24 14:25
*/
@RequestMapping("/order")
@RestController
public class OrderController{
@Autowired
private OrderService orderService;
@GetMapping("{id}")
public OrderDTO findById(@PathVariable Long id){
Order order = orderService.findById(id);
OrderDTO orderDTO = new OrderDTO();
BeanUtils.copyProperties(order,orderDTO);
return orderDTO;
}
}
OrderController类就提供了一个简单的http查询接口,根据id从数据库查询数据。这里我们也可以简化一下,直接创建一个OrderDTO对象。
web模块我们就先说这块,接下来就说下咱们的重点,如何用openfeign开发client模块,并且能通过client模块直接访问到上面的http接口。
2.2.1.3、openfeign-provider-client模块
client模块是供客户端(消费者)使用的,它依赖api模块,并且将web模块的http接口进行封装,供客户端直接使用。
image-20230715151602179
其中client模块的pom文件内容如下:
4.0.0
com.xk
springcloud-openfeign-provider
1.0
openfeign-provider-client
8
8
UTF-8
com.xk
openfeign-provider-api
1.0
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
1.18.24
org.springframework.cloud
spring-cloud-starter-openfeign
pom文件中很重要的一点就是引入了spring-cloud-starter-openfeign包,版本号是3.1.3版本。
注意:web模块并不会依赖本模块。
接下来我们来看下client模块要提供哪些东西。
(1)TokenRequestInterceptor类
package com.xk.openfeign.springcloud.provider.client.interceptor;
import com.xk.openfeign.springcloud.provider.api.util.WebThreadLocalUtil;
import feign.RequestInterceptor;
import feign.RequestTemplate;
/**
* token请求拦截器,用于在请求头放置token,来满足目标系统的校验
* @author xk
* @date 2023.07.07 09:06
*/
public class TokenRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header("token", WebThreadLocalUtil.getToken());
}
}
TokenRequestInterceptor是个拦截器类,它实现了feign包的RequestInterceptor接口,这个接口主要就是在发起请求前对请求的信息做一下预处理。我们此处使用这个,是为了在请求头中写入token参数,服务提供者可以根据请求头中的token来判断客户端是否有权限访问目标http接口。当然此处我们只是演示,而且是假定服务提供者就是通过获取请求头中的token字段的值来获取用户信息。
(2)OrderClient接口
package com.xk.openfeign.springcloud.provider.client;
import com.xk.openfeign.springcloud.provider.api.dto.OrderDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 客户端接口
* url的值可以写死,比如:http://127.0.0.1:8080
* 为了使用的灵活性,此处已变量形式注入
*/
@FeignClient(name="order",url="${app.order.url}")
public interface OrderClient{
@GetMapping("/order/{id}")
@ResponseBody
OrderDTO findById(@PathVariable Long id);
}
OrderClient接口就是消费者要真正直接用到的,它内部的findById方法上面加了我们很熟悉的spring mvc注解。然后接口上面写了@FeignClient注解,openfeign包会对加了@FeignClient注解的接口进行解析,其中的url属性的值我们就以外部配置的方式来提供,当然配置项的名称已经由我们固定了,就是app.order.url,消费者在自己的配置文件中(也可以是启动参数)就需要声明这么一个配置项。
前面说过,openfeign是可以结合注册中心一起使用的,也就是可以通过提供服务的名称而不是url来完成对目标服务的访问。但是出于快速入门的需要,而且考虑到一些简单的服务可能并不需要依赖注册中心,所以本篇我们就先讲解openfeign直接通过目标服务的url进行调用的方式。
值得注意的是:当前3.1.3版本的openfeign不支持在标记@FeignClient注解的接口上使用@RequestMapping注解,及时我们用了,也不会生效。同时,@FeignClient的name属性也必须赋值,不可以为空。
(3)ClientAutoConfiguration配置类
package com.xk.openfeign.springcloud.provider.client;
import com.xk.openfeign.springcloud.provider.api.filter.TokenFilter;
import com.xk.openfeign.springcloud.provider.client.interceptor.TokenRequestInterceptor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Collections;
/**
* feign客户端配置
*/
@EnableFeignClients
@Configuration
public class ClientAutoConfiguration {
@Bean
public TokenRequestInterceptor tokenRequestInterceptor(){
return new TokenRequestInterceptor();
}
@Bean
public FilterRegistrationBean tokenFilter() {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new TokenFilter());
bean.setUrlPatterns(Collections.singletonList("/*"));
return bean;
}
}
该配置类创建了基于feign的请求拦截器对象,以及过滤器对象。另外配置类上面加了一个很重要的注解@EnableFeignClients。
(4)resources目录下的META-INF/spring.factories文件
在client模块的resources资源目录下,创建META-INF目录,并在META-INF目录下创建spring.factories文件,内容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.xk.openfeign.springcloud.provider.client.ClientAutoConfiguration
当消费者(客户端)引入openfeign-provider-client模块后,ClientAutoConfiguration配置类就会生效,同时配置类中的bean也会生效,而且openfeign接口也会被解析,并将生成的代理对象注入到消费者的spring容器中,方便消费者直接使用。
2.2.1.4、小结
到此,我们完成了openfeign服务提供者模块的编写,接下来就需要将client模块和api模块安装到本地仓库,方便消费者模块引用。当然,实际应用中,我们会把这两个模块打包部署到内网的maven私服,也方便团队人员使用。
2.2.2、创建springcloud-openfeign-consumer模块
本模块是个独立的springboot应用,主要用来演示如何调用其他服务的http接口,因此代码层面会简化,只写controller层。
image-20230715151630018
模块的pom文件内容如下:
4.0.0
com.xk
openfeign
1.0
springcloud-openfeign-consumer
8
8
UTF-8
com.xk
openfeign-provider-client
1.0
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
1.18.24
org.springframework.boot
spring-boot-maven-plugin
pom文件中我们是直接引用了openfeign-provider-client模块,引入之后,也会自动将openfeign相关的包导入到项目中。
2.2.2.1、使用openfeign接口进行调用
我们来看下怎么使用openfeign-provider-client模块,来完成对目标服务的调用。
(1)代码
我们新建一个ConsumerController类,用于暴露http接口,用于自测。
内容如下:
package com.xk.openfeign.springcloud.consumer.core.controller;
import com.xk.openfeign.springcloud.provider.api.dto.OrderDTO;
import com.xk.openfeign.springcloud.provider.client.OrderClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author xk
* @since 2023.07.07 09:01
*/
@RequestMapping("/consumer")
@RestController
public class ConsumerController {
@Autowired
private OrderClient orderClient;
@GetMapping("{id}")
public OrderDTO findById(@PathVariable Long id){
return orderClient.findById(id);
}
}
里面的内容很简单,直接将OrderClient对象注入到当前的Controller中,然后就像调用本地方法一样,完成对目标服务的调用,客户端在调用的时候,写法就是这么超级简单。
(2)配置文件
我们之前在写openfeign-provider-client模块的时候,提到过OrderClient要依赖外部的配置项,为了使服务正常使用,我们还需要在消费者(客户端)配置文件中写入下列信息:
yml格式:
app:
order:
#order服务的访问地址前缀
url: http://localhost:8080
或properties格式:
app.order.url=http://localhost:8080
这里的url的值就是openfeign-provider-web服务所在服务器的ip和端口号。到这里,我们的consumer模块就能正常的通过openfeign接口访问服务提供者的http接口了。
2.3、小结
以前我们开发了一个http接口,会告诉别人具体的http接口地址,供别人调用,一旦对方写错了url地址,就会导致调用出错。而openfeign的出现,能大大简化这个调用的过程。别人不需要再记住我们的接口地址是哪个,只需要给别人一个jar包,然后告诉他调用哪个接口中的哪个方法就可以了。当然,我们自己因为要写只供别人使用的openfeign包,工作量就会稍微多点。但是,换一种角度想想,如果我们大家都这么做,那么彼此间的服务调用就都会更加地方便~
觉得有收获的朋友,可以点击关注我,这样方便接收我后续的文章,多谢支持~