传统Java Web非Spring Boot项目从Spring Cloud Eureka中获取服务

更新日志:
2018/3/3 21:05:43 新建
2018/3/11 7:11:59 新增注册到Eureka、从Eureka注销、新增Feign,更新配置文件,更新代码

部门项目的技术框架从 ZooKeeper+Dubbo 转型为Spring Cloud 微服务,转型顺利、开发方便、使用良好,于是完全废弃了ZooKeeper+Dubbo,而Web端后台管理界面的项目由于种种原因不希望大规模重构为Spring Boot项目,继续保持原有的SSM框架,并使用http调用微服务接口。为避免将微服务地址写死,这就需要Web项目连接到Spring Cloud Eureka 上通过服务名获取微服务真实地址。

项目依赖



com.netflix.eureka
eureka-client
1.7.0



com.netflix.ribbon
ribbon-core
${netflix.ribbon.version}


com.netflix.ribbon
ribbon-loadbalancer
${netflix.ribbon.version}


io.reactivex
rxjava




com.netflix.ribbon
ribbon-eureka
${netflix.ribbon.version}



io.github.openfeign
feign-hystrix
${netflix.feign.version}


io.github.openfeign
feign-ribbon
${netflix.feign.version}



io.github.openfeign
feign-slf4j
${netflix.feign.version}


io.github.openfeign
feign-jackson
${netflix.feign.version}


io.reactivex
rxjava
1.1.1

这里使用netflix项目下的eureka、ribbon、Feign、Hystrix。
ribbon-开头的项目都是同一个版本号,所以就抽取出${netflix.ribbon.version}统一管理。feign-开头的项目也都是同一个的版本号,抽取${netflix.feign.version}统一管理。

需要注意:

  1. Maven依赖jar包冲突问题:rxjava项目在ribbon-loadbalancerfeign-hystrix依赖的hystrix-core中都有使用。当前最新版2.2.4的ribbon-loadbalancer使用rxjava:1.0.9。而feign-hystrix依赖的hystrix-core使用rxjava:1.1.1。因为依赖冲突,ribbon-loadbalancer中的rxjava:1.0.9代替掉了hystrix-core中的rxjava:1.1.1。这样当程序运行时会疯狂报找不到类Error,找不到rx/Single,这个类在2.0.9中并没有,2.1.1中有hystrix-core用到了,但是由于依赖冲突使用2.0.9的rxjava没有该类,所以报错。
    解决办法:ribbon-loadbalancer使用 exclusion 排除依赖 rxjava 即可。
  2. feign-core在中央仓库有两个groupId: com.netflix.feignio.github.openfeign 。groupIdcom.netflix.feign在2016年7月提交到8.18.0后就没有再提交,而groupIdio.github.openfeign已经在2018年三月份提交到9.6.0。

配置文件

Ribbon配置

ribbon.properties

xxx-service对应的微服务名

xxx-service.ribbon.DeploymentContextBasedVipAddresses=xxx-service
# 固定写法,xxx-service使用的ribbon负载均衡器
xxx-service.ribbon.NIWSServerListClassName=com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList
# 每分钟更新xxx-service对应服务的可用地址列表
xxx-service.ribbon.ServerListRefreshInterval=60000

Eureka配置

Eureka默认在classpath中寻找eureka-client.properties配置文件

控制是否注册自身到eureka中,本项目虽然不对外提供服务,但需要Eureka监控,在Eureka列表上显示

eureka.registration.enabled=true
# eureka相关配置
# 默认为true,以实现更好的基于区域的负载平衡。
eureka.preferSameZone=true
# 是否要使用基于DNS的查找来确定其他eureka服务器
eureka.shouldUseDns=false
# 由于shouldUseDns为false,因此我们使用以下属性来明确指定到eureka服务器的路由(eureka Server地址)
eureka.serviceUrl.default=http://username:password@localhost:8761/eureka/
eureka.decoderName=JacksonJson
# 客户识别此服务的虚拟主机名,这里指的是eureka服务本身
eureka.vipAddress=XXXplatform
#服务指定应用名,这里指的是eureka服务本身
eureka.name=XXXlatform
#服务将被识别并将提供请求的端口
eureka.port=8080

初始化Ribbon、注册Eureka

之前初始化Ribbon、Eureka、注册到Eureka和获取地址的方法写在静态代码块和静态方法中,这样在项目停止时无法从Eureka中取消注册,这会使Eureka进入安全模式,死掉的项目一直显示在Eureka列表中。

