微服务间调用使用的是 Eureka, Ribbon, Hystrix and Feign

Eureka Eureka 是一个基于 REST 的服务,它主要是用于定位服务,以实现 AWS 云端的负载均衡和中间层服务器的故障转移。此处主要是指微服务注册中心 Ribbon 提供客户侧的软件负载均衡算法, Hystrix 是一个断路器,主要是解决当某个方法调用失败的时候,调用后备方法来替代失败的方法,达到容错,阻止级联错误等功能 Feign 是一个声明web服务客户端

规范

  1. 接口统一命名 微服务简写FeignClient.java 例如 HospitalFeignClient.java

  2. 错误回调类统一命名 微服务简写FeignClientFallback.java 例如 HospitalFeignClientFallback.java

  3. 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;
    }
}

results matching ""

    No results matching ""