微服务间调用使用的是 Eureka, Ribbon, Hystrix and Feign
Eureka Eureka 是一个基于 REST 的服务,它主要是用于定位服务,以实现 AWS 云端的负载均衡和中间层服务器的故障转移。此处主要是指微服务注册中心 Ribbon 提供客户侧的软件负载均衡算法, Hystrix 是一个断路器,主要是解决当某个方法调用失败的时候,调用后备方法来替代失败的方法,达到容错,阻止级联错误等功能 Feign 是一个声明web服务客户端
规范
接口统一命名 微服务简写FeignClient.java 例如 HospitalFeignClient.java
错误回调类统一命名 微服务简写FeignClientFallback.java 例如 HospitalFeignClientFallback.java
dto是数据传输对象,实际上就是对返回的JSON的一个封装,否则你只能JSONObject.getString("key")这样用,按照api上定义的进行封装,实际上也是一个javabean,将自己生产的dto放到dto包下
假如有个微服务名叫 app1,有个resource是GET访问的名叫foos,有个DTO(数据传输对象--Data Transfer Object)叫FooDTO
在client包下新建给App1Client接口
@FeignClient(name = "app1",fallback=App1FeignClientFallback.class)
public interface App1FeignClient {
@RequestMapping(value = "/api/foos",//注意:1
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
List<FooDTO> getAllFoos();
@RequestMapping(value = "/api/foos/{id}",method = RequestMethod.GET,produces = MediaType.APPLICATION_JSON_VALUE)
FooDTO getFooById(@PathVariable("id") Long id);
}
注意 @FeignClient 暂时不支持 @GetMapping
一类的缩写注解,只能使用 @RequestMapping
详情参见 https://github.com/spring-cloud/spring-cloud-netflix/issues/1564
新建错误回调类
@Component
public class App1FeignClientFallback implements App1FeignClient {
private final Logger log = LoggerFactory.getLogger(App1FeignClientFallback.class);
@Override
public List<FooDTO> getAllFoos() {
log.error("/app1/api/foos 调用失败 ");
return null;
}
@Override
public FooDTO getFooById(Long id) {
log.error("/app1/api/foos/{id} 调用失败 ");
return null;
}
}
如果不加容错类,调用的微服务未启动。会报如下错误
***************************
APPLICATION FAILED TO START
***************************
Description:
Field abcClient in web.rest.xxxResource required a bean of type 'client.App1FeignClient' that could not be found.
Action:
Consider defining a bean of type 'client.App1FeignClient' in your configuration.
然后调用方法如下
@RestController
@RequestMapping("/api")
public class BarResource {
@Inject
private App1FeignClient app1FeignClient;
@RequestMapping(value = "/bars",//注意:2
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public List<FooDTO> getAllBars() {
return app1Client.getAllFoos();
}
}
注意1,2处的暴露地址不能重复,否则调用时会报404
如果一切正常,此时调用,应该会报401无权调用。
将
@FeignClient(name = "app1")
改为
@AuthorizedFeignClient(name="app1")
即可
@AuthorizedFeignClient
是JHipster生成的,源码如下(截止v3.9.1)
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.cloud.netflix.feign.FeignClientsConfiguration;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@FeignClient
public @interface AuthorizedFeignClient {
@AliasFor(annotation = FeignClient.class, attribute = "name")
String name() default "";
/**
* A custom <code>@Configuration</code> for the feign client.
*
* Can contain override <code>@Bean</code> definition for the pieces that
* make up the client, for instance {@link feign.codec.Decoder},
* {@link feign.codec.Encoder}, {@link feign.Contract}.
*
* @see FeignClientsConfiguration for the defaults
*/
@AliasFor(annotation = FeignClient.class, attribute = "configuration")
Class<?>[] configuration() default OAuth2InterceptedFeignConfiguration.class;
/**
* An absolute URL or resolvable hostname (the protocol is optional).
*/
String url() default "";
/**
* Whether 404s should be decoded instead of throwing FeignExceptions.
*/
boolean decode404() default false;
/**
* Fallback class for the specified Feign client interface. The fallback class must
* implement the interface annotated by this annotation and be a valid spring bean.
*/
Class<?> fallback() default void.class;
/**
* Path prefix to be used by all method-level mappings. Can be used with or without
* <code>@RibbonClient</code>.
*/
String path() default "";
}
如何测试,详见 官方文档
如果不想用feign,想自己实现,给出一个jsoup的demo,此处只是简单的示例,不考虑实现Token重用,过期续签等问题
注意,需要使用v 1.10.1+版本的jsoup,1.9.2的有bug,包含requestBody的都无论是否设置Content-Type都不生效,统统为x-www-form-urlencoded
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.jsoup.Connection;
import org.jsoup.Connection.Method;
import org.jsoup.Connection.Response;
import org.jsoup.Jsoup;
import org.springframework.beans.factory.annotation.Value;
import net.sf.json.JSONObject;
/**
* <b>描 述</b>: 通过jsoup调用微服务<br/>
* <b>文件名称</b>: MicroServiceClient.java<br/>
* <b>包 名</b>: com.shunneng.client<br/>
* <b>创建时间</b>: 2016年10月25日 下午3:00:08<br/>
* <b>修改时间</b>: <br/>
*
* @author SN_AnJia([email protected])
* @version 1.0
* @since jdk 1.8
*/
public class MicroServiceClient {
Map<String, String> headers = new HashMap<>();
//gateway地址
@Value("${shunneng.apiServer}")//@Value是读取application.yml配置中shunneng.apiServer节点值
private String apiServer ;
private String serverName;
String body;
String authorization;
int code;
String codeMsg;
MicroServiceClient() {}
/**
* <b>创建实例 </b>:HttpUtils.<br/>
* <b>创建时间 </b>:2016年10月26日 上午9:13:58<br/>
* @author SN_AnJia([email protected])
* @version 1.0
*
* @param serverName 服务名称
*/
public MicroServiceClient(String serverName) {
this.serverName=serverName;
}
/**
* <b>创建实例 </b>:HttpUtils.<br/>
* <b>创建时间 </b>:2016年10月26日 上午9:13:40<br/>
* @author SN_AnJia([email protected])
* @version 1.0
*
* @param apiServer 微服务网关地址
* @param serverName 服务名称
*/
public MicroServiceClient(String apiServer,String serverName) {
this.apiServer=apiServer;
this.serverName=serverName;
}
/**
* <b>方法名</b>: login
* <p><b>描 述</b>: 登录</p>
*
* @param username 用户名
* @param password 密码
* @return 对象实例
* @throws Exception
*
* <p><b>创建日期</b>:2016年10月25日 下午4:34:39</p>
* <p><b>修改日期</b>:</p>
* @author SN_AnJia([email protected])
* @version 1.0
* @since jdk 1.8
*/
public MicroServiceClient login(String username, String password) throws Exception {
authorization="Basic d2ViX2FwcDo=";
execute(apiServer + "/uaa/oauth/token", Method.POST, new HashMap<String,String>(){{put("username", username);put("password", password);put("grant_type", "password");}}, password, null);
JSONObject json = JSONObject.fromObject(this.body);
authorization=json.get("token_type") + " " + json.get("access_token");
return this;
}
/**
* <b>方法名</b>: getUrl
* <p><b>描 述</b>: 封装调用api地址 {@code http://api.shunnengnet.com/api/patients}</p>
*
* @param url 调用api的资源地址 e.g. {@code getUrl("patients")-> http://api.shunnengnet.com/api/patients}
* @return 拼好的地址 e.g. {@code http://api.shunnengnet.com/api/patients}
*
* <p><b>创建日期</b>:2016年10月25日 下午4:35:38</p>
* <p><b>修改日期</b>:</p>
* @author SN_AnJia([email protected])
* @version 1.0
* @since jdk 1.8
*/
public String getUrl(String url){
return apiServer+"/"+serverName+"/api/"+url;
}
/**
* <b>方法名</b>: execute
* <p><b>描 述</b>: 远程调用通用工具类</p>
*
* @param url 调用的url e.g. {@code http://api.shunnengnet.com/api/patients}
* @param method GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS,TRACE
* @param datas 要发送的数据,通常作为form-data发送
* @param body 请求体
* @param headers 要携带的header
* @return 远程服务器返回的结果
* @throws Exception
*
* <p><b>创建日期</b>:2016年10月25日 下午5:13:40</p>
* <p><b>修改日期</b>:</p>
* @author SN_AnJia([email protected])
* @version 1.0
* @since jdk 1.8
* @see org.jsoup.Connection.Method
*/
protected String execute(String url,Method method,Map<String, String> datas,String body,Map<String, String> headers) throws Exception {
//部分方法不支持请求body
if (method==null || !method.hasBody()) {
body=null;
}
Connection conn = Jsoup.connect(url)
.header("Authorization", authorization)//OAuth
.header("Content-Type", "application/json;charset=UTF-8")
.header("Accept", "application/json;charset=UTF-8")
.ignoreContentType(true)//Jsoup 默认只支持html等文本型contentType,对于json,图片等会报错,需要手动设置为true
// .timeout(1000)//jsoup默认超时时间是3秒
.ignoreHttpErrors(true)//如果为false,对于500,404等错误会报异常
// .followRedirects(false)//对于3xx等重定向请求,jsoup默认是跟随跳转的
.method(method);
//设置header头
if (null!=headers&&!headers.isEmpty()) {
for (Entry<String, String> header : headers.entrySet()) {
conn.header(header.getKey(), header.getValue());
}
}
//设置请求body
if (null!=body) {
conn.requestBody(body)
.header("Content-Type", "application/json;charset=UTF-8");
}
//设置请求参数
if (null!=datas&&!datas.isEmpty()) {
conn.data(datas);
}
Response resp=conn.execute();
//http状态信息
//一般响应header中的HTTP/1.1 200 OK
//协议 HTTP/1.1
//200 状态码
//OK 状态信息
this.codeMsg=resp.statusMessage();
//http://www.cnblogs.com/shanyou/archive/2012/05/06/2486134.html
//1xx临时响应
//2xx成功
//3xx重定向
//4xx请求错误
//5xx服务器错误
if (!String.valueOf(this.code=resp.statusCode()).startsWith("2")) {
throw new Exception("{code:" + resp.statusCode() + ",msg:'操作失败,statusMessage:"+codeMsg+"'}");
}
//响应内容
this.body = resp.body();
//响应header
this.headers=resp.headers();
return this.body;
}
public static void main(String[] args) throws Exception {
// HttpUtils utils=new HttpUtils("hospital");
MicroServiceClient utils=new MicroServiceClient("http://172.60.20.53:8080","hospital");
//登录
utils.login("username", "password");
//注意查看api接口文档说明的调用方法,比如GET,POST,PUT,DELETE不能混用
//获取省立医院所有科室
//适用于微信,支付宝,微官网等调用
System.err.println(utils.execute(utils.getUrl("departments/SDSL2015"), Method.GET, null, null, null));
//批量推送省立医院
//适用于中间件主动推送科室数据
System.err.println(utils.execute(utils.getUrl("departments/batch"), Method.POST, null, "[{\"address\": \"门诊楼1楼\",\"areaId\": \"center\",\"areaName\": \"中心院区\",\"deptId\": \"0002\",\"deptName\": \"消化内科\",\"deptType\": \"0\",\"detail\": \"消化内科介绍\",\"hosId\": \"SDSL2015\",\"hosName\": \"山东省立医院\",\"parentId\": \"\",\"releaseTime\": \"2016-10-25 18:14:02\",\"remainder\": \"\",\"remark\": \"\",\"sort\": 1,\"telPhone\": \"0531-88819638\"}]", null));
//获取省立医院所有科室
//适用于微信,支付宝,微官网等调用
System.err.println(utils.execute(utils.getUrl("departments/SDSL2015"), Method.GET, null, null, null));
//过滤查询
//id,deptId,deptName等所有对象属性都可以添加进行过滤
Map<String, String> params=new HashMap<>();
params.put("areaId", "center");
System.err.println(utils.execute(utils.getUrl("departments/SDSL2015"), Method.GET, params, null, null));
}
/**
* <b>方法名</b>: getHeaders
* <p><b>描 述</b>: 获取响应headers</p>
*
* @return 响应的hreaders
*
* <p><b>创建日期</b>:2016年10月26日 上午9:14:18</p>
* <p><b>修改日期</b>:</p>
* @author SN_AnJia([email protected])
* @version 1.0
* @since jdk 1.8
*/
public Map<String, String> getHeaders() {
return headers;
}
/**
* <b>方法名</b>: setHeaders
* <p><b>描 述</b>: 设置请求headers</p>
*
* @param headers 请求header e.g. Authorization
*
* <p><b>创建日期</b>:2016年10月26日 上午9:14:42</p>
* <p><b>修改日期</b>:</p>
* @author SN_AnJia([email protected])
* @version 1.0
* @since jdk 1.8
*/
public void setHeaders(Map<String, String> headers) {
this.headers = headers;
}
/**
* <b>方法名</b>: getBody
* <p><b>描 述</b>: 获取响应的body</p>
*
* @return 响应body
*
* <p><b>创建日期</b>:2016年10月26日 上午9:15:17</p>
* <p><b>修改日期</b>:</p>
* @author SN_AnJia([email protected])
* @version 1.0
* @since jdk 1.8
*/
public String getBody() {
return body;
}
/**
* <b>方法名</b>: getCode
* <p><b>描 述</b>: 获取http状态码</p>
*
* @return http状态码 e.g. 200,201,401,404,500
*
* <p><b>创建日期</b>:2016年10月26日 上午9:15:46</p>
* <p><b>修改日期</b>:</p>
* @author SN_AnJia([email protected])
* @version 1.0
* @since jdk 1.8
*/
public int getCode() {
return code;
}
/**
* <b>方法名</b>: getCodeMsg
* <p><b>描 述</b>: 获取http状态信息</p>
*
* @return http状态信息 e.g. OK,Unauthorized
*
* <p><b>创建日期</b>:2016年10月26日 上午9:16:22</p>
* <p><b>修改日期</b>:</p>
* @author SN_AnJia([email protected])
* @version 1.0
* @since jdk 1.8
*/
public String getCodeMsg() {
return codeMsg;
}
}