继承ServletContextListener,重写contextInitialized、contextDestroyed,在应用上下文启动时初始化Ribbon、Eureka、注册到Eureka,应用上下文销毁时注销Eureka。

import java.io.IOException;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.netflix.appinfo.ApplicationInfoManager;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.config.ConfigurationManager;
import com.netflix.discovery.DefaultEurekaClientConfig;
import com.netflix.discovery.DiscoveryManager;

/**
* @ClassName: EurekaInitAndRegisterListener
* @Description: 服务器启动初始化Ribbon和注册到Eureka Server
* @author SuXun
* @date 2018年3月7日 上午9:21:12
/
@SuppressWarnings("deprecation")
public class EurekaInitAndRegisterListener implements ServletContextListener {
private static final Logger LOGGER = LoggerFactory.getLogger(EurekaInitAndRegisterListener.class);
/
*
* 默认的ribbon配置文件名, 该文件需要放在classpath目录下
*/
public static final String RIBBON_CONFIG_FILE_NAME = "ribbon.properties";

@Override
public void contextInitialized(ServletContextEvent sce) {
LOGGER.info("开始初始化ribbon");
try {
// 加载ribbon配置文件
ConfigurationManager.loadPropertiesFromResources(RIBBON_CONFIG_FILE_NAME);
} catch (IOException e) {
e.printStackTrace();
LOGGER.error("ribbon初始化失败");
throw new IllegalStateException("ribbon初始化失败");
}
LOGGER.info("ribbon初始化完成");
// 初始化Eureka Client
LOGGER.info("Eureka初始化完成,正在注册Eureka Server");
DiscoveryManager.getInstance().initComponent(new MyInstanceConfig(), new DefaultEurekaClientConfig());
ApplicationInfoManager.getInstance().setInstanceStatus(InstanceInfo.InstanceStatus.UP);
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
DiscoveryManager.getInstance().shutdownComponent();
}
}

这里有个自定义的类MyInstanceConfig,这个类作用是将注册到Eureka的hostName从主机名换成IP地址加端口号的形式。

import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.util.Enumeration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.netflix.appinfo.MyDataCenterInstanceConfig;

