Spring Cloud OpenFeign:基于Ribbon和Hystrix的声明式服务调用
Spring Cloud OpenFeign:基于Ribbon和Hystrix的声明式服务调用
1、简介
官网:https://spring.io/projects/spring-cloud-openfeign
feign是一个声明式WebService 客户端,使用Feign能让编写Web Service客户端更加简单,我们只需创建一个接口并用注解的方式来配置它,就可以实现对某个服务接口的调用,简化了直接使用RestTemplate来调用服务接口的开发量。Feign具备可插拔的注解支持,同时支持Feign注解、JAX-RS注解及SpringMvc注解。当使用Feign时,Spring Cloud集成了Ribbon和Eureka以提供负载均衡的服务调用及基于Hystrix的服务容错保护功能。
他的使用方法是定义一个服务接口,然后在上面添加注解。Feign也支持可插拔式的编码和解码器。Spring Cloud 对Feign进行了封装,使其支持Spring MVC 标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡。
2、Feign能干什么?
Feign使编写java Http客户端更加容易
前面使用Ribbon + RestTemplate时,利用RestTemplate对Http请求的封装处理,形成了一套模板化的调用方法,但是在实际的开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多次调用。所以Feign在此基础上做了进一步的封装,由他来帮助我们定义和实现依赖服务接口。在Feign的实现下,我们只需要创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring Cloud Ribbon,自动封装服务调用客户端的工作量。
Feign集成了Ribbon
Ribbon维护了服务列表信息,并且通过轮询的方式实现客户端的负载均衡。而与Ribbon不同的是,通过Feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务的调用。
Feign 与 OpenFeign的区别?
Feign |
OpenFeign |
Feign是Spring Cloud组件中的一个轻量级RestFul的Http服务客户端Feign内置了Ribbon,用来做客户端的负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口就可以调用服务注册中心的服务。 |
OpenFeign是Spring Cloud在feign的基础上支持了SpringMvc的注解,如@RequestMapping等等 OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他的服务 |
<dependency> <groupId>org.springframework.cloud</ groupId > <artifactId>spring-cloud-starter-feign</ artifactId> </ dependency > |
<!-- openfeign--> |
3、OpenFeign 的使用:
3.1、在启动类上添加 @EnableFeignClients 注解来启用Feign的客户端功能。
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class FeignServiceApplication {
public static void main(String[] args) {
SpringApplication.run(FeignServiceApplication.class, args);
}
}
3.2、添加UserService接口完成对user-service服务的接口绑定
@FeignClient(value = "user-service")
public interface UserService {
@PostMapping("/user/create")
CommonResult create(@RequestBody User user);
@GetMapping("/user/{id}")
CommonResult<User> getUser(@PathVariable Long id);
@GetMapping("/user/getByUsername")
CommonResult<User> getByUsername(@RequestParam String username);
@PostMapping("/user/update")
CommonResult update(@RequestBody User user);
@PostMapping("/user/delete/{id}")
CommonResult delete(@PathVariable Long id);
}
3.3、修改yml,OpenFeign默认的等待时间1秒钟,超时后将报错:
server:
port: 80
eureka:
client:
register-with-eureka: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
spring:
application:
name: feign-customer-order
# 设置feign客户端超时时间(openFeign默认支持Ribbon)
ribbon:
# 建立连接后从服务器读取到可用资源的时间
ReadTimeout: 5000
# 建立连接所用的时间,适用于网络正常的情况下,两端连接所用的时间
ConnectTimeout: 5000
4、Feign中的服务降级
Feign中的服务降级使用起来非常方便,只需要为Feign客户端定义的接口添加一个服务降级处理的实现类即可,下面我们为UserService接口添加一个服务降级实现类。
4.1、添加服务降级实现类UserFallbackService, 需要注意的是它实现了UserService接口,并且对接口中的每个实现方法进行了服务降级逻辑的实现。
public class UserFallbackService implements UserService {
@Override
public CommonResult create(User user) {
User defaultUser = new User(-1L, "defaultUser", "123456");
return new CommonResult<>(defaultUser);
}
@Override
public CommonResult<User> getUser(Long id) {
User defaultUser = new User(-1L, "defaultUser", "123456");
return new CommonResult<>(defaultUser);
}
@Override
public CommonResult<User> getByUsername(String username) {
User defaultUser = new User(-1L, "defaultUser", "123456");
return new CommonResult<>(defaultUser);
}
@Override
public CommonResult update(User user) {
return new CommonResult("调用失败,服务被降级",500);
}
@Override
public CommonResult delete(Long id) {
return new CommonResult("调用失败,服务被降级",500);
}
}
4.2、修改UserService接口,设置服务降级处理类为UserFallbackService
修改@FeignClient注解中的参数,设置fallback为UserFallbackService.class即可。
@FeignClient(value = "user-service", fallback = UserFallbackService.class)
public interface UserService {
....
}
4.3、修改application.yml,开启Hystrix功能
feign:
hystrix:
enabled: true #在Feign中开启Hystrix
5、日志打印功能
Feign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign中Http请求的细节。
日志级别:
- NONE:默认的,不显示任何日志;
- BASIC:仅记录请求方法、URL、响应状态码及执行时间;
- HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息;
- FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。
我们通过java配置来使Feign打印最详细的Http请求日志信息。
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
在application.yml中配置需要开启日志的Feign客户端
logging:
level:
com.openapi.service: debug
这里的com.openapi.service
是openFeign接口所在的包名,当然你也可以配置一个特定的openFeign接口。
6、Feign的常用配置
feign:
hystrix:
enabled: true #在Feign中开启Hystrix
compression:
request:
enabled: false #是否对请求进行GZIP压缩
mime-types: text/xml,application/xml,application/json #指定压缩的请求数据类型
min-request-size: 2048 #超过该大小的请求会被压缩
response:
enabled: false #是否对响应进行GZIP压缩
logging:
level: #修改日志级别
com.dw.cloud.service.UserService: debug
如果openFeign没有设置对应得超时时间,那么将会采用Ribbon的默认超时时间。
理解了超时设置的原理,由之产生两种方案也是很明了了,如下:
- 设置openFeign的超时时间
- 设置Ribbon的超时时间(默认1s)
1、设置Ribbon的超时时间(不推荐)
设置很简单,在配置文件中添加如下设置:
ribbon:
# 值的是建立链接所用的时间,适用于网络状况正常的情况下, 两端链接所用的时间
ReadTimeout: 5000
# 指的是建立链接后从服务器读取可用资源所用的时间
ConectTimeout: 5000
2、设置openFeign的超时时间(推荐)
feign:
client:
config:
## default 设置的全局超时时间,指定服务名称可以设置单个服务的超时时间
default:
connectTimeout: 5000
readTimeout: 5000
7、OpenFeign GET 请求使用 @SpringQueryMap 传递对象参数
4.1、SpringCloud在2.1.x版本中提供了 @SpringQueryMap 注解,可以传递对象参数。 但是传递的对象参数中只能包含基本类型,如果需要在传递的参数对象中, 包含其他的对象类型, 需要自定义
类实现 QueryMapEncode 接口,自定义参数解析。具体配置如下:
@Configuration
public class FeignConfiguration {
/**
* 使用@SpringQueryMap可以解决GET请求的时候,传递对象
* 关于使用 @SpringQueryMap不能解析对象中的对象的问题
* @return
*/
@Bean
public Feign.Builder feignBuilder() {
return Feign.builder()
.queryMapEncoder(new BeanQueryMapNestEncoder())
.retryer(Retryer.NEVER_RETRY);
}
/**
* 配置feign日志级别
* @return
*/
@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
4.2、自定义 BeanQueryMapNestEncoder 传递复杂参数解析配置:
import feign.Param;
import feign.QueryMapEncoder;
import feign.codec.EncodeException;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
public class BeanQueryMapNestEncoder implements QueryMapEncoder {
private final Map<Class<?>, BeanQueryMapNestEncoder.ObjectParamMetadata> classToMetadata = new HashMap();
public BeanQueryMapNestEncoder() {
}
public Map<String, Object> encode(Object object) {
try {
Map<String, Object> propertyNameToValue = new HashMap();
this.putPropertiesToMap((String)null, object, propertyNameToValue);
return propertyNameToValue;
} catch (IntrospectionException | InvocationTargetException | IllegalAccessException var3) {
throw new EncodeException("Failure encoding object into query map", var3);
}
}
private void putPropertiesToMap(String parentProperty, Object object, Map<String, Object> propertyNameToValue)
throws IntrospectionException, InvocationTargetException, IllegalAccessException {
BeanQueryMapNestEncoder.ObjectParamMetadata metadata = this.getMetadata(object.getClass());
Iterator var5 = metadata.objectProperties.iterator();
while(true) {
while(true) {
PropertyDescriptor pd;
Method method;
Object value;
StringBuffer nameBuffer;
do {
do {
if (!var5.hasNext()) {
return;
}
pd = (PropertyDescriptor)var5.next();
method = pd.getReadMethod();
value = method.invoke(object);
nameBuffer = new StringBuffer();
if (StringUtils.isNotEmpty(parentProperty)) {
nameBuffer.append(parentProperty).append('.');
}
} while(value == null);
} while(value == object);
Param alias = (Param)method.getAnnotation(Param.class);
nameBuffer.append(alias != null ? alias.value() : pd.getName());
if (BeanUtils.isSimpleProperty(value.getClass())) {
propertyNameToValue.put(nameBuffer.toString(), value);
} else if (!(value instanceof Collection)) {
this.putPropertiesToMap(nameBuffer.toString(), value, propertyNameToValue);
} else {
StringBuffer arrValueBuffer = new StringBuffer();
Object arrObj;
for(Iterator var12 = ((Collection)value).iterator(); var12.hasNext(); arrValueBuffer.append(arrObj)) {
arrObj = var12.next();
if (!BeanUtils.isSimpleProperty(arrObj.getClass())) {
break;
}
if (arrValueBuffer.length() != 0) {
arrValueBuffer.append(',');
}
}
if (arrValueBuffer.length() > 0) {
propertyNameToValue.put(nameBuffer.toString(), arrValueBuffer.toString());
}
}
}
}
}
private BeanQueryMapNestEncoder.ObjectParamMetadata getMetadata(Class<?> objectType) throws IntrospectionException {
BeanQueryMapNestEncoder.ObjectParamMetadata metadata = (BeanQueryMapNestEncoder.ObjectParamMetadata)this.classToMetadata.get(objectType);
if (metadata == null) {
metadata = BeanQueryMapNestEncoder.ObjectParamMetadata.parseObjectType(objectType);
this.classToMetadata.put(objectType, metadata);
}
return metadata;
}
private static class ObjectParamMetadata {
private final List<PropertyDescriptor> objectProperties;
private ObjectParamMetadata(List<PropertyDescriptor> objectProperties) {
this.objectProperties = Collections.unmodifiableList(objectProperties);
}
private static BeanQueryMapNestEncoder.ObjectParamMetadata parseObjectType(Class<?> type) throws IntrospectionException {
List<PropertyDescriptor> properties = new ArrayList();
PropertyDescriptor[] var2 = Introspector.getBeanInfo(type).getPropertyDescriptors();
int var3 = var2.length;
for(int var4 = 0; var4 < var3; ++var4) {
PropertyDescriptor pd = var2[var4];
boolean isGetterMethod = pd.getReadMethod() != null && !"class".equals(pd.getName());
if (isGetterMethod) {
properties.add(pd);
}
}
return new BeanQueryMapNestEncoder.ObjectParamMetadata(properties);
}
}
}
8、Feign 异常处理
5.1、自定义Feign解析器:
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.crecgec.baseboot.jsoncore.exception.BaseException;
import feign.Response;
import feign.Util;
import feign.codec.ErrorDecoder;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
@Configuration
public class FeignErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
try {
// 这里直接拿到我们抛出的异常信息
String message = Util.toString(response.body().asReader());
try {
JSONObject jsonObject = JSONObject.parseObject(message);
return new BaseException(jsonObject.getString("resultMsg"), jsonObject.getInteger("resultCode"));
} catch (JSONException e) {
e.printStackTrace();
}
} catch (IOException ignored) {
}
return decode(methodKey, response);
}
}
5.2、定义系统的异常类
public class BaseException extends RuntimeException {
private int status ;
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public BaseException() {
}
public BaseException(String message, int status) {
super(message);
this.status = status;
}
public BaseException(String message) {
super(message);
}
public BaseException(String message, Throwable cause) {
super(message, cause);
}
public BaseException(Throwable cause) {
super(cause);
}
public BaseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
5.3、统一异常拦截转换对应的异常信息返回前端
public class ResultSet {
/**
* 返回的状态码
*/
private Integer resultCode;
/**
* 返回的消息
*/
private String resultMsg;
/**
* 返回的数据
*/
private Object data;
public ResultSet() {
}
public ResultSet(Integer resultCode, String resultMsg) {
this.resultCode = resultCode;
this.resultMsg = resultMsg;
}
public ResultSet(Integer resultCode, String resultMsg, Object data) {
this.resultCode = resultCode;
this.resultMsg = resultMsg;
this.data = data;
}
public Integer getResultCode() {
return resultCode;
}
public void setResultCode(Integer resultCode) {
this.resultCode = resultCode;
}
public String getResultMsg() {
return resultMsg;
}
public void setResultMsg(String resultMsg) {
this.resultMsg = resultMsg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
5.4、全局异常类处理配置:
@ExceptionHandler(value = BaseException.class)
public ResultSet defaultErrorHandler(HttpServletRequest req, HttpServletResponse resp, BaseException e) {
ResultSet resultSet = new ResultSet();
if (e.getStatus() == 400) {
resultSet.setResultCode(-1);
resultSet.setResultMsg(e.getMessage());
resultSet.setData(null);
resp.setStatus(400);
} else {
resp.setStatus(500);
if(logger.isErrorEnabled()){
logger.error("系统异常,请联系系统开发人员进行处理", e);
}
resultSet.setResultCode(-1);
resultSet.setResultMsg(e.getMessage());
resultSet.setData(null);
}
return resultSet;
}
9、使用OpenFeign 实现文件传输
参考 github上 的实现方式:https://github.com/OpenFeign/feign-form
6.1、引入依赖(如果引入的 OpenFeign 中没有以下依赖):
<dependencies>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>3.8.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
<version>3.8.0</version>
</dependency>
</dependencies>
6.2、Feign 调用端接口:
/**
* @Author dw
* @ClassName TestFileService
* @Description
* @Date 2021/9/20 10:11
* @Version 1.0
*/
@FeignClient(value = "cloud-test-server")
public interface ITestFileService {
@RequestMapping(value = "/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE )
public Object fileUpload(@RequestPart("file")MultipartFile multipartFile, @SpringQueryMap MyUser myUser);
}
6.3、调用端的Controller
@RestController
@RequestMapping("file")
public class TestFileController {
@Autowired
private ITestFileService testFileService;
@RequestMapping("upload")
public Object fileUpload(@RequestPart("file") MultipartFile multipartFile){
Role role = new Role("123", "管理員", 3);
MyUser user = new MyUser("张三", 23, role);
Object o = testFileService.fileUpload(multipartFile, user);
return o;
}
}
6.4、服务端Controller
@RestController
@RequestMapping("file")
public class TestController {
@RequestMapping("upload")
public Object fileUpload(@RequestPart("file") MultipartFile multipartFile, MyUser myUser){
System.out.println(multipartFile);
return myUser;
}
}
OK...文件传输完成了
10、如何替换默认的httpclient?
Feign在默认情况下使用的是JDK原生的URLConnection发送HTTP请求,没有连接池,但是对每个地址会保持一个长连接,即利用HTTP的persistence connection。
在生产环境中,通常不使用默认的http client,通常有如下两种选择:
- 使用ApacheHttpClient
- 使用OkHttp
至于哪个更好,其实各有千秋,我比较倾向于ApacheHttpClient,毕竟老牌子了,稳定性不在话下。
那么如何替换掉呢?其实很简单,下面演示使用ApacheHttpClient替换。
1、添加ApacheHttpClient依赖
在openFeign接口服务的pom文件添加如下依赖:
<!-- 使用Apache HttpClient替换Feign原生httpclient-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
为什么要添加上面的依赖呢?从源码中不难看出,请看org.springframework.cloud.openfeign.FeignAutoConfiguration.HttpClientFeignConfiguration
这个类,代码如下:
上述红色框中的生成条件,其中的@ConditionalOnClass(ApacheHttpClient.class)
,必须要有ApacheHttpClient
这个类才会生效,并且feign.httpclient.enabled
这个配置要设置为true
。
2、配置文件中开启
在配置文件中要配置开启,代码如下:
feign:
client:
httpclient:
# 开启 Http Client
enabled: true
3、如何验证已经替换成功?
其实很简单,在feign.SynchronousMethodHandler#executeAndDecode()
这个方法中可以清楚的看出调用哪个client,如下图:
上图中可以看到最终调用的是ApacheHttpClient
。
11、Feign请求及响应拦截器
1、feign请求拦截,处理head、param、body参数,附加解密定制化处理,也可以使用原生解码器;
package com.config; import com.alibaba.fastjson.JSON; import com.google.common.collect.Maps; import com.utils.EncryptAES; import feign.RequestInterceptor; import feign.RequestTemplate; import feign.Util; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.*; @Slf4j @Configuration public class FeignClientConfig { @Bean public RequestInterceptor headerInterceptor() { //重写请求拦截器 return new RequestInterceptor() { @Override public void apply(RequestTemplate requestTemplate) { //设置单独处理feign请求 requestTemplate.header("feign-aes", "true"); ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes == null) { return; } //处理head HttpServletRequest request = attributes.getRequest(); Enumeration<String> headerNames = request.getHeaderNames(); if (null != headerNames) { while (headerNames.hasMoreElements()) { String name = headerNames.nextElement(); String values = request.getHeader(name); requestTemplate.header(name, values); } } //处理RequestParam Map<String, Collection<String>> queries = requestTemplate.queries(); if (null != queries) { Map<String, Collection<String>> newQueries = new HashMap<>(); for (Map.Entry<String, Collection<String>> entry : queries.entrySet()) { Collection<String> value = new ArrayList<>(); Collection<String> pram = entry.getValue(); value.add(EncryptAES.encryptAES((List)pram.get(0))); newQueries.put(entry.getKey(), value); } requestTemplate.queries(null);// 替换原有对象 原对象设为空,否则会追加 requestTemplate.queries(newQueries);// 替换原有对象 } //处理请求body String method = requestTemplate.method(); if (null != requestTemplate.body()) { int length = requestTemplate.body().length; if (0 < length) { String body = requestTemplate.requestBody().asString(); if (StringUtils.isNotBlank(body)) { String decryptStr = EncryptAES.encryptAES(body); Map<String, Object> map = Maps.newHashMap(); map.put("encryData", decryptStr); String jsonString = JSON.toJSONString(map); byte[] bodyData = jsonString.getBytes(Util.UTF_8);// 替换请求体 requestTemplate.body(bodyData, Util.UTF_8); } } } log.info("FeignClient request headers:{}.", requestTemplate.headers()); } }; } }
2、feign响应拦截,处理head、param、body参数,附加解密定制化处理,也可以使用原生解码器;
package com.config; import com.alibaba.fastjson.JSON; import com.utils.EncryptAES; import feign.FeignException; import feign.Response; import org.apache.commons.io.IOUtils; import org.springframework.beans.factory.ObjectFactory; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.cloud.openfeign.support.SpringDecoder; import java.io.IOException; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; public class FeignClientResponseInterceptor extends SpringDecoder { public FeignClientResponseInterceptor(ObjectFactory<HttpMessageConverters> messageConverters) { super(messageConverters); } @Override public Object decode(final Response response, Type type) throws IOException, FeignException { Map<String, Collection<String>> requestHeadMap = response.request().headers(); //设置单独处理feign请求 List<String> strings = (List<String>) requestHeadMap.get("feign-aes"); String header = strings.get(0); Response.Body body = response.body(); String resultStr = ""; if ("true".equals(header)) { String bodyString = IOUtils.toString(body.asReader(StandardCharsets.UTF_8)); HashMap<String, String> hashMap = JSON.parseObject(bodyString, HashMap.class); String value = hashMap.get("value"); String decryptAES = EncryptAES.decryptAES(value); resultStr = decryptAES; } Object o = super.decode(response.toBuilder().body(resultStr, StandardCharsets.UTF_8).build(), type); return o; } }
3、附件加解密工具(支持格式化\r\n、\r\t)等格式化代码加解密
package com.utils; import lombok.extern.slf4j.Slf4j; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.security.SecureRandom; import java.util.Base64; @Slf4j public class EncryptAES { private static final String ENCODING = "UTF-8"; private static final String PASSWORD = "PASSWORD_KEY"; //加密秘钥 /** * 对字符串加密 */ public static String encryptAES(String content) { byte[] encryptResult = encrypt(content); String encryptResultStr = parseByte2HexStr(encryptResult); // BASE64位加密 encryptResultStr = strEncrypt(encryptResultStr); return encryptResultStr; } /** * 对字符串解密 */ public static String decryptAES(String content) { // BASE64位解密 try { String decrypt = strDecrypt(content); byte[] decryptFrom = parseHexStr2Byte(decrypt); byte[] decryptResult = decrypt(decryptFrom); return new String(decryptResult); } catch (Exception e) { log.error("解密异常", e); return null; } } /** * 加密 */ private static byte[] encrypt(String content) { try { KeyGenerator kgen = KeyGenerator.getInstance("AES"); // 注意这句是关键,防止linux下 随机生成key。用其他方式在Windows上正常,但Linux上会有问题 SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG"); secureRandom.setSeed(PASSWORD.getBytes()); kgen.init(128, secureRandom); SecretKey secretKey = kgen.generateKey(); byte[] enCodeFormat = secretKey.getEncoded(); SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES"); Cipher cipher = Cipher.getInstance("AES");// 创建密码器 byte[] byteContent = content.getBytes("utf-8"); cipher.init(Cipher.ENCRYPT_MODE, key);// 初始化 byte[] result = cipher.doFinal(byteContent); return result; // 加密 } catch (Exception e) { log.error("加密异常", e); } return null; } /** * 解密 */ private static byte[] decrypt(byte[] content) { try { KeyGenerator kgen = KeyGenerator.getInstance("AES"); // 防止linux下 随机生成key SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG"); secureRandom.setSeed(PASSWORD.getBytes()); kgen.init(128, secureRandom); SecretKey secretKey = kgen.generateKey(); byte[] enCodeFormat = secretKey.getEncoded(); SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES"); Cipher cipher = Cipher.getInstance("AES");// 创建密码器 cipher.init(Cipher.DECRYPT_MODE, key);// 初始化 byte[] result = cipher.doFinal(content); return result; // 解密 } catch (Exception e) { log.error("解密异常", e); } return null; } /** * 加密字符串 */ private static String strEncrypt(String str) { String result = str; if (str != null && str.length() > 0) { try { byte[] encodeByte = str.getBytes(ENCODING); result = new String(Base64.getEncoder().encode(encodeByte)); } catch (Exception e) { e.printStackTrace(); } } // base64加密超过一定长度会自动换行 需要去除换行符 return result.replaceAll("\r\n", "").replaceAll("\r", "").replaceAll("\n", ""); } /** * 解密字符串 */ private static String strDecrypt(String str) { try { byte[] encodeByte = Base64.getDecoder().decode(str.getBytes()); return new String(encodeByte); } catch (Exception e) { log.error("IO 异常", e); return str; } } /** * 将二进制转换成16进制 */ private static String parseByte2HexStr(byte buf[]) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < buf.length; i++) { String hex = Integer.toHexString(buf[i] & 0xFF); if (hex.length() == 1) { hex = '0' + hex; } sb.append(hex.toUpperCase()); } return sb.toString(); } /** * 将16进制转换为二进制 */ private static byte[] parseHexStr2Byte(String hexStr) { if (hexStr.length() < 1) { return null; } byte[] result = new byte[hexStr.length() / 2]; for (int i = 0; i < hexStr.length() / 2; i++) { int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16); int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16); result[i] = (byte) (high * 16 + low); } return result; } }
解决Feign 的RequestInterceptor 中获取的RequestAttributes为空的问题
原因:Hystrix 的线程池隔离策略的原因。Feign在新的线程中执行,导致获取不到,解决方案:自定义线程隔离策略。
/**
* @Author dw
* @Description 自定义隔离策略 解决RequestContextHolder.getRequestAttributes()为null的问题
* @Date 2024/5/20 16:40
* @Version 1.0
*/
@Slf4j
@ConditionalOnProperty(prefix = "feign", name = "hystrix.enabled", havingValue = "true")
@Component
public class RequestAttributeHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {
private HystrixConcurrencyStrategy delegate;
/**
* 初始化当前自定义的 HystrixConcurrencyStrategy
*/
public RequestAttributeHystrixConcurrencyStrategy() {
try {
this.delegate = HystrixPlugins.getInstance().getConcurrencyStrategy();
if (this.delegate instanceof RequestAttributeHystrixConcurrencyStrategy) {
// 防止重复初始化
return;
}
HystrixCommandExecutionHook commandExecutionHook = HystrixPlugins.getInstance().getCommandExecutionHook();
HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier();
HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher();
HystrixPropertiesStrategy propertiesStrategy = HystrixPlugins.getInstance().getPropertiesStrategy();
HystrixPlugins.reset();
HystrixPlugins.getInstance().registerConcurrencyStrategy(this);
HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook);
HystrixPlugins.getInstance().registerEventNotifier(eventNotifier);
HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher);
HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy);
} catch (Exception e) {
log.error("Failed to register Sleuth Hystrix Concurrency Strategy", e);
}
}
@Override
public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
HystrixProperty<Integer> corePoolSize,
HystrixProperty<Integer> maximumPoolSize,
HystrixProperty<Integer> keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
return this.delegate.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize,
keepAliveTime, unit, workQueue);
}
@Override
public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
HystrixThreadPoolProperties threadPoolProperties) {
return this.delegate.getThreadPool(threadPoolKey, threadPoolProperties);
}
@Override
public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize) {
return this.delegate.getBlockingQueue(maxQueueSize);
}
@Override
public <T> HystrixRequestVariable<T> getRequestVariable(HystrixRequestVariableLifecycle<T> rv) {
return this.delegate.getRequestVariable(rv);
}
@Override
public <T> Callable<T> wrapCallable(Callable<T> callable) {
return new WrappedCallable<>(callable, RequestContextHolder.getRequestAttributes());
}
static class WrappedCallable<T> implements Callable<T> {
private final Callable<T> target;
private final RequestAttributes requestAttributes;
public WrappedCallable(Callable<T> target, RequestAttributes requestAttributes) {
this.target = target;
this.requestAttributes = requestAttributes;
}
@Override
public T call() throws Exception {
try {
RequestContextHolder.setRequestAttributes(requestAttributes, true);
return target.call();
} finally {
RequestContextHolder.resetRequestAttributes();
}
}
}
}