注意:此文档描述的内容正在建设中或处于功能早期阶段,请持续关注文档更新!
基于 spring web 和 resteasy 注解编码风格,通过http协议进行服务间调用互通,dubbo protocol扩展实现的协议
相对于原生的http协议dubbo http 请求增加version和group两个header用于确定服务的唯一, 如果provider一端没有声明group和version,http请求时就不需要传递这连个header,反之必须要传递目标 服务的group和version, 如果使用dubbo http的RestClient这两个header将会默认通过attachment传递 为区别于其他的header,attachment将会增加rest-service-前缀,因此通过其他形式的http client调用 dubbo http服务需要传递 rest-service-version 和 rest-service-group 两个header
POST /test/path HTTP/1.1
Host: localhost:8080
Content-type: application/json
Accept: text/html
rest-service-version: 1.0.0
rest-service-group: dubbo
{"name":"dubbo","age":10,"address":"hangzhou"}
HTTP/1.1 200
Content-Type: text/html
Content-Length: 4
Date: Fri, 28 Apr 2023 14:16:42 GMT
"success"
目前支持以上media,后面还会对type进行扩展
详细的依赖以及spring配置,可以参见dubbo 项目的dubbo-demo-xml模块 https://github.com/apache/dubbo/tree/3.2/dubbo-demo/dubbo-demo-xml
API
@RequestMapping("/spring/demo/service")
public interface SpringRestDemoService {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
Integer hello(@RequestParam("a") Integer a, @RequestParam("b") Integer b);
@RequestMapping(method = RequestMethod.GET, value = "/error")
String error();
@RequestMapping(method = RequestMethod.POST, value = "/say")
String sayHello(@RequestBody String name);
@RequestMapping(method = RequestMethod.POST, value = "/testFormBody", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
Long testFormBody(@RequestBody Long number);
@RequestMapping(method = RequestMethod.POST, value = "/testJavaBeanBody", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
User testJavaBeanBody(@RequestBody User user);
@RequestMapping(method = RequestMethod.GET, value = "/primitive")
int primitiveInt(@RequestParam("a") int a, @RequestParam("b") int b);
@RequestMapping(method = RequestMethod.GET, value = "/primitiveLong")
long primitiveLong(@RequestParam("a") long a, @RequestParam("b") Long b);
@RequestMapping(method = RequestMethod.GET, value = "/primitiveByte")
long primitiveByte(@RequestParam("a") byte a, @RequestParam("b") Long b);
@RequestMapping(method = RequestMethod.POST, value = "/primitiveShort")
long primitiveShort(@RequestParam("a") short a, @RequestParam("b") Long b, @RequestBody int c);
@RequestMapping(method = RequestMethod.GET, value = "/testMapParam")
String testMapParam(@RequestParam Map<String, String> params);
@RequestMapping(method = RequestMethod.GET, value = "/testMapHeader")
String testMapHeader(@RequestHeader Map<String, String> headers);
@RequestMapping(method = RequestMethod.POST, value = "/testMapForm", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
List<String> testMapForm(MultiValueMap<String, String> params);
@RequestMapping(method = RequestMethod.GET, value = "/headerInt")
int headerInt(@RequestHeader("header") int header);
}
Provider
@DubboService(interfaceClass = SpringRestDemoService.class ,protocol = "rest")
public class SpringRestDemoServiceImpl implements SpringRestDemoService {
@Override
public String sayHello(String name) {
return "Hello, " + name;
}
@Override
public Long testFormBody(Long number) {
return number;
}
@Override
public User testJavaBeanBody(User user) {
return user;
}
@Override
public int primitiveInt(int a, int b) {
return a + b;
}
@Override
public long primitiveLong(long a, Long b) {
return a + b;
}
@Override
public long primitiveByte(byte a, Long b) {
return a + b;
}
@Override
public long primitiveShort(short a, Long b, int c) {
return a + b;
}
@Override
public String testMapParam(Map<String, String> params) {
return params.get("param");
}
@Override
public String testMapHeader(Map<String, String> headers) {
return headers.get("header");
}
@Override
public List<String> testMapForm(MultiValueMap<String, String> params) {
return params.get("form");
}
@Override
public int headerInt(int header) {
return header;
}
@Override
public Integer hello(Integer a, Integer b) {
return a + b;
}
@Override
public String error() {
throw new RuntimeException("test error");
}
}
Consumer
@Component
public class SpringRestDemoServiceConsumer {
@DubboReference(interfaceClass = SpringRestDemoService.class )
SpringRestDemoService springRestDemoService;
public void invoke(){
String hello = springRestDemoService.sayHello("hello");
assertEquals("Hello, hello", hello);
Integer result = springRestDemoService.primitiveInt(1, 2);
Long resultLong = springRestDemoService.primitiveLong(1, 2l);
long resultByte = springRestDemoService.primitiveByte((byte) 1, 2l);
long resultShort = springRestDemoService.primitiveShort((short) 1, 2l, 1);
assertEquals(result, 3);
assertEquals(resultShort, 3l);
assertEquals(resultLong, 3l);
assertEquals(resultByte, 3l);
assertEquals(Long.valueOf(1l), springRestDemoService.testFormBody(1l));
MultiValueMap<String, String> forms = new LinkedMultiValueMap<>();
forms.put("form", Arrays.asList("F1"));
assertEquals(Arrays.asList("F1"), springRestDemoService.testMapForm(forms));
assertEquals(User.getInstance(), springRestDemoService.testJavaBeanBody(User.getInstance()));
}
private void assertEquals(Object returnStr, Object exception) {
boolean equal = returnStr != null && returnStr.equals(exception);
if (equal) {
return;
} else {
throw new RuntimeException();
}
}
}
API
@Path("/jaxrs/demo/service")
public interface JaxRsRestDemoService {
@GET
@Path("/hello")
Integer hello(@QueryParam("a") Integer a, @QueryParam("b") Integer b);
@GET
@Path("/error")
String error();
@POST
@Path("/say")
String sayHello(String name);
@POST
@Path("/testFormBody")
Long testFormBody(@FormParam("number") Long number);
@POST
@Path("/testJavaBeanBody")
@Consumes({MediaType.APPLICATION_JSON})
User testJavaBeanBody(User user);
@GET
@Path("/primitive")
int primitiveInt(@QueryParam("a") int a, @QueryParam("b") int b);
@GET
@Path("/primitiveLong")
long primitiveLong(@QueryParam("a") long a, @QueryParam("b") Long b);
@GET
@Path("/primitiveByte")
long primitiveByte(@QueryParam("a") byte a, @QueryParam("b") Long b);
@POST
@Path("/primitiveShort")
long primitiveShort(@QueryParam("a") short a, @QueryParam("b") Long b, int c);
@GET
@Path("testMapParam")
@Produces({MediaType.TEXT_PLAIN})
@Consumes({MediaType.TEXT_PLAIN})
String testMapParam(@QueryParam("test") Map<String, String> params);
@GET
@Path("testMapHeader")
@Produces({MediaType.TEXT_PLAIN})
@Consumes({MediaType.TEXT_PLAIN})
String testMapHeader(@HeaderParam("test") Map<String, String> headers);
@POST
@Path("testMapForm")
@Produces({MediaType.APPLICATION_JSON})
@Consumes({MediaType.APPLICATION_FORM_URLENCODED})
List<String> testMapForm(MultivaluedMap<String, String> params);
@POST
@Path("/header")
@Consumes({MediaType.TEXT_PLAIN})
String header(@HeaderParam("header") String header);
@POST
@Path("/headerInt")
@Consumes({MediaType.TEXT_PLAIN})
int headerInt(@HeaderParam("header") int header);
}
Provider
@DubboService(interfaceClass =JaxRsRestDemoService.class ,protocol = "rest",version = "1.0.0",group = "test")
public class JaxRsRestDemoServiceImpl implements JaxRsRestDemoService {
@Override
public String sayHello(String name) {
return "Hello, " + name;
}
@Override
public Long testFormBody(Long number) {
return number;
}
@Override
public User testJavaBeanBody(User user) {
return user;
}
@Override
public int primitiveInt(int a, int b) {
return a + b;
}
@Override
public long primitiveLong(long a, Long b) {
return a + b;
}
@Override
public long primitiveByte(byte a, Long b) {
return a + b;
}
@Override
public long primitiveShort(short a, Long b, int c) {
return a + b;
}
@Override
public String testMapParam(Map<String, String> params) {
return params.get("param");
}
@Override
public String testMapHeader(Map<String, String> headers) {
return headers.get("header");
}
@Override
public List<String> testMapForm(MultivaluedMap<String, String> params) {
return params.get("form");
}
@Override
public String header(String header) {
return header;
}
@Override
public int headerInt(int header) {
return header;
}
@Override
public Integer hello(Integer a, Integer b) {
return a + b;
}
@Override
public String error() {
throw new RuntimeException("test error");
}
}
Consumer
@Component
public class JaxRsRestDemoService {
@DubboReference(interfaceClass = JaxRsRestDemoService.class)
JaxRsRestDemoService jaxRsRestDemoService;
public void jaxRsRestDemoServiceTest(ClassPathXmlApplicationContext context) {
JaxRsRestDemoService jaxRsRestDemoService = context.getBean("jaxRsRestDemoService", JaxRsRestDemoService.class);
String hello = jaxRsRestDemoService.sayHello("hello");
assertEquals("Hello, hello", hello);
Integer result = jaxRsRestDemoService.primitiveInt(1, 2);
Long resultLong = jaxRsRestDemoService.primitiveLong(1, 2l);
long resultByte = jaxRsRestDemoService.primitiveByte((byte) 1, 2l);
long resultShort = jaxRsRestDemoService.primitiveShort((short) 1, 2l, 1);
assertEquals(result, 3);
assertEquals(resultShort, 3l);
assertEquals(resultLong, 3l);
assertEquals(resultByte, 3l);
assertEquals(Long.valueOf(1l), jaxRsRestDemoService.testFormBody(1l));
MultivaluedMapImpl<String, String> forms = new MultivaluedMapImpl<>();
forms.put("form", Arrays.asList("F1"));
assertEquals(Arrays.asList("F1"), jaxRsRestDemoService.testMapForm(forms));
assertEquals(User.getInstance(), jaxRsRestDemoService.testJavaBeanBody(User.getInstance()));
}
}
因为dubbo http consumer一端实现http 调用的RestClient 实现有三种形式:httpclient,okhttp,URLConnection(jdk内置) 默认请情况下采用okhttp,因此在使用dubbo http 去调用其他http服务时,需要添加引入的依赖有
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-rpc-rest</artifactId>
<version>${dubbo-rpc-rest_version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>${okhttp_version}</version>
</dependency>
或
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${httpclient_version}</version>
</dependency>
/**
* URL rest://localhost:8888/services
* rest: protocol
* localhost:8888: server address
* services: context path
*/
@DubboReference(interfaceClass = HttpService.class ,url = "rest://localhost:8888/services",version = "1.0.0",group = "test")
HttpService httpService;
public void invokeHttpService() {
String http = httpService.http("Others Java Architecture Invoke Dubbo Rest");
System.out.println(http);
}
跨语言调用
python
import requests
url = 'http://localhost:8888/services/curl'
headers = {
'rest-service-group': 'test',
'rest-service-version': '1.0.0'
}
response = requests.get(url, headers=headers)
go
import (
"fmt"
"net/http"
)
func main() {
url := "http://localhost:8888/services/curl"
req, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Set("rest-service-group", "test")
req.Header.Set("rest-service-version", "1.0.0")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error sending request:", err)
return
}
defer resp.Body.Close()
多协议发布
@DubboService(interfaceClass = HttpService.class, protocol = "rest,dubbo", version = "1.0.0", group = "test")
public class HttpServiceImpl implements HttpService {
@Override
public String http(String invokeType) {
return "Rest http request test success! by invokeType: " + invokeType;
}
}
public class HttpClientInvoke {
private final String versionHeader = RestHeaderEnum.VERSION.getHeader();
private final String groupHeader = RestHeaderEnum.GROUP.getHeader();
/**
* contextPath services
*/
private final String url = "http://localhost:8888/services/http";
public void httpServiceHttpClientInvoke() throws IOException {
CloseableHttpClient httpClient = createHttpClient();
HttpRequestBase httpUriRequest = new HttpGet(url);
httpUriRequest.addHeader(versionHeader, "1.0.0");
httpUriRequest.addHeader(RestConstant.ACCEPT, "text/plain");
httpUriRequest.addHeader(groupHeader, "test");
httpUriRequest.addHeader("type", "Http Client Invoke Dubbo Rest Service");
CloseableHttpResponse response = httpClient.execute(httpUriRequest);
RestResult restResult = parseResponse(response);
System.out.println(new String(restResult.getBody()));
}
private RestResult parseResponse(CloseableHttpResponse response) {
return new RestResult() {
@Override
public String getContentType() {
return response.getFirstHeader("Content-Type").getValue();
}
@Override
public byte[] getBody() throws IOException {
if (response.getEntity() == null) {
return new byte[0];
}
return IOUtils.toByteArray(response.getEntity().getContent());
}
@Override
public Map<String, List<String>> headers() {
return Arrays.stream(response.getAllHeaders()).collect(Collectors.toMap(Header::getName, h -> Collections.singletonList(h.getValue())));
}
@Override
public byte[] getErrorResponse() throws IOException {
return getBody();
}
@Override
public int getResponseCode() {
return response.getStatusLine().getStatusCode();
}
@Override
public String getMessage() throws IOException {
return appendErrorMessage(response.getStatusLine().getReasonPhrase(),
new String(getErrorResponse()));
}
};
}
private CloseableHttpClient createHttpClient() {
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
return HttpClients.custom().setConnectionManager(connectionManager).build();
}
}
因为 Dubbo Java 3.2 内部移除了原本 Resteasy 的实现,因此在对 Resteasy 内置的 Response,extend,ExceptionMapper 支持上将会有所变化 ExceptionMapper 转换成了org.apache.dubbo.rpc.protocol.rest.exception.mapper.ExceptionHandler,Response后面也会做适配处理
版本 | JaxRs | j2ee | web容器(tomcat/jetty) | spring web | http client(okhttp/httpclient/jdk URLConnnection ) |
---|---|---|---|---|---|
3.0 | 依赖与resteasy的client和server | 遵循j2ee规范 | 依赖常见web容器 | 不依赖 | 不依赖 |
3.2 | 不依赖(仅需要JaxRs注解包) | 不遵循 | netty实现的http服务器 | 仅依赖spring web注解 | 内部实现RestClient依赖http client(默认为okhttp) |