/**
* @ClassName: MyInstanceConfig
* @Description:
* @author SuXun
* @date 2018年3月7日 上午9:33:31
*/
public class MyInstanceConfig extends MyDataCenterInstanceConfig {
private static final Logger LOG = LoggerFactory.getLogger(MyInstanceConfig.class);
@Override
public String getHostName(boolean refresh) {

try {
return findFirstNonLoopbackAddress().getHostAddress();
} catch (Exception e) {
return super.getHostName(refresh);
}
}

public InetAddress findFirstNonLoopbackAddress() {
InetAddress result = null;
try {
int lowest = Integer.MAX_VALUE;
for (Enumeration nics = NetworkInterface
.getNetworkInterfaces(); nics.hasMoreElements()😉 {
NetworkInterface ifc = nics.nextElement();
if (ifc.isUp()) {
LOG.trace("Testing interface: " + ifc.getDisplayName());
if (ifc.getIndex() < lowest || result == null) {
lowest = ifc.getIndex();
}
else if (result != null) {
continue;
}

// @formatter:off
for (Enumeration addrs = ifc
.getInetAddresses(); addrs.hasMoreElements()😉 {
InetAddress address = addrs.nextElement();
if (address instanceof Inet4Address
&& !address.isLoopbackAddress()) {
LOG.trace("Found non-loopback interface: "
+ ifc.getDisplayName());
result = address;
}
}
// @formatter:on
}
}
}
catch (IOException ex) {
LOG.error("Cannot get first non-loopback address", ex);
}

if (result != null) {
return result;
}

try {
return InetAddress.getLocalHost();
}
catch (UnknownHostException e) {
LOG.warn("Unable to retrieve localhost");
}

return null;
}
}

虽然DiscoveryManager.getInstance().initComponent()方法已经被标记为@Deprecated了,但是ribbon的DiscoveryEnabledNIWSServerList组件代码中依然是通过DiscoveryManager来获取EurekaClient对象的。

获取服务地址

mport java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.netflix.client.ClientFactory;
import com.netflix.loadbalancer.DynamicServerListLoadBalancer;
import com.netflix.loadbalancer.RoundRobinRule;
import com.netflix.loadbalancer.Server;

/**
* @ClassName: AlanServiceAddressSelector
* @Description: 获取到目标服务注册在Eureka地址
* @author SuXun
* @date 2018年3月2日 下午5:23:24
*/
public class AlanServiceAddressSelector {

private static final Logger log = LoggerFactory.getLogger(AlanServiceAddressSelector.class);
private static RoundRobinRule chooseRule = new RoundRobinRule();

/**
* 根据轮询策略选择一个地址
* @param clientName ribbon.properties配置文件中配置项的前缀名, 如myclient
* @return null表示该服务当前没有可用地址
*/
public static AlanServiceAddress selectOne(String clientName) {
// ClientFactory.getNamedLoadBalancer会缓存结果, 所以不用担心它每次都会向eureka发起查询
@SuppressWarnings("rawtypes")
DynamicServerListLoadBalancer lb = (DynamicServerListLoadBalancer) ClientFactory
.getNamedLoadBalancer(clientName);
Server selected = chooseRule.choose(lb, null);
if (null == selected) {
log.warn("服务{}没有可用地址", clientName);
return null;
}
log.debug("服务{}选择结果:{}", clientName, selected);
return new AlanServiceAddress(selected.getPort(), selected.getHost());
}

/**
* 选出该服务所有可用地址
* @param clientName
* @return
*/
public static List selectAvailableServers(String clientName) {
@SuppressWarnings("rawtypes")
DynamicServerListLoadBalancer lb = (DynamicServerListLoadBalancer) ClientFactory
.getNamedLoadBalancer(clientName);
List serverList = lb.getServerList(true);
// List serverList = lb.getReachableServers();
if (serverList.isEmpty()) {
log.warn("服务{}没有可用地址", clientName);
return Collections.emptyList();
}
log.debug("服务{}所有选择结果:{}", clientName, serverList);
List address = new ArrayList();
for (Server server : serverList) {
address.add(new AlanServiceAddress(server.getPort(), server.getHost()));
}
return address;
}
}

地址实体类:

/**
* @ClassName: AlanServiceAddress
* @Description: 地址实体类
* @author SuXun
* @date 2018年3月2日 下午2:14:17
*/
public class AlanServiceAddress {
private int port;
private String host;

public AlanServiceAddress() {
}

public AlanServiceAddress(int port, String host) {
this.port = port;
this.host = host;
}

public int getPort() {
return port;
}

public void setPort(int port) {
this.port = port;
}

public String getHost() {
return host;
}

public void setHost(String host) {
this.host = host;
}

/**
* 将服务地址转换为 http://主机名:端口/ 的格式
* @return
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder(15 + host.length());
sb.append("http://").append(host).append("😊.append(port).append("/");

return sb.toString();
}
}

使用方法

// 选择出myclient对应服务全部可用地址
List list = AlanServiceAddressSelector.selectAvailableServers("myclient");
System.out.println(list);

// 选择出myclient对应服务的一个可用地址(轮询), 返回null表示服务当前没有可用地址
AlanServiceAddress addr = AlanServiceAddressSelector.selectOne("myclient");
System.out.println(addr);

构建Feign客户端

根据服务名在Eureka获取地址,构建Feign,如果缓存有则返回缓存里的Feign,避免重复构建Feign。

构建Feign方法:

import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.LoggerFactory;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommand.Setter;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandProperties;
import com.xxx.platform.common.netflex.eureka.AlanServiceAddress;
import com.xxx.platform.common.netflex.eureka.AlanServiceAddressSelector;
import feign.Feign;
import feign.Logger;
import feign.Request.Options;
import feign.Retryer;
import feign.Target;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.hystrix.HystrixFeign;
import feign.hystrix.SetterFactory;
import feign.jackson.JacksonDecoder;
import feign.jackson.JacksonEncoder;
import feign.slf4j.Slf4jLogger;

/**
* @ClassName: BaseFeignBuilder
* @Description: 用于构建Feign
* @author SuXun
* @date 2018年3月5日 下午3:50:18
*/
public class BaseFeignBuilder {

private static final org.slf4j.Logger log = LoggerFactory.getLogger(BaseFeignBuilder.class);
private static ConcurrentHashMap<String, Object> cacheFeignMap = new ConcurrentHashMap<String, Object>();
private static ConcurrentHashMap<String, String> cacheAddressMap = new ConcurrentHashMap<String, String>();

/**
* 构建HystrixFeign,具有Hystrix提供的熔断和回退功能,JacksonEncoder、JacksonDecoder、Slf4jLogger、Logger.Level.FULL
* @param apiType 使用feign访问的接口类,如MedBodyClient.class
* @param clientName 配置文件中的ribbon client名字
* @param fallback 回退类
* @param url 添加网址
* @return
*/
public static T buildHystrixFeign(Class apiType, T fallback, String url) {
// 之前用GsonEncoder()和GsonDecoder()对Date类型支持不好,改成JacksonEncoder和JacksonDecoder,日期转换正常
return buildHystrixFeign(apiType, fallback, url, new JacksonEncoder(), new JacksonDecoder(),
new Slf4jLogger(BaseFeignBuilder.class), Logger.Level.FULL);
}

/**
* 构建HystrixFeign,具有Hystrix提供的熔断和回退功能
* @param apiType 使用feign访问的接口类,如MedBodyClient.class
* @param clientName clientName 配置文件中的ribbon client名字
* @param fallback 回退类
* @param url 添加网址
* @param encoder 编码器
* @param decoder 解码器
* @param logger 日志对象
* @param logLevel 日志级别
* @return
*/
public static T buildHystrixFeign(Class apiType, T fallback, String url, Encoder encoder, Decoder decoder,
Logger logger, Logger.Level logLevel) {
return HystrixFeign.builder().encoder(encoder).decoder(decoder).logger(logger).logLevel(logLevel)
//options添加Feign请求响应超时时间
.options(new Options(60 * 1000, 60 * 1000)).retryer(Retryer.NEVER_RETRY)
.setterFactory(new SetterFactory() {
@Override
public Setter create(Target<?> target, Method method) {
//添加Hstrix请求响应超时时间
return HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(apiType.getClass().getSimpleName()))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(60 * 1000) // 超时配置
);
}
}).target(apiType, url, fallback);
}

/**
* 获取HystrixFeign。缓存有在缓存取,缓存没有重新构建Feign
* @param apiType 使用feign访问的接口类,如MedBodyClient.class
* @param clientName clientName 配置文件中的ribbon client名字
* @param separator 添加网址分割
* @return
*/
@SuppressWarnings("unchecked")
public static T getCacheFeign(Class apiType, String clientName, T fallback, String separator) {

String resultAddress = getResultAddress(clientName);

String cacheKey = apiType.getName() + "-" + clientName + "-" + fallback.getClass().getName() + "-"
+ resultAddress + separator;
Object cacheFeign = cacheFeignMap.get(cacheKey);
if (cacheFeign == null) {
T buildFeign = buildHystrixFeign(apiType, fallback, resultAddress + separator);
cacheFeignMap.put(cacheKey, buildFeign);
return buildFeign;
} else {
return (T) cacheFeign;
}
}
/**
* 获取服务地址,取不到最新地址在缓存取旧地址,有新地址则返回新地址并刷新缓存
* @param clientName
* @return
*/
public static String getResultAddress(String clientName) {
String recentAddress = null;
AlanServiceAddress alanServiceAddress = AlanServiceAddressSelector.selectOne(clientName);
recentAddress = alanServiceAddress == null ? "" : alanServiceAddress.toString();
String cacheAddress = cacheAddressMap.get(clientName);
String resultAddress = "";

if (StringUtils.isBlank(recentAddress)) {
if (StringUtils.isBlank(cacheAddress)) {
log.error("服务" + clientName + "无可用地址");
throw new RuntimeException("服务" + clientName + "无可用地址");
} else {
resultAddress = cacheAddress;
}
} else {
resultAddress = recentAddress;
cacheAddressMap.put(clientName, recentAddress);
}
return resultAddress;
}
}

通用Feign接口

利用Feign继承特性,特殊需求接口只要继承通用接口就可获得访问通用接口的能力.

import java.util.Map;
import com.xxx.commons.base.ResultJsonEntity;
import feign.Headers;
import feign.Param;
import feign.RequestLine;

/**
* @ClassName: BaseFeignClient
* @Description: Feign基类
* @author SuXun
* @date 2018年3月5日 下午1:25:59
*/
// @Herders里边的键值对冒号后面必须有个空格!
@Headers({ "Content-Type: application/json", "Accept: application/json" })
public interface BaseFeignClient {

@RequestLine("POST /select")
ResultJsonEntity select(Object obj);

@RequestLine("GET /selectAll")
ResultJsonEntity selectAll();

// 因为Example无法被序列化成json,所以参数为Map
@RequestLine("POST /selectByExample")
ResultJsonEntity selectByExample(Map<String, Object> map);

@RequestLine("GET /selectByPrimaryKey/{key}")
ResultJsonEntity selectByPrimaryKey(@Param("key") String key);

@RequestLine("POST /insertSelective")
ResultJsonEntity insertSelective(Object obj);

@RequestLine("POST /updateByPrimaryKeySelective")
ResultJsonEntity updateByPrimaryKeySelective(Object obj);

/**
* @param map key包含:int pageNum,int rowNum,T record和查询条件
* @return
*/
@RequestLine("POST /getPageExampleList")
ResultJsonEntity getPageExampleList(Map<String, Object> map);

}

通用回退类

因为使用HystrixClient.build(),使得Feign拥有熔断器、回退的功能。这里根据通用接口实现的回退类。
这里的ResultJsonEntity、ResultEnum、ResultJsonUtil用于返回平台无关的json数据。

import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.xxx.commons.base.ResultJsonEntity;
import com.xxx.commons.enums.ResultEnum;
import com.xxx.commons.util.ResultJsonUtil;

/**
* @ClassName: BaseFeignClientFallback
* @Description: Feign基类的回退类
* @author SuXun
* @date 2018年3月5日 下午5:04:13
*/
public class BaseFeignClientFallback implements BaseFeignClient {

protected static final Logger LOG = LoggerFactory.getLogger(BaseFeignClientFallback.class);

@Override
public ResultJsonEntity select(Object obj) {
LOG.error("{} select 出错 进入熔断 ", this.getClass().getName());
return ResultJsonUtil.returnResult(ResultEnum.FAIL);
}

@Override
public ResultJsonEntity selectAll() {
LOG.error("{} selectAll 出错 进入熔断", this.getClass().getName());
return ResultJsonUtil.returnResult(ResultEnum.FAIL);
}

@Override
public ResultJsonEntity selectByExample(Map<String, Object> map) {
LOG.error("{} selectByMap 出错 进入熔断 ", this.getClass().getName());
return ResultJsonUtil.returnResult(ResultEnum.FAIL);
}

@Override
public ResultJsonEntity selectByPrimaryKey(String key) {
LOG.error("{} selectByPrimaryKey 出错 进入熔断 ", this.getClass().getName());
return ResultJsonUtil.returnResult(ResultEnum.FAIL);
}

@Override
public ResultJsonEntity insertSelective(Object obj) {
LOG.error("{} insertSelective 出错 进入熔断 ", this.getClass().getName());
return ResultJsonUtil.returnResult(ResultEnum.FAIL);
}

@Override
public ResultJsonEntity updateByPrimaryKeySelective(Object obj) {
LOG.error("{} updateByPrimaryKeySelective 出错 进入熔断 ", this.getClass().getName());
return ResultJsonUtil.returnResult(ResultEnum.FAIL);
}

@Override
public ResultJsonEntity getPageExampleList(Map<String, Object> map) {
LOG.error("{} getPageExampleList 出错 进入熔断 ", this.getClass().getName());
return ResultJsonUtil.returnResult(ResultEnum.FAIL);
}
}

FeignClient使用方法

为了使Feign拥有负载均衡的能力,需要在 @ModelAttribute 注解的方法重复调用 getCacheFeigngetCacheFeign 方法可以获取最新的地址,根据地址构建Feign或者在缓存取出Feign。

private BaseFeignClient xxxClient = null;
@ModelAttribute
public xxxEntity get(@RequestParam(required = false) String id) {
xxxClient = BaseFeignBuilder.getCacheFeign(BaseFeignClient.class,
"xxx-service", new BaseFeignClientFallback(), "xxx");
}

遗憾

非常遗憾,这里是手动获取Eureka中的地址,看起来还不够优雅。
Feign可以结合Ribbon使用,通过传入服务名找地址,之前实现后发现需要两次访问才可以正确访问到服务,两次中必然有一次返回找不到地址,所以没有使用。我会继续研究,下次更新使Feign进行Http调用更优雅一些。

posted @ 2018-03-04 15:02  XanderSu  阅读(5352)  评论(1)    收藏  举报