Initial commit

This commit is contained in:
Eric
2026-01-16 18:51:16 +08:00
commit 98c057de11
280 changed files with 16665 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
<?xml version="1.0"?>
<project
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.lingniu.framework</groupId>
<artifactId>lingniu-framework-dependencies</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../../lingniu-framework-dependencies/pom.xml</relativePath>
</parent>
<artifactId>lingniu-framework-plugin-microservice-common</artifactId>
<name>lingniu-framework-plugin-microservice-common</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hystrix</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.lingniu.framework</groupId>
<artifactId>lingniu-framework-plugin-web</artifactId>
</dependency>
<dependency>
<groupId>cn.lingniu.framework</groupId>
<artifactId>lingniu-framework-plugin-core</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<version>3.1.6</version>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.3.4</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,22 @@
package cn.lingniu.framework.plugin.microservice.common.config;
import lombok.Data;
@Data
public class DefaultHttpProperties {
/**
* 每个route默认的最大连接数
*/
private Integer defaultMaxPerRoute = 25;
/**
* 从连接池中获取连接的超时时间超时间未拿到可用连接会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
*/
private Integer connectionRequestTimeout = 500;
/**
* 空闲永久连接检查间隔
*/
private Integer validateAfterInactivity = 2000;
}

View File

@@ -0,0 +1,18 @@
package cn.lingniu.framework.plugin.microservice.common.config;
import feign.Logger;
import lombok.Data;
@Data
public class FeignProperties {
private Logger.Level loggerLevel = Logger.Level.BASIC;
private Long period = 500L;
private Integer maxPeriod = 1000;
private Integer maxAttempts = 1;
}

View File

@@ -0,0 +1,47 @@
package cn.lingniu.framework.plugin.microservice.common.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@Data
@ConfigurationProperties(MircroServiceConfig.PRE_FIX)
public class MircroServiceConfig {
public static final String PRE_FIX = "framework.lingniu.spring.cloud";
/**
* 是否启用OkHttpClient
*/
private Boolean isOkHttp = true;
/**
* common
* 读取超时
*/
private Integer readTimeout = 1000;
/**
* common
* 连接超时
*/
private Integer connectTimeout = 1000;
/**
* common
* 整个连接池的最大连接数
*/
private Integer maxTotal = 150;
/**
* fegin配置
*/
private FeignProperties feign = new FeignProperties();
/**
* defaultHttp. 默认http配置
*/
private DefaultHttpProperties defaultHttp = new DefaultHttpProperties();
/**
* okHttp相关配置
*/
private OkHttpProperties okHttp = new OkHttpProperties();
}

View File

@@ -0,0 +1,30 @@
package cn.lingniu.framework.plugin.microservice.common.config;
import lombok.Data;
import java.util.concurrent.TimeUnit;
@Data
public class OkHttpProperties {
/**
* OkHttp TimeToLive
*/
private Long timeToLive = 2000L;
/**
* 时间单位
*/
private TimeUnit timeToLiveUnit = TimeUnit.MILLISECONDS;
/**
* 是否开启重定身
*/
private Boolean isFollowRedirects = true;
/**
* 是否开启SSL验证
*/
private Boolean disableSslValidation = false;
/**
* 写起时
*/
private Integer writeTimeout = 1000;
}

View File

@@ -0,0 +1,13 @@
package cn.lingniu.framework.plugin.microservice.common.core;
import cn.lingniu.framework.plugin.core.base.CommonResult;
/**
* @description: 异常解码器
**/
public interface ErrorCommonResultHandler {
CommonResult handler(String body);
}

View File

