本文介绍用不了spring cloud的低版本系统如何借鉴cloud openfeign的封装思想来封装Feign,也能在多个工程以同样的接口申明方式来调用远程接口。
OpenFeign源于Netflix的Feign,是http通信的客户端。屏蔽了网络通信的细节,直接面向接口的方式开发,让开发者感知不到网络通信细节。 所有远程调用,都像调用本地方法一样完成!
封装的关键是OpenFeign 这两个注解,我们后续就是围绕这两个注解来封装
@FeignClient |
该注解用于通知 OpenFeign 组件对 @RequestMapping 注解下的接口进行解析,并通过动态代理的方式产生实现类,实现负载均衡和服务调用。 |
@EnableFeignClients |
该注解用于开启 OpenFeign 功能(一般使用在SpringBoot启动类上),当 Spring Cloud 应用启动时,OpenFeign 会扫描标有 @FeignClient 注解的接口,生成代理并注册到 Spring 容器中。 |
注解声明,面向接口:
/**
* REST 声明式规范(即支持的注解)有以下几种:Feign、JAX-RS 1/2、Spring Web MVC 都需要进行适配。这几种声明式注解的适配接口是 feign.Contract
* 这里选的是JAX-RS 1/2
*/
@FeignClient(name ="InnerOperatorService", path = "business/inner-invoke/", url = "${rootServiceUrl}", primary = false, contextId = "jaxrs")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Path("/operator")
public interface IInnerOperatorService {
@POST
@Path("/login/{code}/{password}")
Response login(@PathParam("code") String code, @PathParam("password")String password);
}
启动类配置
@SpringBootApplication(scanBasePackages ={"com.liangmixian.selfserviceApp"})
@EnableCaching
@EnableFeignClients({"com.liangmixian.sms.business.api.service"})
@EnableConfigurationProperties({LoginMap.class, AuthSmsParamsConfig.class})
public class FsaApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(FsaApplication.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(FsaApplication.class);
}
}
替换默认的底层通信
默认远程访问协议为jdk自带的sun.net.www.protocol.http.HttpURLConnection效率低,可改为okHttp
application.properties 配置,关闭默认的sun.net.www.protocol.http.HttpURLConnection,开启okhttp
feign.httpclient.enable=false
feign.okhttp.enable=true
超时控制:
OpenFeign默认超时时间为1s,超过1s就会返回错误页面。如果我们的接口处理业务确实超过1s,就需要对接口进行超时配置,如下:
ribbon: #设置feign客户端连接所用的超时时间,适用于网络状况正常情况下,两端连接所用时间
ReadTimeout: 1000 #指的是建立连接所用时间
ConnectTimeout: 1000 #指建立连接后从服务读取到可用资源所用时间
工作原理分析(这是我们后续封装工作的核心)
SpringBoot 应用启动时, 由针对 @EnableFeignClient 这一注解的处理逻辑触发程序扫描 classPath中所有被@FeignClient 注解的类, 将这些类解析为 BeanDefinition 并生成bean注册到 Spring 容器中,当业务请求真实发生时,Spring 会尝试从容器中查找创建好的动态代理对象,最终的调用被统一转发到了由 Feign 框架实现的 InvocationHandler 中, InvocationHandler 负责将接口中的入参转换为 HTTP 的形式, 发到服务端, 最后再解析 HTTP 响应, 将结果转换为 Java 对象, 予以返回。
扫描@FeignClient注解的接口并注册到Spring容器:
class FeignClientsRegistrar
implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
// patterned after Spring Integration IntegrationComponentScanRegistrar
// and RibbonClientsConfigurationRegistgrar
private ResourceLoader resourceLoader;
private Environment environment;
FeignClientsRegistrar() {
}
static void validateFallback(final Class clazz) {
Assert.isTrue(!clazz.isInterface(),
"Fallback class must implement the interface annotated by @FeignClient");
}
static void validateFallbackFactory(final Class clazz) {
Assert.isTrue(!clazz.isInterface(), "Fallback factory must produce instances "
+ "of fallback classes that implement the interface annotated by @FeignClient");
}
static String getName(String name) {
if (!StringUtils.hasText(name)) {
return "";
}
String host = null;
try {
String url;
if (!name.startsWith("http://") && !name.startsWith("https://")) {
url = "http://" + name;
}
else {
url = name;
}
host = new URI(url).getHost();
}
catch (URISyntaxException e) {
}
Assert.state(host != null, "Service id not legal hostname (" + name + ")");
return name;
}
static String getUrl(String url) {
if (StringUtils.hasText(url) && !(url.startsWith("#{") && url.contains("}"))) {
if (!url.contains("://")) {
url = "http://" + url;
}
try {
new URL(url);
}
catch (MalformedURLException e) {
throw new IllegalArgumentException(url + " is malformed", e);
}
}
return url;
}
static String getPath(String path) {
if (StringUtils.hasText(path)) {
path = path.trim();
if (!path.startsWith("/")) {
path = "/" + path;
}
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
}
return path;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}
private void registerDefaultConfiguration(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
Map defaultAttrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
String name;
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
}
else {
name = "default." + metadata.getClassName();
}
registerClientConfiguration(registry, name,
defaultAttrs.get("defaultConfiguration"));
}
}
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
LinkedHashSet candidateComponents = new LinkedHashSet<>();
Map attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
final Class>[] clients = attrs == null ? null
: (Class>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
Set basePackages = getBasePackages(metadata);
for (String basePackage : basePackages) {
candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
}
}
else {
for (Class> clazz : clients) {
candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
}
}
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// verify annotated class is an interface
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
Map attributes = annotationMetadata
.getAnnotationAttributes(FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
registerClientConfiguration(registry, name,
attributes.get("configuration"));
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map attributes) {
String className = annotationMetadata.getClassName();
Class clazz = ClassUtils.resolveClassName(className, null);
ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
? (ConfigurableBeanFactory) registry : null;
String contextId = getContextId(beanFactory, attributes);
String name = getName(attributes);
FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
factoryBean.setBeanFactory(beanFactory);
factoryBean.setName(name);
factoryBean.setContextId(contextId);
factoryBean.setType(clazz);
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(clazz, () -> {
factoryBean.setUrl(getUrl(beanFactory, attributes));
factoryBean.setPath(getPath(beanFactory, attributes));
factoryBean.setDecode404(Boolean
.parseBoolean(String.valueOf(attributes.get("decode404"))));
Object fallback = attributes.get("fallback");
if (fallback != null) {
factoryBean.setFallback(fallback instanceof Class
? (Class>) fallback
: ClassUtils.resolveClassName(fallback.toString(), null));
}
Object fallbackFactory = attributes.get("fallbackFactory");
if (fallbackFactory != null) {
factoryBean.setFallbackFactory(fallbackFactory instanceof Class
? (Class>) fallbackFactory
: ClassUtils.resolveClassName(fallbackFactory.toString(),
null));
}
return factoryBean.getObject();
});
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
definition.setLazyInit(true);
validate(attributes);
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);
// has a default, won't be null
boolean primary = (Boolean) attributes.get("primary");
beanDefinition.setPrimary(primary);
String[] qualifiers = getQualifiers(attributes);
if (ObjectUtils.isEmpty(qualifiers)) {
qualifiers = new String[] { contextId + "FeignClient" };
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
qualifiers);
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
private void validate(Map attributes) {
AnnotationAttributes annotation = AnnotationAttributes.fromMap(attributes);
// This blows up if an aliased property is overspecified
// FIXME annotation.getAliasedString("name", FeignClient.class, null);
validateFallback(annotation.getClass("fallback"));
validateFallbackFactory(annotation.getClass("fallbackFactory"));
}
/* for testing */ String getName(Map attributes) {
return getName(null, attributes);
}
String getName(ConfigurableBeanFactory beanFactory, Map attributes) {
String name = (String) attributes.get("serviceId");
if (!StringUtils.hasText(name)) {
name = (String) attributes.get("name");
}
if (!StringUtils.hasText(name)) {
name = (String) attributes.get("value");
}
name = resolve(beanFactory, name);
return getName(name);
}
private String getContextId(ConfigurableBeanFactory beanFactory,
Map attributes) {
String contextId = (String) attributes.get("contextId");
if (!StringUtils.hasText(contextId)) {
return getName(attributes);
}
contextId = resolve(beanFactory, contextId);
return getName(contextId);
}
private String resolve(ConfigurableBeanFactory beanFactory, String value) {
if (StringUtils.hasText(value)) {
if (beanFactory == null) {
return this.environment.resolvePlaceholders(value);
}
BeanExpressionResolver resolver = beanFactory.getBeanExpressionResolver();
String resolved = beanFactory.resolveEmbeddedValue(value);
if (resolver == null) {
return resolved;
}
return String.valueOf(resolver.evaluate(resolved,
new BeanExpressionContext(beanFactory, null)));
}
return value;
}
private String getUrl(ConfigurableBeanFactory beanFactory,
Map attributes) {
String url = resolve(beanFactory, (String) attributes.get("url"));
return getUrl(url);
}
private String getPath(ConfigurableBeanFactory beanFactory,
Map attributes) {
String path = resolve(beanFactory, (String) attributes.get("path"));
return getPath(path);
}
protected ClassPathScanningCandidateComponentProvider getScanner() {
return new ClassPathScanningCandidateComponentProvider(false, this.environment) {
@Override
protected boolean isCandidateComponent(
AnnotatedBeanDefinition beanDefinition) {
boolean isCandidate = false;
if (beanDefinition.getMetadata().isIndependent()) {
if (!beanDefinition.getMetadata().isAnnotation()) {
isCandidate = true;
}
}
return isCandidate;
}
};
}
protected Set getBasePackages(AnnotationMetadata importingClassMetadata) {
Map attributes = importingClassMetadata
.getAnnotationAttributes(EnableFeignClients.class.getCanonicalName());
Set basePackages = new HashSet<>();
for (String pkg : (String[]) attributes.get("value")) {
if (StringUtils.hasText(pkg)) {
basePackages.add(pkg);
}
}
for (String pkg : (String[]) attributes.get("basePackages")) {
if (StringUtils.hasText(pkg)) {
basePackages.add(pkg);
}
}
for (Class> clazz : (Class[]) attributes.get("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}
if (basePackages.isEmpty()) {
basePackages.add(
ClassUtils.getPackageName(importingClassMetadata.getClassName()));
}
return basePackages;
}
private String getQualifier(Map client) {
if (client == null) {
return null;
}
String qualifier = (String) client.get("qualifier");
if (StringUtils.hasText(qualifier)) {
return qualifier;
}
return null;
}
private String[] getQualifiers(Map client) {
if (client == null) {
return null;
}
List qualifierList = new ArrayList<>(
Arrays.asList((String[]) client.get("qualifiers")));
qualifierList.removeIf(qualifier -> !StringUtils.hasText(qualifier));
if (qualifierList.isEmpty() && getQualifier(client) != null) {
qualifierList = Collections.singletonList(getQualifier(client));
}
return !qualifierList.isEmpty() ? qualifierList.toArray(new String[0]) : null;
}
private String getClientName(Map client) {
if (client == null) {
return null;
}
String value = (String) client.get("contextId");
if (!StringUtils.hasText(value)) {
value = (String) client.get("value");
}
if (!StringUtils.hasText(value)) {
value = (String) client.get("name");
}
if (!StringUtils.hasText(value)) {
value = (String) client.get("serviceId");
}
if (StringUtils.hasText(value)) {
return value;
}
throw new IllegalStateException("Either 'name' or 'value' must be provided in @"
+ FeignClient.class.getSimpleName());
}
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
Object configuration) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientSpecification.class);
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
registry.registerBeanDefinition(
name + "." + FeignClientSpecification.class.getSimpleName(),
builder.getBeanDefinition());
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
}
生成代理对象的FactoryBean:
public class FeignClientFactoryBean implements FactoryBean
封装之前,先对比看下引入feign会在代码量上改观多少,再说引入会遇到什么问题且每个是怎么解决的,最后介绍使用注意事项。
HttpUtils调用方式参见如下代码,设置传参组装请求并接收返回值,其中每一步都靠手动组装,且每次调用都靠手动:
public interface ICenterProxyService {
CenterResponse sendCode(String phone);
CenterResponse checkSmsCode(String phone,String code);
}
public class CenterProxyService implements ICenterProxyService {
@Override
public CenterResponse sendCode(String phone){
String smsUrl = getCenterUrl()+CenterProxyConstant.CENTER_PREFIX+CenterProxyConstant.SMS_URL;
Map params = new HashMap();
params.put("phone", phone);
return doPostForm(params,smsUrl);
}
public CenterResponse checkSmsCode(String phone,String code){
String smsUrl = getCenterUrl()+CenterProxyConstant.CENTER_PREFIX+CenterProxyConstant.CHECK_SMS_CODE_URL;
Map params = new HashMap();
params.put("phone", phone);
params.put("code", code);
return doPostForm(params,smsUrl);
}
private String getCenterUrl() {
SysParameter p = sysParameterDao.findSysParameter(CenterProxyConstant.PARAMCODE);
return p.getValue() == null || "".equals(p.getValue()) ? p.getDefaultValue() : p.getValue();
}
private CenterResponse doPostForm(Map params,String url){
logger.info("# url #"+url);
logger.info("# params #"+params);
CenterResponse centerResponse = new CenterResponse();
String result = "";
try {
result = HttpUtils.postForm(url, params);
logger.info("# doPost result #"+result);
setResult(centerResponse,result);
} catch (Exception e) {
e.printStackTrace();
logger.error(e);
centerResponse.setSuccess(false);
centerResponse.setMsg(e.getMessage());
}
return centerResponse;
}
private CenterResponse doPost(String json,String url){
logger.info("# url #"+url);
logger.info("# json #"+json);
CenterResponse centerResponse = new CenterResponse();
String result = "";
try {
result = HttpUtils.postJson(url,json.getBytes(Charset.forName("UTF-8")));
logger.info("# doPost result #"+result);
setResult(centerResponse,result);
} catch (Exception e) {
e.printStackTrace();
logger.error(e);
centerResponse.setSuccess(false);
centerResponse.setMsg(e.getMessage());
}
return centerResponse;
}
private void setResult(CenterResponse centerResponse,String result){
logger.info("# center.return.values #"+result);
Map map= JsonConvertUtils.getObject(result, Map.class);
if(map==null){
return ;
}
Object success = map.get("success");
Object message = map.get("msg");
if ( null == success ||(Boolean)success.equals(Boolean.FALSE)) {
centerResponse.setSuccess(false);
centerResponse.setMsg(message+"");
return;
}
Map jsonDataMap = (Map) map.get("jsonData");
Object codeResult = jsonDataMap.get("code");
Object msgResult = jsonDataMap.get("msg");
if (CenterProxyConstant.SUCCESS_CODE_0.equals((String)codeResult) || CenterProxyConstant.SUCCESS_CODE_1000.equals((String)codeResult)) {
centerResponse.setSuccess(true);
}else {
centerResponse.setSuccess(false);
centerResponse.setMsg(msgResult+"");
}
}
public ISysParameterDao getSysParameterDao() {
return sysParameterDao;
}
public void setSysParameterDao(ISysParameterDao sysParameterDao) {
this.sysParameterDao = sysParameterDao;
}
HttpUtils最终会调用这个请求构建并发送的方法,可以看到我们之前封装的很漂亮但很辛苦:
public static String sendRequest(String url, String method, Map headers, InputStream bodyStream, File saveToFile) throws Exception {
assertUrlValid(url);
HttpURLConnection conn = null;
try {
// 打开链接
URL urlObj = new URL(url);
conn = (HttpURLConnection) urlObj.openConnection();
// 设置各种默认属性
setDefaultProperties(conn);
// 设置请求方法
if (method != null && method.length() > 0) {
conn.setRequestMethod(method);
}
// 添加请求头
if (headers != null && headers.size() > 0) {
for (Map.Entry entry : headers.entrySet()) {
conn.setRequestProperty(entry.getKey(), entry.getValue());
}
}
// 设置请求内容
if (bodyStream != null) {
conn.setDoOutput(true);
copyStreamAndClose(bodyStream, conn.getOutputStream());
}
// 获取响应code
int code = conn.getResponseCode();
logger.info("# code #"+code);
// 处理重定向
if (code == HttpURLConnection.HTTP_MOVED_PERM || code == HttpURLConnection.HTTP_MOVED_TEMP) {
String location = conn.getHeaderField("Location");
if (location != null) {
closeStream(bodyStream);
// 重定向为 GET 请求
return sendRequest(location, "GET", headers, null, saveToFile);
}
}
// 获取响应内容长度
// long contentLength = conn.getContentLengthLong();
long contentLength = conn.getContentLength();
logger.info("# contentLength #"+contentLength);
// 获取内容类型
String contentType = conn.getContentType();
logger.info("# contentType #"+contentType);
// 获取响应内容输入流
InputStream in = conn.getInputStream();
// 没有响应成功, 均抛出异常
if (code != HttpURLConnection.HTTP_OK) {
throw new IOException("Http Error: " + code + "; Desc: " + handleResponseBodyToString(in, contentType));
}
// 如果文件参数不为null, 则把响应内容保存到文件
if (saveToFile != null) {
handleResponseBodyToFile(in, saveToFile);
return saveToFile.getPath();
}
// 如果需要将响应内容解析为文本, 则限制最大长度
if (contentLength > TEXT_REQUEST_MAX_LENGTH) {
throw new IOException("Response content length too large: " + contentLength);
}
return handleResponseBodyToString(in, contentType);
} finally {
closeConnection(conn);
}
}
feign调用方式参见代码,设置传参组装请求并接收返回值,其中每一步都靠接口注解声明:
@liangmixianFeignClient(urlPara="center.ip.and.port",path = "/appIntf")
@Consumes(MediaType.APPLICATION_JSON)
@Path("/sms")
public interface IAuthCenterProxyService {
@POST
@Path("/sendCode")
AuthCenterResponse sendCode(@QueryParam("phone")String phone);
@POST
@Path("/checkSmsCode")
AuthCenterResponse checkSmsCode(@QueryParam("phone")String phone,@QueryParam("code")String code);
}
所以说单纯从工作量来看,引入feign肯定是划算多了,还不说feign封装能做性能改进这些优化工作
只在一个工程中简单用spring上下文扫描feign接口是扫不全的
OSGI架构中不同bundle中的跨bundle扫描feign接口需要每个bundle上下文提前扫,类似BeanFactoryHolder,每个Bundle都要执行,才能收集到所有bundle中的feign接口,
(每个bundle都有自己的spring 容器,所以说FeignClientsRegistrar简单加个@Component肯定不行)
feign接口的扫描注册器放哪个工程合适呢?
一开始想把注册器FeignClientsRegistrar放在core工程(这样可以访问dao_interface),其他工程bundle也想执行扫描是找不到这个类的,特别是接口机,那就试试放到common工程里,这样每个bundle都能找到,如果没依赖common依赖下就能用,比如innerintf_interface就没有依赖common
放common中还想读数据库中的系统参数,common又不能依赖dao_interface啊
获取ISysParameterDao接口的实例并调用方法findSysParameter,将找不到ISysParameterDao和SysParameter,报ClassNotFoundException异常,dao_interface已经依赖了common,不能再反过来依赖,那就用反射调用方法,这样就避开依赖限制;
请求地址配置在系统参数中,那启动阶段生成feign代理对象时拿不到系统参数值的,因为DAO实例还没生成
提前扫出来构建feign代理类需要通过系统参数查询url,但是启动阶段还调用不了DAO,那就想办法在运行阶段再查系统参数:JDK动态代理生成一个空的代理对象,在运行期调用invoke方法再生成可用的feign代理类并进行调用
调用方和服务方传参解析的问题
UnrecognizedPropertyException: Unrecognized field "count" (class com….AuthCenterResponse), not marked as ignorable (3 known properties: "msg", "jsonData", "success"]),这是主应用调用中间层进行短信验证,如果成功时中间层不会返回"msg",异常时才返回,对方可能少属性,且"count"对于调用方是不会用也不该存在,也就是两边属性肯定不一致,那就这么配置下
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);
表示反序列化忽略不认识的属性,就像解决spring 的MappingJackson2HttpMessageConverter也可以这么设置,这样不用在每个DTO上增加注解@JsonIgnoreProperties(ignoreUnknown = true)
OkHttp依赖包需要转化
有这么个依赖链条:feign okhttp===>okhttp===>okio===>animal-sniffer-annotations,
而okio只是简单的jar包,参见这里https://mvnrepository.com/artifact/com.squareup.okio/okio/1.6.0,每个版本都不是bundle类型,所以需要转成bundle,只需改下MENIFEST.MF文件即可,另外okio又依赖animal-sniffer-annotations,且也不是bundle包,那就把animal-sniffer-annotations的class打进okio-1.6.0.jar里边
httpinvoker客户端不能直接引用并调用feign接口
如果是后台任务、controller这些地方调用feign接口,解决完上述几个问题其实已经可以使用,最后剩个问题:client端通过OsgiHttpInvokerServiceWithHandlerExporter去找server_开头的服务对象实例,找到的却是JdkDynamicAopProxy对象,调proxy对象的sendCode方法就会报NoSuchMethodException异常,所以针对这个问题只能再增加一层服务专门处理httpinvoker调用,这层服务再调用feign接口即可
多加一层调用:
public class AuthCenterProxyService implements ICenterProxyService {
private Log logger = LogFactory.getLog(getClass());
@Override
public CenterResponse sendCode(String phone) {
AuthCenterResponse result = getAuthCenterProxyService().sendCode(phone);
return getCenterResponse(result);
}
@Override
public CenterResponse checkSmsCode(String phone, String code) {
AuthCenterResponse result = getAuthCenterProxyService().checkSmsCode(phone, code);
return getCenterResponse(result);
}
public IAuthCenterProxyService getAuthCenterProxyService() {
return BeanFactoryHolder.get().getBean("authCenterProxyService",IAuthCenterProxyService.class);
}
private CenterResponse getCenterResponse(AuthCenterResponse result){
CenterResponse centerResponse = new CenterResponse();
logger.info("# center.return.values #"+result);
if (!result.getSuccess()) {
centerResponse.setSuccess(false);
centerResponse.setMsg(result.getMsg());
return centerResponse;
}
String codeResult = result.getJsonData().getCode();
String msgResult = result.getJsonData().getMsg();
if (CenterProxyConstant.SUCCESS_CODE_0.equals((String)codeResult) || CenterProxyConstant.SUCCESS_CODE_1000.equals((String)codeResult)) {
centerResponse.setSuccess(true);
}else {
centerResponse.setSuccess(false);
centerResponse.setMsg(msgResult);
}
return centerResponse;
}
}
最后我们看下解决了上述几个问题的底层封装代码:
@FeignClient注解有没有很眼熟,就类似spring cloud实现的@FeignClient
/**
* 标记feign接口,据此生成feign调用代理对象
*/
@Target({ ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface FeignClient {
//系统参数配置
int SYS_PARAMETER = 0;
//本地文件配置
int LOCAL_PROPERTIES = 1;
//直接指定,本身就是url地址
int ITSELF = 2;
/**
* url根路径配置项
*/
String urlPara();
/**
* url根路径之后首先拼接的路径,用于多个接口的请求根路径一样但各自子路径不一样的情况
*/
String path() default "";
/**
* url参数配置类型,默认用系统参数配置
*/
int urlParaType() default SYS_PARAMETER;
/**
* 针对urlParaType配置为1的情况,需要指定properties配置文件的路径
*/
String propertiesFile() default "";
/**
* 连接超时时间,单位为秒
* 默认60秒
*/
int connectTimeout() default 60;
/**
* 读取超时时间,单位为秒
* 默认60秒
*/
int readTimeout() default 30;
}
FeignClientsRegistrar眼熟吧,叫法也跟spring cloud封装一样,只不过里边内容很恨很不一样
public class FeignClientsRegistrar implements BeanFactoryPostProcessor,ResourceLoaderAware, ApplicationContextAware, BundleContextAware, InitializingBean{
private List basePackages = Arrays.asList("com.liangmixian.sms.**.feign");
private static final String DEFAULT_RESOURCE_PATTERN = "*.class";
private MetadataReaderFactory metadataReaderFactory;
// private ResourcePatternResolver resourcePatternResolver;//在osgi架构中不能扫描到别的bundle,故不能用
private ApplicationContext applicationContext;
private List bundleApplicationContexts = new ArrayList();
private BundleContext bundleContext;
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// 获取指定包下面的类
for (String basePackage : basePackages) {
Set> beanClasses = scannerPackages(basePackage );
for (Class> beanClass : beanClasses) {
//要求是feign client接口,且包含注解@EnableFeignClient
if (!beanClass .isInterface() || !beanClass.isAnnotationPresent(FeignClient.class)) {
continue;
}
FeignClient enableFeignAnnotation = (FeignClient) beanClass.getAnnotation(FeignClient.class);
// BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(beanClass);
// 在这里,我们可以给该对象的属性注入对应的实例。类似mybatis,就在这里注入了dataSource和sqlSessionFactory,
FeignClientFactoryBean> factoryBean = new FeignClientFactoryBean(beanClass);
factoryBean.setEnableFeignClient(enableFeignAnnotation);
try {
String simpleName = beanClass.getSimpleName();
if (simpleName.startsWith("I")) {
simpleName = simpleName.substring(1);
}
String beanName = simpleName.substring(0, 1).toLowerCase() + simpleName.substring(1);
beanFactory.registerSingleton(beanName, factoryBean.getObject());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
/**
* 根据包路径获取包及子包下的所有类
*
* @param basePackage
* basePackage
* @return Set> Set>
*/
private Set> scannerPackages(String basePackage) {
Set> set = new LinkedHashSet>();
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + resolveBasePackage(basePackage) + '/' + DEFAULT_RESOURCE_PATTERN;
for (ApplicationContext ac : bundleApplicationContexts) {
try {
Resource[] resources = ac.getResources(packageSearchPath);
for (Resource resource : resources) {
if (resource.isReadable()) {
MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
String className = metadataReader.getClassMetadata().getClassName();
Class> clazz;
try {
clazz = ac.getClassLoader().loadClass(className);
set.add(clazz);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
return set;
}
protected String resolveBasePackage(String basePackage) {
return ClassUtils.convertClassNameToResourcePath(applicationContext.getEnvironment().resolveRequiredPlaceholders(basePackage));
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
// this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
}
@Override
public void setBundleContext(BundleContext bundleContext) {
this.bundleContext = bundleContext;
}
@Override
public void afterPropertiesSet() throws Exception {
ServiceReference[] srs = null;
try {
srs = bundleContext.getAllServiceReferences("org.springframework.context.ApplicationContext", null);
} catch (InvalidSyntaxException e) {
throw new RuntimeException(e);
}
if (srs != null) {
for (ServiceReference sr : srs) {
bundleApplicationContexts.add((ApplicationContext) bundleContext.getService(sr));
}
}
}
}
FeignClientFactoryBean类名也跟spring cloud封装的一样,不过不一样的是只生成Feig.Builder对象不生成feign接口的代理对象,为什么呢里边注释有说
/**
* 接口实例工厂,这里主要是用于提供接口的实例对象
*/
public class FeignClientFactoryBean implements FactoryBean {
private Class interfaceType;
private FeignClient enableFeignAnnotation;
public FeignClientFactoryBean(Class interfaceType) {
this.interfaceType = interfaceType;
}
@Override
public T getObject() throws Exception {
// 这里主要是创建接口对应的实例,便于注入到spring容器中
/* 这里直接生成feign接口的代理对象是不行的,因为启动阶段还调用不了系统参数获取其配置的url,spring cloud的openfeign在这一步就生成Feign.Builder那是获取的配置文件
T proxy = getFeignBuilder().target(interfaceType, getUrl());
return (T) proxy;*/
InvocationHandler handler = new FeignClientInvocationHandler(interfaceType,enableFeignAnnotation, getFeignBuilder());
return (T) Proxy.newProxyInstance(interfaceType.getClassLoader(),
new Class[] {interfaceType},handler);
}
@Override
public Class getObjectType() {
return interfaceType;
}
@Override
public boolean isSingleton() {
return true;
}
public void setEnableFeignClient(FeignClient enableFeignAnnotation) {
this.enableFeignAnnotation = enableFeignAnnotation;
}
private Builder getFeignBuilder(){
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);
return Feign.builder().client(new OkHttpClient())
.contract(new JAXRSContract())
.encoder(new JacksonEncoder(mapper))
.decoder(new JacksonDecoder(mapper))
.errorDecoder(new FeignExceptionDecoder())
.logLevel(Level.BASIC)
.options(new Request.Options(feignClientParas.connectTimeout() * 1000, feignClientParas.readTimeout() * 1000))
.retryer(new Retryer.Default(50000, 5000, 3))
.requestInterceptor(new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
ApplicationSession applicationSession = ApplicationSessionHolder.getApplicationSession();
if (applicationSession != null) {
template.header("sessionId",applicationSession.getId());
}
}
});
}
}
FeignClientInvocationHandler才生成Feig接口的代理对象,在这一步才能读取数据库数据
public class FeignClientInvocationHandler implements InvocationHandler {
private Class interfaceType;
private FeignClient enableFeignAnnotation;
private Builder feignBuilder;
public FeignClientInvocationHandler(Class intefaceType,FeignClient enableFeignAnnotation,Builder feignBuilder) {
this.interfaceType = intefaceType;
this.enableFeignAnnotation = enableFeignAnnotation;
this.feignBuilder = feignBuilder;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
T proxyObject = feignBuilder.target(interfaceType, getUrl());
return method.invoke(proxyObject, args);
// 这里可以得到参数数组和方法等,可以通过反射,注解等,进行结果集的处理
// mybatis就是在这里获取参数和相关注解,然后根据返回值类型,进行结果集的转换
}
private String getUrl() throws SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
if (enableFeignAnnotation.urlParaType() == FeignClient.SYS_PARAMETER){
Object sysParameterDao = BeanFactoryHolder.get().getBean("sysParameterDaoImp");
//用反射解决跨bundle报错ClassNotFoundException: ISysParameterDao或SysParameter cannot be found by com.liangmixian.liangmixianibusiness.common_1.0.0
Method method = sysParameterDao.getClass().getMethod("findSysParameter",String.class);
Object sysParameter = method.invoke(sysParameterDao, enableFeignAnnotation.urlPara());
Method getValueMethod = sysParameter.getClass().getMethod("getValue");
String paraValue = (String)getValueMethod.invoke(sysParameter);
String urlValue = paraValue;
if (!StringUtils.isEmpty(urlValue)) {
return urlValue + enableFeignAnnotation.path();
}
Method getDefaultValueMethod = sysParameter.getClass().getMethod("getDefaultValue");
return (String)getDefaultValueMethod.invoke(sysParameter) + enableFeignAnnotation.path();
} else if (enableFeignAnnotation.urlParaType() == FeignClient.LOCAL_PROPERTIES){
//自己扩展
// String url = new Properties("***/*.properties").getValue(enableFeignAnnotation.urlPara());
return "";
}
return enableFeignAnnotation.urlPara() + enableFeignAnnotation.path();
}
}
增加的feign接口一定要用注解@FeignClient,且所属包路径符合规则:"com.liangmixian.sms.**.feign"
新增了feign接口的当前bundle一定要实例化FeignClientsRegistrar
第一次本地启动注意发布一下依赖的jar
附其他测试用例:
//-----------------------------------------------------------测试客户端-------------------------------------------------------------------
@liangmixianFeignClient(urlPara = "feign.test.url")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Path("/test-fegin")
public interface FeignJaxrsTestService {
@Path("/queryPeople")
@GET
Long queryPeopleIdById(@QueryParam("typeId") Long typeId);
@Path("/queryPeopleByTypeId/{typeId}")
@GET
List queryPeopleByTypeId(@PathParam("typeId") Long typeId);
@Path("/add")
@POST
People addPeople(People People);
@Path("/queryHugePeople")
@GET
PeopleType queryHugePeopleType();
}
public class People implements Serializable {
public People(){}
private Long id;
private String name;
private PeopleType type;
public PeopleType getType() {
return type;
}
public void setTyepe(PeopleType type) {
this.type = type;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public enum PeopleType {
MAN,
WOMAN;
public static PeopleType of(Integer typeId) {
if(MAN.ordinal()==typeId) {
return MAN;
} else {
return WOMAN;
}
}
}
//-----------------------------------------------------------测试服务端-------------------------------------------------------------------
@Controller
@RequestMapping(value="/test-fegin")
public class TestFeignController {
@ResponseBody
@RequestMapping(value = "/queryPeople",method={RequestMethod.GET})
Long queryPeopleIdById(@RequestParam(value = "typeId") Long id){
return id + 1;
}
@ResponseBody
@RequestMapping(value = "/queryPeopleByTypeId/{typeId}",method={RequestMethod.GET})
List queryPeopleByTypeId(@PathVariable("typeId") Integer typeId){
People p1 = new People();
p1.setTyepe(PeopleType.of(typeId));
p1.setId(1000L);
p1.setName("董小姐");
People p2 = new People();
p2.setTyepe(PeopleType.of(typeId));
p2.setId(2000L);
p2.setName("蔡嫡妃");
return Arrays.asList(p1,p2);
}
@ResponseBody
@RequestMapping(value = "/add",method={RequestMethod.POST})
People addPeople(@RequestBody People People){
People.setId(2222L);
return People;
}
@ResponseBody
@RequestMapping(value = "/queryHugePeople",method={RequestMethod.GET})
PeopleType queryHugePeople(){
return PeopleType.MAN;
}
}