Feign的功能
主要功能是让开发人员只使用简单注解就能像调用一般RPC一样调用HTTP请求,在此基础上,框架扩展了原SpringMVC参数只能为一个类的功能。
基本思路
代理实现->bean的注册->多参数实现
代码实现
注解定义
需要定义三个注解。
第一个“HttpConsumer”,用于消费端注册服务。
/**
* 标注需要代理为http请求发送
*
* @author kun
* @data 2022/1/15 17:50
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HttpConsumer {
/**
* 域名
*
* @return 域名
*/
String domain();
/**
* 端口,默认80
*
* @return 端口号
*/
String port() default "80";
}
第二个“HttpRequest”,用于服务提供方定义服务。
/**
* 用于标记可以通过http访问的服务
*
* @author kun
* @data 2022/1/15 17:55
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface HttpRequest {
/**
* http请求url
*
* @return url
*/
String value();
}
第三个“MultiRequestBody”,用于实现多变量传参。
/**
* 标注方法参数
*
* @author kun
* @data 2022/1/15 17:57
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface MultiRequestBody {
/**
* 解析参数时用到JSON中的key
*
* @return JSON格式参数
*/
String value();
}
动态代理
创建代理的目的是将方法转化为http调用。
/**
* HttpConsumer注解代理类
*
* @author kun
* @data 2022/1/15 19:30
*/
public class HttpConsumerInterceptor implements MethodInterceptor {
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final String HTTP_HEAD = "http://";
private final Class> proxyKlass;
private final HttpDomain httpDomain;
private final OkHttpClient okHttpClient;
public HttpConsumerInterceptor(Class> proxyKlass, HttpDomain httpDomain, OkHttpClient okHttpClient) {
this.proxyKlass = proxyKlass;
this.httpDomain = httpDomain;
this.okHttpClient = okHttpClient;
}
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
HttpRequest httpRequest = method.getAnnotation(HttpRequest.class);
if (Objects.isNull(httpRequest)) {
throw new IllegalStateException("method[" + method.getName() + "] must annotated with @HttpRequest!");
}
HttpRequest klassAnnotation = proxyKlass.getAnnotation(HttpRequest.class);
String namespace = Objects.isNull(klassAnnotation) ? null : klassAnnotation.value();
String url = getUrl(httpRequest.value(), namespace);
Request request = buildRequest(method, args, url);
Call call = okHttpClient.newCall(request);
Response response = call.execute();
ResponseBody body = response.body();
if (Objects.isNull(body)) {
return null;
}
byte[] bytes = body.bytes();
String res = new String(bytes, StandardCharsets.UTF_8);
if (StringUtils.isEmpty(res)) {
return null;
}
try {
return OBJECT_MAPPER.readValue(res, method.getReturnType());
} catch (Throwable t) {
Map, ?> map = OBJECT_MAPPER.readValue(res, Map.class);
Object err = map.get("error");
if (Objects.nonNull(err)) {
throw new RuntimeException(err.toString());
}
throw new RuntimeException(t);
}
}
private String getUrl(String requestPath, String namespace) {
if (Objects.isNull(namespace)) {
return HTTP_HEAD + httpDomain.getDomain() + ":" + httpDomain.getPort() + "/" + requestPath;
}
return HTTP_HEAD + httpDomain.getDomain() + ":" + httpDomain.getPort() + "/" + namespace + "/" + requestPath;
}
private Request buildRequest(Method method, Object[] args, String url) throws JsonProcessingException {
Request.Builder builder = new Request.Builder();
Map paramMap = new HashMap<>(4);
Parameter[] parameters = method.getParameters();
for (int i=0; i
自定义FactoryBean
使用FactoryBean生成bean
/**
* @author kun
* @data 2022/1/16 14:50
*/
public class HttpConsumerProxyFactoryBean implements FactoryBean
注册bean
使用BeanDefinition方式注册bean
/**
* @author kun
* @data 2022/1/16 13:59
*/
@EnableConfigurationProperties(HttpConsumerProperties.class)
public class HttpConsumerPostProcessor implements BeanClassLoaderAware, EnvironmentAware, BeanFactoryPostProcessor, ApplicationContextAware {
private static final Logger logger = LoggerFactory.getLogger(HttpConsumerPostProcessor.class);
private ClassLoader classLoader;
private ApplicationContext context;
private ConfigurableEnvironment environment;
private ConfigurableListableBeanFactory beanFactory;
private final Map httpClientBeanDefinitions = new HashMap<>(4);
private OkHttpClient okHttpClient;
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
this.beanFactory = configurableListableBeanFactory;
this.okHttpClient = buildOkHttpClient(environment);
postProcessBeanFactory(beanFactory, (BeanDefinitionRegistry) beanFactory);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = (ConfigurableEnvironment) environment;
}
private OkHttpClient buildOkHttpClient(ConfigurableEnvironment environment) {
HttpConsumerProperties properties = BinderUtils.bind(environment, HttpConsumerProperties.PREFIX, HttpConsumerProperties.class);
OkHttpClient.Builder builder = new OkHttpClient.Builder();
ThreadPoolExecutor executor = new ThreadPoolExecutor(properties.getCoreThreads(), properties.getMaxThreads(),
properties.getKeepAliveTime(), TimeUnit.SECONDS, new SynchronousQueue<>(), Util.threadFactory("OkHttp Custom Dispatcher", false));
Dispatcher dispatcher = new Dispatcher(executor);
dispatcher.setMaxRequests(properties.getMaxRequests());
dispatcher.setMaxRequestsPerHost(properties.getMaxRequests());
builder.dispatcher(dispatcher);
builder.connectTimeout(properties.getConnectTimeOut(), TimeUnit.SECONDS);
builder.readTimeout(properties.getReadTimeOut(), TimeUnit.SECONDS);
builder.writeTimeout(properties.getWriteTimeOut(), TimeUnit.SECONDS);
builder.connectionPool(new ConnectionPool(properties.getMaxIdleConnections(), properties.getConnectionKeepAliveTime(), TimeUnit.SECONDS));
return builder.build();
}
private void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory, BeanDefinitionRegistry registry) {
for (String beanName : beanFactory.getBeanDefinitionNames()) {
BeanDefinition definition = beanFactory.getBeanDefinition(beanName);
String beanClassName = definition.getBeanClassName();
// 当用@Bean 返回的类型是Object时,beanClassName是null
if (Objects.isNull(beanClassName)) {
continue;
}
Class> clazz = ClassUtils.resolveClassName(definition.getBeanClassName(), this.classLoader);
ReflectionUtils.doWithFields(clazz, this::parseElement, this::annotatedWithHttpConsumer);
}
for (String beanName : httpClientBeanDefinitions.keySet()) {
if (context.containsBean(beanName)) {
throw new IllegalArgumentException("[HttpConsumer Starter] Spring context already has a bean named " + beanName
+ ", please change @HttpConsumer field name.");
}
registry.registerBeanDefinition(beanName, httpClientBeanDefinitions.get(beanName));
logger.info("registered HttpConsumer factory bean \"{}\" in spring context.", beanName);
}
}
private void parseElement(Field field) {
Class> interfaceClass = field.getType();
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException("field [" + field.getName() + "] annotated with @HttpConsumer must be interface!");
}
HttpConsumer httpConsumer = AnnotationUtils.getAnnotation(field, HttpConsumer.class);
HttpDomain httpDomain = HttpDomain.from(httpConsumer);
// 支持占位符${}
httpDomain.setDomain(beanFactory.resolveEmbeddedValue(httpDomain.getDomain()));
httpDomain.setPort(beanFactory.resolveEmbeddedValue(httpDomain.getPort()));
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
.rootBeanDefinition(HttpConsumerProxyFactoryBean.class)
.addConstructorArgValue(interfaceClass)
.addConstructorArgValue(httpDomain)
.addConstructorArgValue(okHttpClient);
BeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();
beanDefinition.setPrimary(true);
beanDefinition.setAutowireCandidate(true);
httpClientBeanDefinitions.put(field.getName(), beanDefinition);
}
private boolean annotatedWithHttpConsumer(Field field) {
return field.isAnnotationPresent(HttpConsumer.class);
}
}
多参数适配
主要是多SpringMVC的参数进行配置
/**
* 参数解析
*
* @author kun
* @data 2022/1/15 21:06
*/
public class MultiRequestBodyResolver implements HandlerMethodArgumentResolver {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final String JSON_REQUEST_BODY = "JSON_REQUEST_BODY";
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.hasParameterAnnotation(MultiRequestBody.class);
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
String requestBody = getRequestBody(nativeWebRequest);
OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
JsonNode rootNode = OBJECT_MAPPER.readTree(requestBody);
if (StringUtils.isEmpty(rootNode)) {
throw new IllegalArgumentException("requestBody must not empty!");
}
MultiRequestBody multiRequestBody = methodParameter.getParameterAnnotation(MultiRequestBody.class);
if (Objects.isNull(multiRequestBody)) {
throw new IllegalArgumentException("param must annotated with @MultiRequestBody!");
}
String key = !StringUtils.isEmpty(multiRequestBody.value()) ? multiRequestBody.value() : methodParameter.getParameterName();
JsonNode value = rootNode.get(key);
if (Objects.isNull(value)) {
return null;
}
Class> paramType = methodParameter.getParameterType();
return OBJECT_MAPPER.readValue(value.toString(), paramType);
}
/**
*
* 获取请求体的JSON字符串
*/
private String getRequestBody(NativeWebRequest webRequest) {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
String jsonBody = (String) webRequest.getAttribute(JSON_REQUEST_BODY, NativeWebRequest.SCOPE_REQUEST);
if (!StringUtils.isEmpty(jsonBody)) {
return jsonBody;
}
try (BufferedReader reader = servletRequest.getReader()) {
jsonBody = IOUtils.toString(reader);
webRequest.setAttribute(JSON_REQUEST_BODY, jsonBody, NativeWebRequest.SCOPE_REQUEST);
return jsonBody;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
/**
* JSON解析数据
*
* @author kun
* @data 2022/1/15 21:03
*/
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer pathMatchConfigurer) {
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer contentNegotiationConfigurer) {
}
@Override
public void configureAsyncSupport(AsyncSupportConfigurer asyncSupportConfigurer) {
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer defaultServletHandlerConfigurer) {
}
@Override
public void addFormatters(FormatterRegistry formatterRegistry) {
}
@Override
public void addInterceptors(InterceptorRegistry interceptorRegistry) {
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry resourceHandlerRegistry) {
}
@Override
public void addCorsMappings(CorsRegistry corsRegistry) {
}
@Override
public void addViewControllers(ViewControllerRegistry viewControllerRegistry) {
}
@Override
public void configureViewResolvers(ViewResolverRegistry viewResolverRegistry) {
}
/**
* 参数解析器
*
* @param list 解析器
*/
@Override
public void addArgumentResolvers(List list) {
list.add(new MultiRequestBodyResolver());
}
@Override
public void addReturnValueHandlers(List list) {
}
@Override
public void configureMessageConverters(List> list) {
MappingJackson2HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter();
LinkedList mediaTypes = new LinkedList<>();
mediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
jacksonConverter.setSupportedMediaTypes(mediaTypes);
jacksonConverter.setDefaultCharset(StandardCharsets.UTF_8);
list.add(jacksonConverter);
}
@Override
public void extendMessageConverters(List> list) {
}
@Override
public void configureHandlerExceptionResolvers(List list) {
}
@Override
public void extendHandlerExceptionResolvers(List list) {
}
@Override
public Validator getValidator() {
return null;
}
@Override
public MessageCodesResolver getMessageCodesResolver() {
return null;
}
}
编写为Spring starter
/**
* 服务调用方生效
*
* @author kun
* @data 2022/1/16 15:36
*/
@ConditionalOnClass(OkHttpClient.class)
public class HttpClientAuthConfig {
@Bean
@ConditionalOnMissingBean
public HttpConsumerPostProcessor httpConsumerPostProcessor() {
return new HttpConsumerPostProcessor();
}
}
/**
* 服务提供方生效
*
* @author kun
* @data 2022/1/16 15:40
*/
@ConditionalOnClass(WebMvcConfigurer.class)
public class SpringMvcConfigurerAutoConfig {
@Bean
@ConditionalOnMissingBean
public WebMvcConfig webMvcConfig() {
return new WebMvcConfig();
}
@Bean
@ConditionalOnMissingBean
public HttpRequestValidator httpRequestValidator() {
return new HttpRequestValidator();
}
}
测试
服务提供方
/**
* @author kun
* @data 2022/1/22 14:25
*/
@HttpRequest("demo")
public interface DemoHttpService {
@HttpRequest("checkSuccess")
String checkSuccess(@MultiRequestBody("param1") String param1, @MultiRequestBody("param2") String param2);
}
服务调用方
/**
* @author kun
* @data 2022/1/22 14:37
*/
@Configuration
public class HttpConfig {
@HttpConsumer(domain = "localhost", port = "8080")
private DemoHttpService demoHttpService;
}
/**
* @author kun
* @data 2022/1/22 14:40
*/
@Component
public class DemoConsumer {
@Resource
private DemoHttpService demoHttpService;
public String checkSuccess() {
return demoHttpService.checkSuccess("param1", "param2");
}
}
完整代码地址
https://github.com/wanggangkun/myFeign