@@ -0,0 +1,122 @@
package cn.lingniu.framework.plugin.microservice.common.core;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* Implementation of {@link HttpMessageConverter} that can read and write strings.
*
* <p>By default, this converter supports all media types ({@code &#42;&#47;&#42;}),
* and writes with a {@code Content-Type} of {@code text/plain}. This can be overridden
* by setting the {@link #setSupportedMediaTypes supportedMediaTypes} property.
*
* @author Arjen Poutsma
* @author Juergen Hoeller
* @since 3.0
*/
public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
@Nullable
private volatile List<Charset> availableCharsets;
private boolean writeAcceptCharset = true;
/**
* A default constructor that uses {@code "ISO-8859-1"} as the default charset.
*
* @see #StringHttpMessageConverter(Charset)
*/
public StringHttpMessageConverter() {
this(DEFAULT_CHARSET);
}
/**
* A constructor accepting a default charset to use if the requested content
* type does not specify one.
*/
public StringHttpMessageConverter(Charset defaultCharset) {
super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL);
}
/**
* Indicates whether the {@code Accept-Charset} should be written to any outgoing request.
* <p>Default is {@code true}.
*/
public void setWriteAcceptCharset(boolean writeAcceptCharset) {
this.writeAcceptCharset = writeAcceptCharset;
}
@Override
public boolean supports(Class<?> clazz) {
return String.class == clazz;
}
@Override
protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException {
Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
return StreamUtils.copyToString(inputMessage.getBody(), charset);
}
@Override
protected Long getContentLength(String str, @Nullable MediaType contentType) {
Charset charset = getContentTypeCharset(contentType);
return (long) str.getBytes(charset).length;
}
@Override
protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {
if (this.writeAcceptCharset) {
outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets());
}
Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType());
StreamUtils.copy(str, charset, outputMessage.getBody());
}
/**
* Return the list of supported {@link Charset}s.
* <p>By default, returns {@link Charset#availableCharsets()}.
* Can be overridden in subclasses.
*
* @return the list of accepted charsets
*/
protected List<Charset> getAcceptedCharsets() {
List<Charset> charsets = this.availableCharsets;
if (charsets == null) {
charsets = new ArrayList<>(Charset.availableCharsets().values());
this.availableCharsets = charsets;
}
return charsets;
}
private Charset getContentTypeCharset(@Nullable MediaType contentType) {
if (contentType != null && contentType.getCharset() != null) {
return contentType.getCharset();
} else {
Charset charset = getDefaultCharset();
Assert.state(charset != null, "No default charset");
return charset;
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright © 2015-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.lingniu.framework.plugin.microservice.common.core.decoder;
import cn.lingniu.framework.plugin.core.base.CommonResult;
import cn.lingniu.framework.plugin.core.exception.ServerException;
import cn.lingniu.framework.plugin.core.exception.enums.GlobalErrorCodeConstants;
import cn.lingniu.framework.plugin.microservice.common.core.ErrorCommonResultHandler;
import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils;
import feign.Response;
import feign.Util;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
/**
* 异常解析
*/
@Slf4j
public class LingniuErrorDecoder extends ErrorDecoder.Default implements ErrorDecoder {
private final ErrorCommonResultHandler handler;
public LingniuErrorDecoder(ErrorCommonResultHandler errorCommonResultHandler) {
this.handler = errorCommonResultHandler;
}
@Override
public Exception decode(final String methodKey, final Response response) {
try {
if (HttpStatus.NOT_FOUND.value() == response.status() || ObjectEmptyUtils.isEmpty(response.body())) {
return new ServerException(GlobalErrorCodeConstants.NOT_FOUND);
} else if (HttpStatus.UNAUTHORIZED.value() == response.status()) {
return new ServerException(GlobalErrorCodeConstants.UNAUTHORIZED);
} else if (HttpStatus.METHOD_NOT_ALLOWED.value() == response.status()) {
return new ServerException(GlobalErrorCodeConstants.FORBIDDEN);
} else if (HttpStatus.TOO_MANY_REQUESTS.value() == response.status()) {
return new ServerException(GlobalErrorCodeConstants.TOO_MANY_REQUESTS);
} else if (HttpStatus.REQUEST_TIMEOUT.value() == response.status() || HttpStatus.GATEWAY_TIMEOUT.value() == response.status()) {
return new ServerException(GlobalErrorCodeConstants.TIME_OUT_ERROR);
} else if (HttpStatus.BAD_GATEWAY.value() == response.status()) {
return new ServerException(GlobalErrorCodeConstants.BAD_GATE_WAY_ERROR);
}
String body = Util.toString(response.body().asReader());
CommonResult result = handler.handler(body);
if (ObjectEmptyUtils.isNotEmpty(result) && result.getCode() != 0) {
if (log.isWarnEnabled()) {
log.warn("微服务调用: {} 响应异常信息:{} {} ", methodKey, result.getCode(), result.getMsg());
}
return new ServerException(result.getCode(), result.getMsg());
} else if (HttpStatus.valueOf(response.status()).is5xxServerError()) {
return new ServerException(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR);
} else {
return new ServerException(GlobalErrorCodeConstants.UNKNOWN);
}
} catch (Exception ex) {
log.error("Feign异常解码出现问题", ex);
return new ServerException(GlobalErrorCodeConstants.FEIGN_DECODE_ERROR);
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright © 2015-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.lingniu.framework.plugin.microservice.common.core.decoder;
import feign.FeignException;
import feign.Response;
import feign.codec.Decoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.LinkedList;
/**
* LingniuFeignDecoder
*/
@Slf4j
public class LingniuFeignDecoder implements Decoder {
private Decoder decoder;
public LingniuFeignDecoder(final Decoder decoder) {
this.decoder = decoder;
}
@Override
public Object decode(final Response response, final Type t) throws IOException, FeignException {
Type type = t;
if (isParameterizeHttpEntity(type)) {
type = ((ParameterizedType) type).getActualTypeArguments()[0];
Object decodedObject = decoder.decode(response, type);
return createResponse(decodedObject, response);
} else if (isHttpEntity(type)) {
return createResponse(null, response);
} else {
Object decodeResponse = decoder.decode(response, type);
return decodeResponse;
}
}
private boolean isParameterizeHttpEntity(final Type type) {
if (type instanceof ParameterizedType) {
return isHttpEntity(((ParameterizedType) type).getRawType());
}
return false;
}
@SuppressWarnings("rawtypes")
private boolean isHttpEntity(final Type type) {
if (type instanceof Class) {
Class c = (Class) type;
return HttpEntity.class.isAssignableFrom(c);
}
return false;
}
@SuppressWarnings("unchecked")
private <T> ResponseEntity<T> createResponse(final Object instance, final Response response) {
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
for (String key : response.headers().keySet()) {
headers.put(key, new LinkedList<>(response.headers().get(key)));
}
return new ResponseEntity<>((T) instance, headers, HttpStatus.valueOf(response.status()));
}
}

View File

@@ -0,0 +1,83 @@
package cn.lingniu.framework.plugin.microservice.common.core.interceptor;
import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
@Slf4j
public class CharlesRequestInterceptor implements RequestInterceptor {
private static ObjectMapper objectMapper;
static {
objectMapper = new ObjectMapper();
}
@Override
public void apply(RequestTemplate template) {
// feign 不支持 GET 方法传 POJO, json body转query
if (template.method().equals("GET") && ObjectEmptyUtils.isNotEmpty(template.body())) {
try {
JsonNode jsonNode = objectMapper.readTree(template.body());
template.body(null, Charset.defaultCharset());
Map<String, Collection<String>> queries = new HashMap<>();
buildQuery(jsonNode, "", queries);
template.queries(queries);
} catch (IOException e) {
log.error("CharlesRequestInterceptor异常", e);
}
}
}
private String encoderParameters(String parameterValue) {
try {
return URLEncoder.encode(parameterValue, "utf-8");
} catch (Exception ex) {
log.error("参数转换异常", ex);
}
return parameterValue;
}
private void buildQuery(JsonNode jsonNode, String path, Map<String, Collection<String>> queries) {
if (!jsonNode.isContainerNode()) {
if (jsonNode.isNull()) {
return;
}
Collection<String> values = queries.get(path);
if (null == values) {
values = new ArrayList<>();
queries.put(path, values);
}
values.add(encoderParameters(jsonNode.asText()));
return;
}
if (jsonNode.isArray()) {
Iterator<JsonNode> it = jsonNode.elements();
while (it.hasNext()) {
buildQuery(it.next(), path, queries);
}
} else {
Iterator<Map.Entry<String, JsonNode>> it = jsonNode.fields();
while (it.hasNext()) {
Map.Entry<String, JsonNode> entry = it.next();
if (ObjectEmptyUtils.isNotEmpty(path)) {
buildQuery(entry.getValue(), path + "." + entry.getKey(), queries);
} else {
buildQuery(entry.getValue(), entry.getKey(), queries);
}
}
}
}
}

View File

@@ -0,0 +1,19 @@
package cn.lingniu.framework.plugin.microservice.common.core.interceptor;
import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class FeignLoggerInterceptor implements RequestInterceptor {
public FeignLoggerInterceptor() {
}
@Override
public void apply(RequestTemplate requestTemplate) {
String bodyContent = ObjectEmptyUtils.isNotEmpty(requestTemplate.body()) ? new String(requestTemplate.body()) : "";
log.info("Feign调用日志:URL:{} Mehod:{} Body:{} ", requestTemplate.url(), requestTemplate.method(), bodyContent);
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2013-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package cn.lingniu.framework.plugin.microservice.common.init;
import cn.lingniu.framework.plugin.microservice.common.config.MircroServiceConfig;
import feign.Client;
import feign.httpclient.ApacheHttpClient;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.cloud.openfeign.loadbalancer.RetryableFeignBlockingLoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* DefaultFeignLoadBalancedConfiguration
*/
@Configuration
@ConditionalOnProperty(value = "framework.lingniu.spring.cloud.isOkHttp", havingValue = "false")
class DefaultFeignLoadBalancedConfiguration {
@Bean
public Client feignClient(MircroServiceConfig mircroServiceConfig,
LoadBalancerClient loadBalancerClient,
LoadBalancedRetryFactory loadBalancedRetryFactory,
LoadBalancerClientFactory loadBalancerClientFactory) {
ApacheHttpClient delegate = new ApacheHttpClient(httpClient(mircroServiceConfig));
return new RetryableFeignBlockingLoadBalancerClient(delegate,
loadBalancerClient, loadBalancedRetryFactory, loadBalancerClientFactory);
}
public HttpClient httpClient(MircroServiceConfig mircroServiceConfig) {
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", SSLConnectionSocketFactory.getSocketFactory())
.build();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
connectionManager.setMaxTotal(mircroServiceConfig.getMaxTotal());
connectionManager.setDefaultMaxPerRoute(mircroServiceConfig.getDefaultHttp().getDefaultMaxPerRoute());
connectionManager.setValidateAfterInactivity(mircroServiceConfig.getDefaultHttp().getValidateAfterInactivity());
RequestConfig requestConfig = RequestConfig.custom()
.setSocketTimeout(mircroServiceConfig.getReadTimeout())
.setConnectTimeout(mircroServiceConfig.getConnectTimeout())
.setConnectionRequestTimeout(mircroServiceConfig.getDefaultHttp().getConnectionRequestTimeout())
.build();
return HttpClientBuilder.create()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(connectionManager)
.build();
}
}

View File

@@ -0,0 +1,96 @@
package cn.lingniu.framework.plugin.microservice.common.init;
import cn.lingniu.framework.plugin.microservice.common.config.MircroServiceConfig;
import cn.lingniu.framework.plugin.microservice.common.core.StringHttpMessageConverter;
import cn.lingniu.framework.plugin.microservice.common.core.decoder.LingniuFeignDecoder;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.form.FormEncoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@ConditionalOnClass(name = {"com.alibaba.fastjson.support.config.FastJsonConfig"})
@ConditionalOnProperty(prefix = MircroServiceConfig.PRE_FIX, name = "json-converter", havingValue = "fastjson")
@Configuration
@AutoConfigureBefore({LingniuCloudAutoConfiguration.class})
public class HttpMessageFastJsonConverterConfiguration {
HttpMessageConverters converters = new HttpMessageConverters(createStringConverter(), createFastJsonConverter());
@Bean
public Decoder fastJsonFeignDecoder() {
return new LingniuFeignDecoder(new SpringDecoder(() -> converters));
}
@Bean
public Encoder fastJsonFeignEncoder() {
return new FormEncoder(new SpringEncoder(() -> converters));
}
private HttpMessageConverter createStringConverter() {
//fastConverter转换纯String有问题该处使用的是重写后的转换器防止被Spring优先级排到后面导致没有生效
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(StandardCharsets.UTF_8);
stringHttpMessageConverter.setWriteAcceptCharset(false);
return stringHttpMessageConverter;
}
private HttpMessageConverter createFastJsonConverter() {
//创建fastJson消息转换器
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
//升级最新版本需加=============================================================
List<MediaType> supportedMediaTypes = new ArrayList<>();
supportedMediaTypes.add(MediaType.APPLICATION_JSON);
supportedMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
supportedMediaTypes.add(MediaType.APPLICATION_ATOM_XML);
supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
supportedMediaTypes.add(MediaType.APPLICATION_PDF);
supportedMediaTypes.add(MediaType.APPLICATION_RSS_XML);
supportedMediaTypes.add(MediaType.APPLICATION_XHTML_XML);
supportedMediaTypes.add(MediaType.APPLICATION_XML);
supportedMediaTypes.add(MediaType.IMAGE_GIF);
supportedMediaTypes.add(MediaType.IMAGE_JPEG);
supportedMediaTypes.add(MediaType.IMAGE_PNG);
supportedMediaTypes.add(MediaType.TEXT_EVENT_STREAM);
supportedMediaTypes.add(MediaType.TEXT_HTML);
supportedMediaTypes.add(MediaType.TEXT_MARKDOWN);
supportedMediaTypes.add(MediaType.TEXT_PLAIN);
supportedMediaTypes.add(MediaType.TEXT_XML);
fastConverter.setSupportedMediaTypes(supportedMediaTypes);
//创建配置类
FastJsonConfig fastJsonConfig = new FastJsonConfig();
//修改配置返回内容的过滤
//WriteNullListAsEmpty List字段如果为null,输出为[],而非null
//WriteNullStringAsEmpty 字符类型字段如果为null,输出为"",而非null
//DisableCircularReferenceDetect 消除对同一对象循环引用的问题默认为false如果不配置有可能会进入死循环
//WriteNullBooleanAsFalseBoolean字段如果为null,输出为false,而非null
//WriteMapNullValue是否输出值为null的字段,默认为false
fastJsonConfig.setSerializerFeatures(
SerializerFeature.DisableCircularReferenceDetect,
SerializerFeature.WriteMapNullValue
);
fastConverter.setFastJsonConfig(fastJsonConfig);
return fastConverter;
}
}

View File

@@ -0,0 +1,171 @@
/*
* Copyright © 2015-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.lingniu.framework.plugin.microservice.common.init;
import cn.lingniu.framework.plugin.core.base.CommonResult;
import cn.lingniu.framework.plugin.core.config.CommonConstant;
import cn.lingniu.framework.plugin.microservice.common.config.MircroServiceConfig;
import cn.lingniu.framework.plugin.microservice.common.core.ErrorCommonResultHandler;
import cn.lingniu.framework.plugin.microservice.common.core.decoder.LingniuErrorDecoder;
import cn.lingniu.framework.plugin.microservice.common.core.decoder.LingniuFeignDecoder;
import cn.lingniu.framework.plugin.microservice.common.core.interceptor.CharlesRequestInterceptor;
import cn.lingniu.framework.plugin.microservice.common.core.interceptor.FeignLoggerInterceptor;
import cn.lingniu.framework.plugin.util.json.JsonUtil;
import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils;
import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig;
import feign.Client;
import feign.Feign;
import feign.Request;
import feign.Retryer;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import feign.form.FormEncoder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Scope;
import org.springframework.core.env.Environment;
import org.springframework.web.client.RestTemplate;
import java.util.Optional;
/**
* LingniuCloudAutoConfiguration配置
*/
@Configuration
@Slf4j
@EnableFeignClients(basePackages = {"cn.lingniu.framework.plugin.microservice.common", "cn.lingniu"})
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
@EnableConfigurationProperties({MircroServiceConfig.class})
@AutoConfigureAfter(FrameworkWebConfig.class)
@RequiredArgsConstructor
@Import({OkHttpFeignLoadBalancedConfiguration.class, DefaultFeignLoadBalancedConfiguration.class, HttpMessageFastJsonConverterConfiguration.class})
public class LingniuCloudAutoConfiguration implements EnvironmentAware {
private static final long PERIOD = 500L;
private static final int MAX_PERIOD = 1000;
private static final int MAX_ATTEMPTS = 2;
private final ObjectFactory<HttpMessageConverters> messageConverters;
private final MircroServiceConfig mircroServiceConfig;
private final FrameworkWebConfig frameworkWebConfig;
private Environment environment;
@Bean
public ErrorDecoder getLingniuErrorDecoder(ErrorCommonResultHandler errorCommonResultHandler) {
return new LingniuErrorDecoder(errorCommonResultHandler);
}
@Bean
@ConditionalOnProperty(prefix = MircroServiceConfig.PRE_FIX, name = "json-converter", havingValue = "jackson", matchIfMissing = true)
public Decoder getLingniuFeignDecoder() {
return new LingniuFeignDecoder(new SpringDecoder(messageConverters));
}
@Bean
@ConditionalOnProperty(prefix = MircroServiceConfig.PRE_FIX, name = "json-converter", havingValue = "jackson", matchIfMissing = true)
public Encoder getFormEncoder() {
return new FormEncoder(new SpringEncoder(messageConverters));
}
@Bean
@ConditionalOnMissingBean
public ErrorCommonResultHandler errorHandler() {
return (b) -> JsonUtil.json2Bean(b, CommonResult.class);
}
@Bean
public CharlesRequestInterceptor charlesRequestInterceptor() {
return new CharlesRequestInterceptor();
}
@Bean
public FeignLoggerInterceptor headersInterceptor() {
return new FeignLoggerInterceptor();
}
/**
* feignBuilder创建
*/
@Bean
@Scope("prototype")
public Feign.Builder feignBuilder(ErrorCommonResultHandler handler, Optional<Retryer> retryer, Client restClient, Request.Options options) {
Feign.Builder builder = Feign.builder()
.errorDecoder(new LingniuErrorDecoder(handler))
.options(options)
.logLevel(mircroServiceConfig.getFeign().getLoggerLevel())
.retryer(retryer.isPresent() ? retryer.get() : Retryer.NEVER_RETRY)
.client(restClient);
return builder;
}
@Bean
public Request.Options options() {
return new Request.Options(mircroServiceConfig.getConnectTimeout(), mircroServiceConfig.getReadTimeout());
}
/**
* todo 后续可扩展
* 创建 RestTemplate 实例
* @param restTemplateBuilder {@link RestTemplateAutoConfiguration#restTemplateBuilder}
*/
@Bean
@ConditionalOnMissingBean
@LoadBalanced
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder.build();
}
/**
* feignRetryer实例化
*
* @return feignRetryer实例
*/
@Bean
@ConditionalOnProperty(prefix = MircroServiceConfig.PRE_FIX, name = "feign-retryer", havingValue = "true", matchIfMissing = false)
public Retryer feignRetryer() {
return new Retryer.Default(ObjectEmptyUtils.isEmpty(mircroServiceConfig.getFeign().getPeriod()) ? PERIOD : mircroServiceConfig.getFeign().getPeriod(),
ObjectEmptyUtils.isEmpty(mircroServiceConfig.getFeign().getMaxPeriod()) ? MAX_PERIOD : mircroServiceConfig.getFeign().getMaxPeriod(),
ObjectEmptyUtils.isEmpty(mircroServiceConfig.getFeign().getMaxAttempts()) ? MAX_ATTEMPTS : mircroServiceConfig.getFeign().getMaxAttempts());
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
}

View File

@@ -0,0 +1,31 @@
package cn.lingniu.framework.plugin.microservice.common.init;
import cn.lingniu.framework.plugin.util.config.PropertyUtils;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
@Order(Integer.MIN_VALUE + 500)
public class LingniuCloudInit implements org.springframework.context.ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
ConfigurableEnvironment environment = applicationContext.getEnvironment();
PropertyUtils.setDefaultInitProperty("spring.main.allow-bean-definition-overriding", "true");
PropertyUtils.setDefaultInitProperty("spring.autoconfigure.exclude[0]", "com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure");
PropertyUtils.setDefaultInitProperty("spring.autoconfigure.exclude[1]", "org.springframework.boot.actuate.autoconfigure.redis.RedisHealthIndicatorAutoConfiguration");
PropertyUtils.setDefaultInitProperty("spring.autoconfigure.exclude[2]", "org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthIndicatorAutoConfiguration");
PropertyUtils.setDefaultInitProperty("spring.autoconfigure.exclude[3]", "org.springframework.boot.actuate.autoconfigure.elasticsearch.ElasticSearchRestHealthIndicatorAutoConfiguration");
if (environment.getProperty("framework.lingniu.spring.cloud.isOkHttp", "true").equalsIgnoreCase(Boolean.TRUE.toString())) {
PropertyUtils.setDefaultInitProperty("feign.httpclient.enabled", "false");
PropertyUtils.setDefaultInitProperty("feign.okhttp.enabled", "true");
} else {
PropertyUtils.setDefaultInitProperty("feign.httpclient.enabled", "true");
PropertyUtils.setDefaultInitProperty("feign.okhttp.enabled", "false");
}
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2013-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package cn.lingniu.framework.plugin.microservice.common.init;
import cn.lingniu.framework.plugin.microservice.common.config.MircroServiceConfig;
import feign.Client;
import feign.okhttp.OkHttpClient;
import okhttp3.ConnectionPool;
import okhttp3.EventListener;
import okhttp3.Interceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory;
import org.springframework.cloud.commons.httpclient.OkHttpClientFactory;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.cloud.openfeign.loadbalancer.RetryableFeignBlockingLoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PreDestroy;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* OkHttpFeignLoadBalancedConfiguration
*/
@Configuration
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnProperty(value = "framework.lingniu.spring.cloud.isOkHttp", havingValue = "true", matchIfMissing = true)
class OkHttpFeignLoadBalancedConfiguration {
@Bean
public Client feignClient(okhttp3.OkHttpClient okHttpClient,
LoadBalancerClient loadBalancerClient,
LoadBalancedRetryFactory loadBalancedRetryFactory,
LoadBalancerClientFactory loadBalancerClientFactory) {
return new RetryableFeignBlockingLoadBalancerClient(new OkHttpClient(okHttpClient), loadBalancerClient, loadBalancedRetryFactory, loadBalancerClientFactory);
}
@Configuration
protected static class OkHttpFeignConfiguration {
private okhttp3.OkHttpClient okHttpClient;
@Bean
public ConnectionPool httpClientConnectionPool(MircroServiceConfig mircroServiceConfig,
OkHttpClientConnectionPoolFactory connectionPoolFactory) {
return connectionPoolFactory.create(mircroServiceConfig.getMaxTotal(), mircroServiceConfig.getOkHttp().getTimeToLive(), mircroServiceConfig.getOkHttp().getTimeToLiveUnit());
}
@Bean
public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory, Optional<List<Interceptor>> okFeignInterceptors,
ConnectionPool connectionPool, MircroServiceConfig mircroServiceConfig) {
okhttp3.OkHttpClient.Builder builder = httpClientFactory.createBuilder(mircroServiceConfig.getOkHttp().getDisableSslValidation()).
connectTimeout(mircroServiceConfig.getConnectTimeout(), TimeUnit.MILLISECONDS)
.readTimeout(mircroServiceConfig.getReadTimeout(), TimeUnit.MILLISECONDS)
.writeTimeout(mircroServiceConfig.getOkHttp().getWriteTimeout(), TimeUnit.MILLISECONDS)
.followRedirects(mircroServiceConfig.getOkHttp().getIsFollowRedirects())
.connectionPool(connectionPool);
okFeignInterceptors.ifPresent(n -> {n.forEach(builder::addInterceptor);});
this.okHttpClient = builder.build();
return okHttpClient;
}
@PreDestroy
public void destroy() {
if (okHttpClient != null) {
okHttpClient.dispatcher().executorService().shutdown();
okHttpClient.connectionPool().evictAll();
}
}
}
}

View File

@@ -0,0 +1,4 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.lingniu.framework.plugin.microservice.common.init.LingniuCloudAutoConfiguration
org.springframework.context.ApplicationContextInitializer=\
cn.lingniu.framework.plugin.microservice.common.init.LingniuCloudInit

View File

@@ -0,0 +1,55 @@
<?xml version="1.0"?>
<project
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.lingniu.framework</groupId>
<artifactId>lingniu-framework-dependencies</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../../lingniu-framework-dependencies/pom.xml</relativePath>
</parent>
<artifactId>lingniu-framework-plugin-microservice-nacos</artifactId>
<name>lingniu-framework-plugin-microservice-nacos</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<artifactId>nacos-client</artifactId>
<groupId>com.alibaba.nacos</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<artifactId>nacos-client</artifactId>
<groupId>com.alibaba.nacos</groupId>
</dependency>
<dependency>
<groupId>cn.lingniu.framework</groupId>
<artifactId>lingniu-framework-plugin-core</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>cn.lingniu.framework</groupId>
<artifactId>lingniu-framework-plugin-microservice-common</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxjava</artifactId>
<version>1.3.8</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,72 @@
# 框架核心web应用模块
## 概述 (Overview)
1. 定位: 基于 Alibaba Nacos 封装,支持服务注册、发现、多中心注册等功能的自动化组件
2. 核心能力:
* 支持多中心注册与服务发现
3. 适用场景:
* 公司内部微服务统一注册中心
* 与 FeignClient 配合实现服务间调用
## 如何配置
```yaml
spring:
application:
name: lingniu-framework-demo
profiles:
active: dev
cloud:
nacos:
enabled: true #开启微服务自定义注册自动获取当前框架信息启动时间等到metadata中
discovery:
server-addr: http://nacos-uat-new-inter.xx.net.cn:8848 #注册中心地址
username: nacos_test #注册中心用户名
password: nacos_test #注册中心密码
```
## 接口提供端
```java
@RestController
@RequestMapping
public class GithubApiController {
@GetMapping("/repos/{owner}/{repo}/contributors")
public CommonResult<List<Contributor>> contributors(@PathVariable("owner") String owner, @PathVariable("repo") String repo) {
// 模拟返回贡献者列表
Contributor contributor1 = new Contributor("user1", 10);
Contributor contributor2 = new Contributor("user2", 5);
List<Contributor> response = new ArrayList<>();
response.add(contributor1);
response.add(contributor2);
return CommonResult.success(response);
}
}
```
## 接口调用方
```java
// name = 接口提供方的应用名
@FeignClient(name = "lingniu-framework-provider-demo"
//外部接口配置url: @FeignClient(name = "github-api", url = "https://api.github.com"
// 方式1 配置fallback
// , fallback = GithubApiClientFallBack.class
// 方式2 配置fallbackFactory
// , fallbackFactory = GithubApiClientFallbackFactory.class
)
public interface DemoFeignClient {
@GetMapping("/repos/{owner}/{repo}/contributors")
CommonResult<List<Contributor>> contributors(@PathVariable("owner") String owner, @PathVariable("repo") String repo);
@GetMapping("/reposNotFound")
CommonResult<List<Contributor>> reposNotFound(@RequestParam("owner") String owner, @RequestParam("repo") String repo);
}
```