一、使用feign调用其他服务接口
1、加入对应引用
<!-- feign 依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>2.2.2.RELEASE</version> </dependency> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> <version>10.7.4</version> </dependency> <dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.38.41.ALL</version> </dependency> |
---|
2、在启动类开启扫描feignClient修饰的接口的注解EnableFeignClients指定扫描路径
import com.hospital.platform.common.filter.ResponseFilter;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.client.RestTemplate;
@Configuration
@SpringBootApplication(scanBasePackages = "com.hospital.platform",exclude = DataSourceAutoConfiguration.class)
@MapperScan(basePackages={"com.hospital.platform.service.*.dao"})
@EnableAsync
@EnableFeignClients(basePackages = {"com.hospital.platform.api.apaas"})
@EnableScheduling
public class HospitalApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(OnlineHospitalApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(HospitalApplication.class, args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
@Bean
public FilterRegistrationBean someFilterRegistration()
{
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new ResponseFilter());// 配置一个返回值加密过滤器
registration.addUrlPatterns("/*");
registration.addInitParameter("paramName", "paramValue");
registration.setName("responseFilter");
return registration;
}
}
3、编写远程调用接口
import com.api.apaas.feign.config.DataIntegrationFeignConfig;
import com.api.apaas.feign.factory.DataIntegrationClientFallbackFactory;
import com.api.regulators.constants.ServiceConstant;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
@FeignClient(url = "http://localhost:8078/apaas",
name = DATA-INTEGRATION-SERVICE,
configuration = DataIntegrationFeignConfig.class,
fallbackFactory = DataIntegrationClientFallbackFactory.class)
public interface IDataIntegrationServiceClient {
@PostMapping(value = "/unit/test", consumes = "application/x-www-form-urlencoded;charset=UTF-8")
String test();
}
PS:
(1)、这里@FeignClient注解的name入参指定服务名不能使用下划线连接
(2)、接口的返回值必须是指定类型的对象,使用map对象会引起类型不匹配异常
二、配置异常捕获fallback
1、先在@FeignClien注解上配置fallback属性指定异常回调的实现类
import com.api.apaas.feign.IDataIntegrationServiceClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class DataIntegrationServiceClientFallbackImpl implements IDataIntegrationServiceClient {
private Throwable throwable;
public Throwable getThrowable() {
return throwable;
}
public void setThrowable(Throwable throwable) {
this.throwable = throwable;
}
@Override
public String test() {
log.info("000000");
log.error("异常捕获:{}");
return null;
}
}
2、在配置文件中配置开启熔断功能
#feign熔断开启
feign:
hystrix:
enabled: true
同时需要配置超时时间,否则请求都会进入callback
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 60000
三、对请求进行加密数据处理
注入配置类用于对请求进行加密、添加鉴权参数等处理,在@FeignClient注解的configuration属性进行配置类指定,一个@FeignClient只能指定一个
import com.common.helper.util.AESUtil;
import feign.Logger;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
@Slf4j
public class DataIntegrationFeignConfig implements RequestInterceptor {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Override
public void apply(RequestTemplate template) {
// 接口入参加密
String jsonBody = template.requestBody().asString();
try {
template.body(AESUtil.encrypt(jsonBody));
} catch (Exception e) {
log.error("feign请求入参加密失败");
}
//header添加Authorization
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
template.header("Authorization",request.getHeader("accessToken"));
}
}
四、利用okhttp拦截器对Response响应体进行日志打印和解密处理
1、首先需要把springboot版本升级到2.2.13及以上
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.13.RELEASE</version>
</parent>
ps:升级后如果开启多数据源出现循环引用问题,需要在启动类的@SpringBootApplication注解通过exclude属性指定多数据源配置类禁止 SpringBoot 自动注入数据源配置
2、编写对应的拦截器代码
import com.common.interceptor.FeignInterceptor;
import feign.Client;
import feign.Feign;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory;
import org.springframework.cloud.commons.httpclient.OkHttpClientFactory;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* okHttp配置
*/
@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignOkHttpConfig {
@Resource
private FeignInterceptor feignInterceptor;
@Bean
@ConditionalOnMissingBean({Client.class})
public Client feignClient(OkHttpClient client) {
return new feign.okhttp.OkHttpClient(client);
}
@Bean
@ConditionalOnMissingBean({ConnectionPool.class})
public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties, OkHttpClientConnectionPoolFactory connectionPoolFactory) {
Integer maxTotalConnections = httpClientProperties.getMaxConnections();
Long timeToLive = httpClientProperties.getTimeToLive();
TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
}
@Bean
public OkHttpClient client(OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool, FeignHttpClientProperties httpClientProperties) {
Boolean followRedirects = httpClientProperties.isFollowRedirects();
Integer connectTimeout = httpClientProperties.getConnectionTimeout();
Boolean disableSslValidation = httpClientProperties.isDisableSslValidation();
OkHttpClient build = httpClientFactory.createBuilder(disableSslValidation)
.followRedirects(followRedirects)
.connectionPool(connectionPool)
// 连接超时
.connectTimeout(60, TimeUnit.SECONDS)
// 读超时
.readTimeout(60, TimeUnit.SECONDS)
// 写超时
.writeTimeout(60, TimeUnit.SECONDS)
// 是否自动重连
.retryOnConnectionFailure(true)
// 日志拦截器
.addInterceptor(feignInterceptor)
.build();
return build;
}
}
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import com.hospital.platform.common.helper.util.AESUtil;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import okio.Buffer;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* 拦截器
*/
@Slf4j
@Component
public class FeignInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
TimeInterval timer = DateUtil.timer();
Response response = chain.proceed(request);
logForRequest(request);
// 根据请求路径判断是否需要解密
if (response.request().url().toString().contains("/apaas/")) {
return decryptForResponse(response, timer);
}
return logForResponse(response, timer);
}
private Response decryptForResponse(Response response, TimeInterval timer) {
try {
Response.Builder builder = response.newBuilder();
Response clone = builder.build();
log.info("=========decrypt==start======");
log.info(String.format("repose url:%s,code:%s,time is:%s,headers:%s", clone.request().url(), clone.code(), timer.intervalMs() + "ms", clone.protocol()));
ResponseBody body = clone.body();
if (body != null) {
MediaType mediaType = body.contentType();
if (mediaType != null && isText(mediaType)) {
String content = body.string();
content = AESUtil.decrypt(content);
body = ResponseBody.create(mediaType, content);
log.info(String.format("message:%s,contentType:%s,content is:%s,", clone.message(), mediaType, content));
return response.newBuilder().body(body).build();
}
}
} catch (Exception e) {
log.warn("print reponse error", e);
}finally {
log.info("=========decrypt==end======");
}
return response;
}
private Response logForResponse(Response response, TimeInterval timer) {
try {
Response.Builder builder = response.newBuilder();
Response clone = builder.build();
log.info("=========repose==log==start======");
log.info(String.format("repose url:%s,code:%s,time is:%s,headers:%s", clone.request().url(), clone.code(), timer.intervalMs() + "ms", clone.protocol()));
ResponseBody body = clone.body();
if (body != null) {
MediaType mediaType = body.contentType();
if (mediaType != null && isText(mediaType)) {
String content = body.string();
log.info(String.format("message:%s,contentType:%s,content is:%s,", clone.message(), mediaType.toString(), content ));
body = ResponseBody.create(mediaType, content);
return response.newBuilder().body(body).build();
}
}
} catch (Exception e) {
log.warn("print reponse error", e);
}finally {
log.info("=========repose==log==end======");
}
return response;
}
private void logForRequest(Request request) {
String url = request.url().toString();
String method = request.method();
Headers headers = request.headers();
String headerStr = headers != null && headers.size() > 0 ? headers.toString() : "";
log.info("=========request==log==start======");
log.info(String.format("request url:%s,method:%s,headers:%s", url, method, headerStr));
RequestBody requestBody = request.body();
if (requestBody != null) {
MediaType mediaType = requestBody.contentType();
if (mediaType != null && isText(mediaType)) {
log.info("requestBody mediaType:%s,bodyToString:%s", mediaType.toString(), bodyToString(request));
}
}
log.info("=========request==log==end======");
}
private String bodyToString(final Request request) {
final Request copy = request.newBuilder().build();
final Buffer buffer=new Buffer();
try {
copy.body().writeTo(buffer);
} catch (IOException e) {
return "something error,when show requestBody";
}
return buffer.readUtf8();
}
private boolean isText(MediaType mediaType) {
if (mediaType.type() != null && mediaType.type().equals("text")) {
return true;
}
if (mediaType.subtype() != null) {
if (mediaType.subtype().equals("json") ||
mediaType.subtype().equals("xml") ||
mediaType.subtype().equals("html") ||
mediaType.subtype().equals("webviewhtml")) {
return true;
}
}
return false;
}
}
ps:这里需要注意String content = body.string();执行一次后,看源码会自动将资源关闭,再次调用string()就会空指针,所以无法多次调用body.string()