演示用GitHub地址:https://github.com/suyin58/dubbo-rest-example
1 Dubbo_rest介紹
Dubbo自2.6.0版本后,合並了dubbox的restful風格的接口暴露方式,其restful的處理采用的是jboss.resteasy框架。使用該功能可以簡便的將dubbo服務直接通過http的方式發布,不需要再使用中轉的http應用暴露服務。
如上圖,原有結構中,HTTP訪問需要通過API應用中轉服務,RPC訪問調用dubbo應用,使用dubbo_rest之后,HTTP和RPC訪問都直接調用dubbo應用即可。
2 使用方法
參考demo項目 https://github.com/suyin58/dubbo-rest-example,
由於dubbo 2.6依賴jar包javax.json.bind-api是JDK1.8版本,因此建議使用JDK1.8版本,但是使用JDK1.7版本也可以,不影響以下介紹的功能點。
2.1 POM依賴
2.1.1 Dubbo依賴
2.1.2 Resteasy依賴
2.2 接口暴露
2.2.1 服務協議配置
在dubbo_service.xml中增加dubbo:protocol name="rest"顯式申明提供rest服務。
2.2.2 接口服務配置
同原有Dubbo服務一樣,將Dubbo服務發布出來,需要使用<dubbo:service顯式申明。
2.3 服務注解
所有在<dubbo:service顯式申明提供服務的接口實現類(也可以加載接口中,設計原則建議放在實現類做具體操作)中,需要增加@Path注解。
如果某個dubbo服務顯式進行了申明,但是沒有增加@Path注解,否則會應用無法啟動,並報錯【RESTEASY003130: Class is not a root resource. It, or one of its interfaces must be annotated with @Path】
其原因是在於resteasy中定義掃描並加載哪些類,是由dubbo提供的,dubbo將所有顯式申明的<dubbo:service都被掃描,如果某個類沒有@Path則會報錯
參考:https://blog.csdn.net/wtopps/article/details/76919008
2.4 方法注解
2.5 參數注解
l @PathParam -->url路徑問號前的參數
請求路徑:http://www.wjs.com/product/111/detail
Path路徑:@Path(“product/{productCode}/detail”)
參數注解:detail(@PathParam(“productCode”) code){
l @QueryParam -->url路徑問號后中的參數
請求路徑:http://www.wjs.com/product/detail?productCode=111
Path路徑:@Path(“product/detail”)
參數注解:detail(@QueryParam(“productCode”) code)
l @FormParam -->x-www.form-urlencoded參數
值 |
描述 |
application/x-www-form-urlencoded |
在發送前編碼所有字符(默認) |
multipart/form-data |
不對字符編碼。 在使用包含文件上傳控件的表單時,必須使用該值。 |
通過Form表單提交的請求,需要區分是普通form表單(enctype=application/x-www-form-urlencoded),還是文件上傳表單(enctype=multipart/form-data)。
普通表單可以在方法體中使用@FormParam注解,也可以在類的屬性中使用@FormParam注解。文件表單,由於服務端獲取到的是個文件六,不能在方法體中使用@FormParam注解,但是可以在MultipartForm注解的類中使用@FormParam注解。
l @BeanParam – 對象屬性賦值
如果接收參數處理是個對象的話,可以使用@BeanParam注解對象獲取參數
參數注解:pageListTemplate(@BeanParam DeductTplQry qry)
對象屬性注解:
l @MultipartForm -- multipart/form-data表單參數
如果是文件上傳,那么需要通過@MultipartForm注解獲取對象參數
參數注解:upload(@MultipartForm DiskFile diskFile,
對象屬性注解:
在這里需要注意的是,文件上傳由於resteasy框架的缺陷,無法自動獲取流中的文件名稱,需要通過前端的form表單提供並傳給后台。
l @Context
如果需要部分HTTP上下環境參數的話,例如request或者response的話,可以通過@Context注解獲取。
參數注解:httparg(@Context HttpServletRequest request, @Context HttpServletResponse){
2.6 文件上傳/下載
2.6.1 單個文件上傳
單個文件上傳,參考@ MultipartForm注解說明
2.6.2 多個文件上傳
@MultipartForm不支持,使用MultipartFormDataInput的方式處理。
示例代碼:
@POST
@Path("/uploadmulti")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Override
public Object uploadmulti(MultipartFormDataInput input) {
System.out.println("進入業務邏輯");
// MultipartFormDataReader
Map<String, List<InputPart>> uploadForm = input.getFormDataMap();
InputStream inputStream = null;
OutputStream outStream = null;
final String DIRCTORY = "D:/temp/datainputmulti/";
//取得文件表單名
try {
for (Iterator<Entry<String, List<InputPart>>> it = uploadForm.entrySet().iterator() ; it.hasNext() ;) {
Entry<String, List<InputPart>> entry = it.next();
List<InputPart> inputParts = entry.getValue();
initDirectory(DIRCTORY);
for (InputPart inputPart : inputParts) {
// 文件名稱
String fileName = getFileName(inputPart.getHeaders());
inputStream = inputPart.getBody(InputStream.class, null);
//把文件流保存;
File file = new File(DIRCTORY + fileName);
intindex;
byte[] bytes = newbyte[1024];
outStream = new FileOutputStream(file);
while ((index = inputStream.read(bytes)) != -1) {
outStream.write(bytes, 0, index);
outStream.flush();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}finally {
if(null != inputStream){
try {
inputStream.close();
} catch (IOException e) {
}
}
if(null != outStream){
try {
outStream.close();
} catch (IOException e) {
}
}
}
return Response.ok().build();
}
異常處理:文件名稱獲取亂碼問題
MultipartFormDataInput的方式獲取文件名稱存在字符集亂碼的問題,需要通過重新編譯代碼的方式解決。解決方式參考:https://www.cnblogs.com/loveyou/p/9529856.html
異常處理:
2.6.3 文件下載
文件下載,通過參數的@Context獲取http Response,然后直接通過Response.outputstream往外面寫流即可。
示例代碼:
@GET
@Path("/download")
@Produces("application/json; charset=UTF-8")
@Override
publicvoid download(@QueryParam(value = "fileName") String fileName, @Context HttpServletRequest request, @Context HttpServletResponse response) {
InputStream in = null;
OutputStream out = null;
try {
fileName = "app.log";
String filePath = "D:\\logs\\manageplat\\" + fileName;
response.setHeader("content-disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
in = new FileInputStream(filePath); //獲取文件的流
intlen = 0;
bytebuf[] = newbyte[1024];//緩存作用
out = response.getOutputStream();//輸出流
while ((len = in.read(buf)) > 0) //切忌這后面不能加分號 ”;“
{
out.write(buf, 0, len);//向客戶端輸出,實際是把數據存放在response中,然后web服務器再去response中讀取
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3 封裝
3.1 權限攔截
攔截器的配置,在Dubbo的protocol協議中的extension顯式申明。
3.2 編碼攔截
編碼攔截在獲得請求的時候進行處理,需要繼承接口ContainerRequestFilter。
@Override
publicvoid filter(ContainerRequestContext requestContext) throws IOException {
System.err.println("進入請求攔截——filter");
// 編碼處理
request.setCharacterEncoding(ENCODING_UTF_8);
response.setCharacterEncoding(ENCODING_UTF_8);
request.setAttribute(InputPart.DEFAULT_CHARSET_PROPERTY, ENCODING_UTF_8);
requestContext.setProperty(InputPart.DEFAULT_CHARSET_PROPERTY, ENCODING_UTF_8);
// 客戶端head顯示提醒不要對返回值進行封裝
requestContext.setProperty("Not-Wrap-Result", requestContext.getHeaderString("Not-Wrap-Result") == null ? "" : requestContext.getHeaderString("Not-Wrap-Result"));
// 請求參數打印
logRequest(request);
}
3.3 異常處理
系統異常情況對異常結果進行封裝,需要繼承接口ExceptionMapper。
/**
* 異常攔截
*/
@Override
public Response toResponse(Exception e) {
// System.err.println("進入結果處理——toResponse");
String errMsg = e.getMessage();
JsonResult<Object> result = new JsonResult<>(false, StringUtils.isEmpty(errMsg)? "系統異常" : errMsg);
if(javax.ws.rs.ClientErrorException.class.isAssignableFrom(e.getClass())){
ClientErrorException ex = (ClientErrorException) e;
LOGGER.error("請求錯誤:" + e.getMessage());
returnex.getResponse();
}
if(einstanceof BaseException){
BaseException ex = (BaseException) e;
result.setData(ex.getErrorParams());
}
LOGGER.error(errMsg, e);
return Response.status(200).entity(result).build();
}
3.4 結果封裝
對結果封裝,需要繼承WriterInterceptor, ContainerResponseFilter,對200狀態碼的結果進行封裝處理,以及對異常狀態碼的結果進行封裝處理。
@Override
publicvoid aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
System.err.println("進入結果處理——aroundWriteTo");
// 針對需要封裝的請求對結構進行封裝處理。這里需要注意的是對返回類型已經是封裝類(比如:異常處理器的響應可能已經是封裝類型)時要忽略掉。
Object originalObj = context.getEntity();
String wrapTag = context.getProperty("Not-Wrap-Result") == null ? "" : context.getProperty("Not-Wrap-Result").toString(); // 客戶端顯示提醒不要對返回值進行封裝
Boolean wraped = originalObjinstanceof JsonResult; // 已經被封裝過了的,不用再次封裝
if (StringUtils.isBlank(wrapTag) && !wraped){
JsonResult<Object> result = new JsonResult<>(true, "執行成功");
result.setData(context.getEntity());
context.setEntity(result);
// 以下兩處set避免出現Json序列化的時候,對象類型不符的錯誤
context.setType(result.getClass());
context.setGenericType(result.getClass().getGenericSuperclass());
}
context.proceed();
}
@Override
publicvoid filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
System.err.println("進入結果處理——filter");
// 它的目的是專門處理方法返回類型是 void,或者某個資源類型返回是 null 的情況,
// 這種情況下JAX-RS 框架一般會返回狀態204,表示請求處理成功但沒有響應內容。我們對這種情況也重新處理改為操作成功
String wrapTag = requestContext.getProperty("Not-Wrap-Result") == null ? "" : requestContext.getProperty("Not-Wrap-Result").toString(); // 客戶端顯示提醒不要對返回值進行封裝
if (StringUtils.isBlank(wrapTag) &&responseContext.getStatus() == 204 && !responseContext.hasEntity()){
responseContext.setStatus(200);
responseContext.setEntity(new JsonResult<>(true, "執行成功"));
responseContext.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
}
3.5 客戶端申明不對結果做封裝
requestContext.getHeaderString("Not-Wrap-Result")
客戶端請求的時候,增加header”Not-Wrap-Result”