SpringCloud系列--4.Feign
在了解Feign之前,我们先熟悉下rest客户端的Apache CXF 以及 Restlet这两款webservice框架
编写一个服务端,此服务端有几个方法
- person/createXML2 xml传参以及返回
- person/create json格式参数以及
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.crazyit.cloud</groupId>
<artifactId>rest-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>1.5.4.RELEASE</version>
</dependency>
<!--
服务器端要加上这个依赖,否则客户端在请求时会报以下异常:
Exception in thread "main" feign.FeignException: status 415 reading PersonClient#createPersonXML(Person); content:
{"timestamp":1502705981406,"status":415,"error":"Unsupported Media Type","exception":"org.springframework.web.HttpMediaTypeNotSupportedException","message":"Content type 'application/xml;charset=UTF-8' not supported","path":"/person/createXML"}
-->
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-xml-provider</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
Person.java 注意里面 @JacksonXmlxxx注解的作用,一会看操作结果
package org.crazyit.cloud;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
@JacksonXmlRootElement(localName = "PersonAlias")
public class Person {
private Integer id;
@JacksonXmlProperty(localName = "nameAlias")
private String name;
private Integer age;
private String message;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
启动类
package org.crazyit.cloud;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
@SpringBootApplication
public class ServerApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ServerApplication.class).run(args);
}
}
Controller类
package org.crazyit.cloud;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyController {
/**
* 查询方法,参数为Person的id
*/
@RequestMapping(value = "/person/{personId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public Person findPerson(@PathVariable("personId") Integer personId,
HttpServletRequest request) {
Person p = new Person();
p.setId(personId);
p.setName("Crazyit");
p.setAge(30);
p.setMessage(request.getRequestURL().toString());
return p;
}
@RequestMapping(value = "/hello", method = RequestMethod.GET)
@ResponseBody
public String hello() {
return "Hello World";
}
/**
* 参数为JSON
*/
@RequestMapping(value = "/person/create", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String createPerson(@RequestBody Person person) {
System.out.println(person.getName() + "-" + person.getAge());
return "Success, Person Id: " + person.getId();
}
/**
* 参数与返回值均为XML
*/
@RequestMapping(value = "/person/createXML", method = RequestMethod.POST,
consumes = MediaType.APPLICATION_XML_VALUE,
produces = MediaType.APPLICATION_XML_VALUE)
@ResponseBody
public String createXMLPerson(@RequestBody Person person) {
System.out.println(person.getName() + "-" + person.getId());
return "<result><message>success</message></result>";
}
@RequestMapping(value = "/person/createXML2", method = RequestMethod.POST,
consumes = MediaType.APPLICATION_XML_VALUE,
produces = MediaType.APPLICATION_XML_VALUE)
@ResponseBody
public Person createXMLPerson2(@RequestBody Person person) {
System.out.println(person.getName() + "-" + person.getId());
return person;
}
}
分别使用cxf 与restlet调用
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.crazyit.cloud</groupId>
<artifactId>rest-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<!-- CXF -->
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-core</artifactId>
<version>3.1.10</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-rs-client</artifactId>
<version>3.1.10</version>
</dependency>
<!-- Restlet -->
<dependency>
<groupId>org.restlet.jee</groupId>
<artifactId>org.restlet</artifactId>
<version>2.3.10</version>
</dependency>
<dependency>
<groupId>org.restlet.jee</groupId>
<artifactId>org.restlet.ext.jackson</artifactId>
<version>2.3.10</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>maven-restlet</id>
<name>Restlet repository</name>
<url>http://maven.restlet.org</url>
</repository>
</repositories>
</project>
cxf示例如下:
package org.crazyit.cloud;
import java.io.InputStream;
import javax.ws.rs.core.Response;
import org.apache.cxf.helpers.IOUtils;
import org.apache.cxf.jaxrs.client.WebClient;
/**
* CXF访问客户端
* @author 杨恩雄
*
*/
public class CxfClient {
public static void main(String[] args) throws Exception {
// 创建WebClient
WebClient client = WebClient.create("http://localhost:8080/person/1");
// 获取响应
Response response = client.get();
// 获取响应内容
InputStream ent = (InputStream) response.getEntity();
String content = IOUtils.readStringFromStream(ent);
// 输出字符串
System.out.println(content);
}
}
restlet示例如下:
package org.crazyit.cloud;
import java.util.HashMap;
import java.util.Map;
import org.restlet.data.MediaType;
import org.restlet.ext.jackson.JacksonRepresentation;
import org.restlet.representation.Representation;
import org.restlet.resource.ClientResource;
/**
* Restlet客户端
* @author 杨恩雄
*
*/
public class RestletClient {
public static void main(String[] args) throws Exception {
ClientResource client = new ClientResource(
"http://localhost:8080/person/1");
// 调用get方法,服务器发布的是GET
Representation response = client.get(MediaType.APPLICATION_JSON);
// 创建JacksonRepresentation实例,将响应转换为Map
JacksonRepresentation jr = new JacksonRepresentation(response,
HashMap.class);
// 获取转换后的Map对象
Map result = (HashMap) jr.getObject();
// 输出结果
System.out.println(result.get("id") + "-" + result.get("name") + "-"
+ result.get("age") + "-" + result.get("message"));
}
}
单独使用Feign
package org.crazyit.cloud;
import feign.RequestLine;
/**
* 客户端调用的服务接口
* @author 杨恩雄
*
*/
public interface HelloClient {
@RequestLine("GET /hello")
String sayHello();
}
package org.crazyit.cloud;
import feign.Feign;
import feign.gson.GsonDecoder;
public class HelloMain {
public static void main(String[] args) {
// 调用Hello接口
HelloClient hello = Feign.builder().target(HelloClient.class,
"http://localhost:8080/");
System.out.println(hello.getClass().getName());
System.out.println(hello.sayHello());
}
}
package org.crazyit.cloud;
import feign.Headers;
import lombok.Data;
import lombok.NoArgsConstructor;
import feign.Param;
import feign.RequestLine;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
* Person客户端服务类
* @author 杨恩雄
*
*/
public interface PersonClient {
@RequestLine("GET /person/{personId}")
Person findById(@Param("personId") Integer personId);
@RequestLine("POST /person/create")
@Headers("Content-Type: application/json")
String createPerson(Person person);
@RequestLine("POST /person/createXML")
@Headers("Content-Type: application/XML")
String createPersonxml(Person person);
@Data // 为所有属性加上setter和getter等方法
@XmlRootElement
class Person {
Integer id;
String name;
Integer age;
String message;
String nameAlias;
}
}
package org.crazyit.cloud;
import feign.gson.GsonEncoder;
import feign.jaxb.JAXBContextFactory;
import feign.jaxb.JAXBDecoder;
import feign.jaxb.JAXBEncoder;
import org.crazyit.cloud.PersonClient.Person;
import feign.Feign;
import feign.gson.GsonDecoder;
/**
* Person服务的运行主类
* @author 杨恩雄
*
*/
public class PersonMain {
public static void main(String[] args) {
PersonClient personService = Feign.builder()
.decoder(new GsonDecoder())
.target(PersonClient.class, "http://localhost:8080/");
Person person = personService.findById(2);
System.out.println(person.id);
System.out.println(person.name);
System.out.println(person.age);
System.out.println(person.message);
PersonClient personClient = Feign.builder().encoder(new GsonEncoder())
.target(PersonClient.class, "http://localhost:8080/");
Person p = new Person();
p.id = 1;
p.name = "Person Name";
p.age = 33;
String response = personClient.createPerson(p);
System.out.println(response);
JAXBContextFactory jaxbContextFactory = new JAXBContextFactory.Builder().build();
PersonClient pClient = Feign.builder().encoder(new JAXBEncoder(jaxbContextFactory))
// .decoder(new JAXBDecoder(jaxbContextFactory))
.target(PersonClient.class, "http://localhost:8080");
PersonClient.Person paramP = new PersonClient.Person();
paramP.id = 1;
paramP.name = "xml";
paramP.age = 88;
// paramP.nameAlias = "aliasxml";
//调用接口并返回参数
String result = pClient.createPersonxml(paramP);
System.out.println(result);
}
}
注意:
Feign使用第三方注解
为了让Feign知道注解的作用,一般都会在调用时候指定contract(new xxxContract())
我们自定义注解@MyUrl
package org.crazyit.feign.contract;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
/**
* 自定义的注解
* @author 杨恩雄
*
*/
@Target(METHOD)
@Retention(RUNTIME)
public @interface MyUrl {
// 定义url与method属性
String url();
String method();
}
package org.crazyit.feign.contract;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import feign.Contract;
import feign.MethodMetadata;
/**
* 自定义Contract
* @author 杨恩雄
*
*/
public class MyContract extends Contract.BaseContract {
@Override
protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
}
/**
* 用于处理方法级的注解
*/
protected void processAnnotationOnMethod(MethodMetadata data,
Annotation annotation, Method method) {
// 是MyUrl注解才进行处理
if(MyUrl.class.isInstance(annotation)) {
// 获取注解的实例
MyUrl myUrlAnn = method.getAnnotation(MyUrl.class);
// 获取配置的HTTP方法
String httpMethod = myUrlAnn.method();
// 获取服务的url
String url = myUrlAnn.url();
// 将值设置到模板中
data.template().method(httpMethod);
data.template().append(url);
}
}
@Override
protected boolean processAnnotationsOnParameter(MethodMetadata data,
Annotation[] annotations, int paramIndex) {
return false;
}
}
package org.crazyit.feign.contract;
/**
* 服务客户端
* @author 杨恩雄
*
*/
public interface HelloClient {
@MyUrl(method = "GET", url = "/hello")
String myHello();
}
package org.crazyit.feign.contract;
import org.crazyit.feign.MyFeignClient;
import org.crazyit.feign.PersonClient;
import feign.Feign;
import feign.gson.GsonEncoder;
public class ContractTest {
public static void main(String[] args) {
// 获取服务接口
HelloClient helloClient = Feign.builder()
.contract(new MyContract())
.target(HelloClient.class, "http://localhost:8080/");
// 请求Hello World接口
String result = helloClient.myHello();
System.out.println(" 接口响应内容:" + result);
}
}
运行ContractTest会访问http://localhost:8080/hello 接口,并返回响应。
PS: MyContract类中,需要实现三个方法,分别是处理类注解,处理方法注解,处理参数注解的方法,由于我们只定义了一个方法注解@MyUrl,因此实现processAnnotationOnMethod即可。
请求拦截器
Feign支持请求拦截器,在发送请求前可以对发送的模板进行操作,例如设置请求头的属性等,我们自定义请求拦截器实现RequestInterceptor,
package org.crazyit.feign.interceptor;
import feign.RequestInterceptor;
import feign.RequestTemplate;
public class MyInterceptor implements RequestInterceptor {
public void apply(RequestTemplate template) {
template.header("Content-Type", "application/json");
}
}
package org.crazyit.feign.interceptor;
import org.crazyit.feign.PersonClient;
import feign.Feign;
public class InterceptorTest {
public static void main(String[] args) {
// 获取服务接口
PersonClient personClient = Feign.builder()
.requestInterceptor(new MyInterceptor())
.target(PersonClient.class, "http://localhost:8080/");
System.out.println(personClient.sayHello());
}
}
我们也可以定义自己的Client实现类实现Client接口
package org.crazyit.feign;
import java.io.IOException;
import java.net.URI;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import feign.Client;
import feign.Request;
import feign.Request.Options;
import feign.Response;
/**
* 自定义的Feign客户
*
* @author 杨恩雄
*
*/
public class MyFeignClient implements Client {
public Response execute(Request request, Options options)
throws IOException {
System.out.println("==== 这是自定义的Feign客户端");
try {
// 创建一个默认的客户端
CloseableHttpClient httpclient = HttpClients.createDefault();
// 获取调用的HTTP方法
final String method = request.method();
// 创建一个HttpClient的HttpRequest
HttpRequestBase httpRequest = new HttpRequestBase() {
public String getMethod() {
return method;
}
};
// 设置请求地址
httpRequest.setURI(new URI(request.url()));
// 执行请求,获取响应
HttpResponse httpResponse = httpclient.execute(httpRequest);
// 获取响应的主体内容
byte[] body = EntityUtils.toByteArray(httpResponse.getEntity());
// 将HttpClient的响应对象转换为Feign的Response
Response response = Response.builder()
.body(body)
.headers(new HashMap<String, Collection<String>>())
.status(httpResponse.getStatusLine().getStatusCode())
.build();
return response;
} catch (Exception e) {
throw new IOException(e);
}
}
}
package org.crazyit.feign;
import org.crazyit.feign.PersonClient.Person;
import feign.Feign;
import feign.gson.GsonEncoder;
public class MyClientTest {
public static void main(String[] args) {
// 获取服务接口
PersonClient personClient = Feign.builder()
.encoder(new GsonEncoder())
.client(new MyFeignClient())
.target(PersonClient.class, "http://localhost:8080/");
// 请求Hello World接口
String result = personClient.sayHello();
System.out.println(" 接口响应内容:" + result);
}
}
输出
==== 这是自定义的Feign客户端
接口响应内容:Hello World
接口日志:
设置接口日志级别:
NONE: 默认值,不进行日志记录
BASIC: 记录请求方法,URL,响应状态码和执行时间
HEADERS: 除了BASIC记录的信息外,还包括请求头和响应头
FULL: 记录全部日志,包括请求头,请求体,请求与响应的元数据。
package org.crazyit.feign;
import feign.Feign;
import feign.Logger;
public class TestLog {
public static void main(String[] args) {
// 获取服务接口
PersonClient personClient = Feign.builder()
.logLevel(Logger.Level.FULL)
.logger(new Logger.JavaLogger().appendToFile("/Users/apple/coding/springcloud/codes/05/5.2/feign-use/logs/http.log"))
.target(PersonClient.class, "http://localhost:8080/");
System.out.println(personClient.sayHello());;
}
}
上面示例中日志文件测试时候用相对路径会报错,如果具体使用到项目中需进行排查测试。
.logger(new Logger.JavaLogger().appendToFile("logs/http.log"))
这样会报错。