前言
SpringCloud 其良好的背景以及社区非常高的活跃度,使其发展迅速,成为微服务实施的首选框架。
如果是新的业务考虑使用SpringCloud来进行实现,面临的一个比较严峻的问题就是老的应用如何访问SpringCloud微服务,因为目前可见的SpringCloud客户端无论是Ribbon还是Feign都必须在SpringCloud中使用,但是老应用的架构什么样的都有,因此实现一个简单的通过API访问SpringCloud的应用是当务之急。
设计目标
1:可以动态更新Eureka的服务信息,和Eureka的信息基本同步。
2:可以发现服务是否可用。
3:可以发现服务实例是否可用。
4:客户端可以实现简单负载均衡(类似Ribbon,随机路由,哈希路由)
5:客户端和服务器端规范通接口通信。
6:客户端动态代理实现接口调用。
研究阶段
如何同Eureka进行通信是关键,可以通过Eureka获得服务URL,这样就可以发送请求。
关键接口EurekaClient。这个接口的实例过程会自动加载EurekaClient配置文件。会主动同Eureka建立连接,定时任务启动,去和Eureka同步信息。有了这个其他的都好办了。
问我如何找到这个EurekaClient的,那就去http://bbs.springcloud.cn 去和大家交流,一定会有帮助的。
找到这个,那就得看如何获取实例,目前获取实例的方法有些土,等有时间再详细研究更科学方法。
//最关键的代码,加载配置文件,向Eureka发送请求,获取服务列表。
DiscoveryManager.getInstance().initComponent(new MyDataCenterInstanceConfig(), new DefaultEurekaClientConfig());
ApplicationInfoManager.getInstance().setInstanceStatus(InstanceStatus.UP);
EurekaClient client = DiscoveryManager.getInstance().getEurekaClient();
//获取从Eureka获取的全部的应用列表
Applications apps = client.getApplications();
//根据应用的名称获取已经可用的应用对象,可能是注册了多个。
Application app=apps.getRegisteredApplications(serviceName);
String reqUrl=null;
if(app!=null)
{
List<InstanceInfo> instances = app.getInstances();
if(instances.size()>0)
{
//获取其中一个应用实例,这里可以添加路由算法
InstanceInfo instance = instances.get(0);
//获取公开的地址和端口
reqUrl="http://"+instance.getIPAddr()+":"+instance.getPort();
}
}123456789101112131415161718192021
基本上前期的工作就完成了,启动后,可以看到客户端定时去Eureka进行更新。
完整代码:TestClientMain.java
public class TestClientMain
{
public static void main(String[] args)
{
//这个名称需要用你的服务的名称替换
String serviceName="TESTSERVICE";
//最关键的代码,加载配置文件,向Eureka发送请求,获取服务列表。
DiscoveryManager.getInstance().initComponent(new MyDataCenterInstanceConfig(), new DefaultEurekaClientConfig());
ApplicationInfoManager.getInstance().setInstanceStatus(InstanceStatus.UP);
EurekaClient client = DiscoveryManager.getInstance().getEurekaClient();
//获取从Eureka获取的全部的应用列表
Applications apps = client.getApplications();
//根据应用的名称获取已经可用的应用对象,可能是注册了多个。
Application app=apps.getRegisteredApplications(serviceName);
String reqUrl=null;
if(app!=null)
{
List<InstanceInfo> instances = app.getInstances();
if(instances.size()>0)
{
//获取其中一个应用实例,这里可以添加路由算法
InstanceInfo instance = instances.get(0);
//获取公开的地址和端口
reqUrl="http://"+instance.getIPAddr()+":"+instance.getPort();
}
}
System.out.println("输出URL="+reqUrl);
}
}123456789101112131415161718192021222324252627282930
eureka-client.properties
eureka.region=default
eureka.name=sampleEurekaClient
eureka.preferSameZone=true
eureka.shouldUseDns=false
eureka.serviceUrl.default=http://localhost:1111/eureka/12345
可以发现这条日志
DEBUG org.apache.http.wire - >> “GET /eureka/apps/ HTTP/1.1[\r][\n]”
也可以发现类似
输出URL=http://...:3333
获得了Application 的Instance的URL,就可以发送REST请求了。OK基于这思路开始我们的实现过程。
实现阶段
包括了5个不同的maven工程。
1:MyRibbon
2:MyRibbonInterface
3:MyRibbonService
4:EurekaServer
5:MyRibbonClient
MyRibbon这个工程是我们实现的公共API,提供了对SpringCloud引用的调用。
MyRibbonInterface这个工程师定义了服务接口API,面向接口进行编程,具体服务提供者(MyRibbonService)需要实现这些接口。
MyRibbonService具体的服务实现,注册在Eureka,为客户端提供调用。
EurekaServer一个Eureka的服务器
MyRibbonClient这个应用就是我们测试的客户端应用。
MyRibbon工程
pom.xml
<modelVersion>4.0.0</modelVersion>
<groupId>org.lipengbin.spring</groupId>
<artifactId>MyRibbon</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.5.RELEASE</version>
<relativePath />
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty</artifactId>
<version>6.1.26</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</depende
定义两个注解
MyService,MyMethod
MyService.java
package org.lipengbin.myribbon.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 定义接口服务的时候,需要指定这个接口服务对应在Eureka上对应的服务名称。
* 需要根据这个服务的名称获取对应的Application。
* @author Administrator
*
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyService
{
String value() default "";
}1234567891011121314151617181920
MyMethod.java
package org.lipengbin.myribbon.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 在服务接口的方法上使用。
* 主要是为了拼装URL和参数
*
* http://ip:port/MyMethod?a=10&b=20
*
* @author Administrator
*
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyMethod
{
String value() default "";
}12345678910111213141516171819202122
MyEurekaClient.java
package org.lipengbin.myribbon.eureka;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.client.RestTemplate;
import com.netflix.appinfo.ApplicationInfoManager;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.appinfo.MyDataCenterInstanceConfig;
import com.netflix.appinfo.InstanceInfo.InstanceStatus;
import com.netflix.discovery.DefaultEurekaClientConfig;
import com.netflix.discovery.DiscoveryManager;
import com.netflix.discovery.EurekaClient;
import com.netflix.discovery.shared.Application;
import com.netflix.discovery.shared.Applications;
/**
- 同EurekaServer建立连接
- 负责定时更新
- 负责获取指定的Service
- 外部不需要调用这个类
- 这个类是个单例
- @author Administrator
-
*/br/>@SuppressWarnings("deprecation")
class MyEurekaClient
{private static final Logger logger = LoggerFactory.getLogger(MyEurekaClient.class); private EurekaClient client; @Autowired private RestTemplate restTemplate; protected MyEurekaClient() { if(restTemplate==null) { restTemplate=new RestTemplate(); } init(); } protected void init() { DiscoveryManager.getInstance().initComponent(new MyDataCenterInstanceConfig(), new DefaultEurekaClientConfig()); ApplicationInfoManager.getInstance().setInstanceStatus(InstanceStatus.UP); client = DiscoveryManager.getInstance().getEurekaClient(); } /** * 根据Service名称和请求的,获取返回内容! * @param serviceName * @param url * @return */ public <T> T request(String serviceName,String url,Class<T> returnClaz) { Applications apps = client.getApplications(); Application app=apps.getRegisteredApplications(serviceName); if(app!=null) { List<InstanceInfo> instances = app.getInstances(); if(instances.size()>0) { try { InstanceInfo instance = instances.get(0); String reqUrl="http://"+instance.getIPAddr()+":"+instance.getPort()+"/"+url; return restTemplate.getForEntity(reqUrl, returnClaz).getBody(); } catch(Exception e) { logger.error("request is error。["+serviceName+"]",e); return null; } } else { logger.error("Application instance not exist。["+serviceName+"]"); return null; } } else { logger.error("Target Application not exist。["+serviceName+"]"); return null; } } }1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
MyEurekaHandler.java
package org.lipengbin.myribbon.eureka;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import org.lipengbin.myribbon.annotation.MyMethod;
import org.lipengbin.myribbon.annotation.MyService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
/**
- 利用动态代理封装了接口的远程调用。
- @author Administrator
-
*/
public class MyEurekaHandler implements InvocationHandler
{private static final Logger logger = LoggerFactory.getLogger(MyEurekaHandler.class);
/**
- 代理的目标接口类
*/
private Class<?> target;
/**
- Eureka中定义的Service的名称
*/
private String serviceName;
private MyEurekaClient client;
MyEurekaHandler(MyEurekaClient client)
{
this.client=client;
}@SuppressWarnings("unchecked")
public <T> T create(Class<T> target)
{
this.target=target;
MyService s=target.getAnnotation(MyService.class);
if(s!=null)
{
serviceName=s.value().toUpperCase();
return (T)Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class<?>[]{target}, this);
}
else
{
logger.error(target.getName()+",没有定义 @MyService!");
return null;
}
}/**
- 获取服务的名称
- @return
*/
public String getService()
{
return this.serviceName;
}
/**
- 函数的实现内容,可以使用用其他的方式实现,例如通信等。
-
String url=parseBySpring(method, args); if(url!=null) { builder.append("?"); builder.append(url); } return client.request(serviceName, builder.toString(), returnClaz);
}
}
/** - Java8是可以通过类文件获取参数的名称,但是需要在编译的时候进行参数设置。
- @param method
- @param args
-
@return
*/
protected String parseByJava8(Method method, Object[] args)
{
StringBuilder builder=new StringBuilder();
Parameter[] params=method.getParameters();
int length=params.length;
for(int i=0;i<length;i++)
{
Parameter p=params[i];
Object value=args[i];
if(i>0)
{
builder.append("&");
}builder.append(p.getName()); builder.append("="); builder.append(value);
}
return builder.toString();
}
/**
- @param method
- @param args
-
@return
*/
protected String parseBySpring(Method method, Object[] args)
{
StringBuilder builder=new StringBuilder();
//以下这种获取参数名称的解析,需要在编译的时候设置一下。见图
ParameterNameDiscoverer parameterNameDiscoverer =new DefaultParameterNameDiscoverer();
// new LocalVariableTableParameterNameDiscoverer(); 这个不知道为什么不好使?ASM5以上。设置都是OK的
try
{
System.out.println(method.getName());String[] parameterNames = parameterNameDiscoverer.getParameterNames(method); for(int i=0;i<args.length;i++) { if(i>0) { builder.append("&"); } builder.append(parameterNames[i]); builder.append("="); builder.append(args[i]); } return builder.toString();
}
catch(Exception e)
{
e.printStackTrace();
return null;
}
}
}
- 代理的目标接口类
这里需要说一下两个参数Discoverer。
DefaultParameterNameDiscoverer:这个使用的Java8新提供的Parameter获取参数名称。
这种方式和parseByJava8基本上是一样的。
需要在Eclipse中进行设置。
LocalVariableTableParameterNameDiscoverer:这个是使用ASM获取参数名称。
需要在Eclipse进行设置。
MyEurekaFactory.java
这个类是为外界提供调用的,使用方式很简单,后面会写一个完整的demo例子.
package org.lipengbin.myribbon.eureka;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
- 这个工厂类管理了当前应用需要调用或者已经调用的接口实例。
- 因为通过动态代理对服务进行了封装。
- 因此对于一个应用来讲,是需要对服务实例进行管理的,否则每次都会进行创建。
- @author Administrator
-
*/
public class MyEurekaFactory
{
private static final Logger logger = LoggerFactory.getLogger(MyEurekaFactory.class);
/**
-
当前应用对应的服务Map
*/
public Map<Class<?>,Object> serviceMap=new HashMap<>();private static MyEurekaFactory factory;
private static ReentrantLock lock=new ReentrantLock();
private MyEurekaClient client;
/**
- 这个是当前的启动入口
*/
private MyEurekaFactory()
{
client=new MyEurekaClient();
}
/** - 单例模式创建对象,当然可以通过注解的形式进行创建
-
@return
*/
public static MyEurekaFactory gi()
{
if(factory==null)
{
lock.lock();
if(factory==null)
{
factory=new MyEurekaFactory();
}
lock.unlock();
}
return factory;
}@SuppressWarnings("unchecked")
public <T> T createService(Class<T> serviceInterface)
{
if(serviceMap.containsKey(serviceInterface))
{
logger.debug("Service存在" +serviceInterface);
return (T)serviceMap.get(serviceInterface);
}
else
{
logger.debug("Service不存在" +serviceInterface);
return add(serviceInterface);
}
}
/** - 此处未做同步,因为创建多个实例会被覆盖,不会出现问题!
- 只会影响首次的创建效率
- @param serviceInterface
- @return
*/
private <T> T add(Class<T> serviceInterface)
{
MyEurekaHandler handler=new MyEurekaHandler(client);
T t=handler.create(serviceInterface);
serviceMap.put(serviceInterface, t);
return t;
}
}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
MyRibbonInterface工程
pom.xml
<modelVersion>4.0.0</modelVersion>
<groupId>org.lipengbin.spring</groupId>
<artifactId>MyRibbonInterface</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.lipengbin.spring</groupId>
<artifactId>MyRibbon</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>1234567891011
MyValue.java
简单的返回对象
package org.lipengbin.test;
public class MyValue
{
private String name;
private int value;
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public int getValue()
{
return value;
}
public void setValue(int value)
{
this.value = value;
}
}12345678910111213141516171819202122232425262728
定义的服务接口
package org.lipengbin.test;
import org.lipengbin.myribbon.annotation.MyMethod;
import org.lipengbin.myribbon.annotation.MyService;
@MyService("TestService")
public interface TestServicebr/>{
@MyMethod("execute")
public MyValue execute(String name,Integer value);
}1234567891011
MyRibbonService工程
pom.xml
1
<modelVersion>4.0.0</modelVersion>
<groupId>org.lipengbin.spring</groupId>
<artifactId>MyRibbonService</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.5.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.lipengbin.spring</groupId>
<artifactId>MyRibbon</artifactId>
<version>0.0.1-SNAPSHOT</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.lipengbin.spring</groupId>
<artifactId>MyRibbonInterface</artifactId>
<version>0.0.1-SNAPSHOT</version>
<type>jar</type>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
TestServiceImpl.java
package org.lipengbin.spring.test;
import org.lipengbin.test.MyValue;
import org.lipengbin.test.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestServiceImpl implements TestServicebr/>{
@Autowired
private DiscoveryClient client;
@RequestMapping(value = "/execute" ,method = RequestMethod.GET)
public MyValue execute(@RequestParam String name,@RequestParam Integer value)
{
System.out.println("------------------>");
ServiceInstance instance = client.getLocalServiceInstance();
MyValue myvalue=new MyValue();
myvalue.setName("name="+name);
myvalue.setValue(100+value);
return myvalue;
}
}
TestServiceApplication.java
package org.lipengbin.spring.test;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
application.properties
spring.application.name=TestService
server.port=3333
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/123
EurekaServer工程
pom.xml
<modelVersion>4.0.0</modelVersion>
<groupId>org.lipengbin</groupId>
<artifactId>Eureka-Server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.5.RELEASE</version>
<relativePath />
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty</artifactId>
<version>6.1.26</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
EurekaServer.java
package org.lipengbin;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServerbr/>@SpringBootApplication
public class EurekaServer
{
public static void main(String[] args)
{
new SpringApplicationBuilder(EurekaServer.class).web(true).run(args);
}
}
MyRibbonClient工程
pom.xml
<modelVersion>4.0.0</modelVersion>
<groupId>org.lipengbin.spring</groupId>
<artifactId>MyRibbonClient</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.lipengbin.spring</groupId>
<artifactId>MyRibbon</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.lipengbin.spring</groupId>
<artifactId>MyRibbonInterface</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>12345678910111213141516
eureka-client.properties
eureka.region=default
eureka.name=sampleEurekaClient
eureka.preferSameZone=true
eureka.shouldUseDns=false
eureka.serviceUrl.default=http://localhost:1111/eureka/12345
ClientMain.java
package org.lipengbin.spring.test.client;
import org.lipengbin.myribbon.eureka.MyEurekaFactory;
import org.lipengbin.test.MyValue;
import org.lipengbin.test.TestService;
public class ClientMain
{
public static void main(String[] args)
{
TestService service=MyEurekaFactory.gi().createService(TestService.class);
MyValue value=service.execute("joy", 8);
System.out.println(value.getName()+","+value.getValue());
}
}
执行过程
1:将MyRibbon执行mvn install安装到本地仓库
2:将MyRibbonInterface 执行mvn install安装到本地仓库
3:关闭MyRibbon,和MyRibbonInterface,强制让其他工程引入jar包。
4:启动EurekaServer
5:启动MyRibbonService
6:运行客户端MyRibbonClient