更新日志:
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 上通过服务名获取微服务真实地址。
项目依赖
<!-- eureka 服务发现 -->
<dependency>
<groupId>com.netflix.eureka</groupId>
<artifactId>eureka-client</artifactId>
<version>1.7.0</version>
</dependency>
<!-- Ribbon 负载均衡 -->
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-core</artifactId>
<version>${netflix.ribbon.version}</version>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-loadbalancer</artifactId>
<version>${netflix.ribbon.version}</version>
<exclusions>
<exclusion>
<groupId>io.reactivex</groupId>
<artifactId>rxjava</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-eureka</artifactId>
<version>${netflix.ribbon.version}</version>
</dependency>
<!-- Feign 包装http请求 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hystrix</artifactId>
<version>${netflix.feign.version}</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-ribbon</artifactId>
<version>${netflix.feign.version}</version>
</dependency>
<!-- <dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-gson</artifactId>
<version>${netflix.feign.version}</version>
</dependency> -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-slf4j</artifactId>
<version>${netflix.feign.version}</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-jackson</artifactId>
<version>${netflix.feign.version}</version>
</dependency>
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxjava</artifactId>
<version>1.1.1</version>
</dependency>
这里使用netflix项目下的eureka、ribbon、Feign、Hystrix。
ribbon-开头的项目都是同一个版本号,所以就抽取出${netflix.ribbon.version}
统一管理。feign-开头的项目也都是同一个的版本号,抽取${netflix.feign.version}统一管理。
需要注意:
- Maven依赖jar包冲突问题:
rxjava
项目在ribbon-loadbalancer
和feign-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
即可。 - feign-core在中央仓库有两个groupId:
com.netflix.feign
和io.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<NetworkInterface> 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<InetAddress> 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<AlanServiceAddress> selectAvailableServers(String clientName) {
@SuppressWarnings("rawtypes")
DynamicServerListLoadBalancer lb = (DynamicServerListLoadBalancer) ClientFactory
.getNamedLoadBalancer(clientName);
List<Server> serverList = lb.getServerList(true);
// List<Server> serverList = lb.getReachableServers();
if (serverList.isEmpty()) {
log.warn("服务{}没有可用地址", clientName);
return Collections.emptyList();
}
log.debug("服务{}所有选择结果:{}", clientName, serverList);
List<AlanServiceAddress> address = new ArrayList<AlanServiceAddress>();
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<AlanServiceAddress> 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> T buildHystrixFeign(Class<T> 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> T buildHystrixFeign(Class<T> 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> T getCacheFeign(Class<T> 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
注解的方法重复调用 getCacheFeign
,getCacheFeign
方法可以获取最新的地址,根据地址构建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调用更优雅一些。