对于Restful风格应用的框架,平时用的最多的应该就是SpringBoot,SpringMVC了。但是也有另外一派,就是使用完全实现JavaEE标准的框架。所以本文会在完全实现JavaEE(相关JSR标准)的框架上进行讨论,不引入Spring。
以前在JavaEE(Java Enterprise Edition)中如何处理HTTP请求呢?
是使用Servlet,具体来说在所谓的“控制层”通过继承HttpServlet来进行处理.例如
public class indexServlet extends HttpServlet {
@Override
protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
// handle request
req.getParameter("parameterName");
......
// return response
resp.getWriter().write("something");
}
}
web.xml配置
<web-app
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>indexServletservlet-name>
<servlet-class>IndexServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>indexServletservlet-name>
<url-pattern>index/*url-pattern>
servlet-mapping>
web-app>
到后来可以支持@WebServlet达到和上面一样的效果
但是随着RESTFUl架构风格越来越成为主流,Java业界并没有对JavaEE中如何使用Restful风格进行一个标准的定义
JAX-RS是JavaEE规范的一部分,对应的JSR(Java Specification Request)-Java规范提案最新的是JSR370,对应JAX-RS版本2.1
在线查看JSR370地址
在JSR370目录中,我们可以看到整体的内容大概有12章。作为入门,我们简单了解第2、3、4章即可
A JAX-RS application consists of one or more resources (see Chapter 3) and zero or more providers (see Chapter 4). This chapter describes aspects of JAX-RS that apply to an application as a whole, subsequent chapters describe particular aspects of a JAX-RS application and requirements on JAX-RS implementations
可以看到,JAX-RS对于Application的定位是主要有Resource和Provider这两个内容组成。可以将它视为一个全局的servlet,处理HTTP的入口点。
可以看到这里只有一个servlet了,而不像传统的servlet项目,有多少个“控制器”,就要在web.xml中配置多少个servlet
一句话解释什么是resources
A resource class is a Java class that uses JAX-RS annotations to implement a corresponding Web resource. Resource classes are POJOs that have at least one method annotated with @Path or a request method designator
资源类是被@Path注解标注的方法所在的类,有过SpringMVC开发经验的人这里应该很好看懂。使用的注解也和SpringMVC大同小异。
例如 @GET, @POST, @PUT, @DELETE, @PATCH, @HEAD and @OPTIONS 标注HTTP方法类型
还有@QueryParam , @PathParam等接收url参数和路径参数。
@Produces和@Consumes分别代表响应类型、接受的请求类型
入门的话知道这几个注解就可以了。想要了解更多可以查看JSR370在线pdf的第3.2和3.5章
Providers in JAX-RS are responsible for various cross-cutting concerns such as filtering requests, converting representations into Java objects, mapping exceptions to responses, etc. A provider can be either pre-packaged in the JAX-RS runtime or supplied by an application. All application-supplied providers implement interfaces in the JAX-RS API and MAY be annotated with @Provider for automatic discovery purposes; the integration of pre-packaged providers into the JAX-RS runtime is implementation dependent. This chapter introduces some of the basic JAX-RS providers; other providers are introduced in Chapter 5 and Chapter 6
Provider是程序中公共的部分。比如各种Filter、读取和响应的处理(将JSON转成对应实体类类似这种)-如果在传统的servlet中,处理各种各样的Content-Type我们怎么处理?思路大概就是搞一个filter,拦截Request或者Response,自己再根据实际场景自己手动去进行HttpServletRequest.getParameters处理等等,在JAX-RS中,也提供了最基本的接口。如果我们要自己实现JAX-RS的话,实现这些基本的接口,利用上面说的这种思路,处理各种不同的情形以达到满足我们使用的需求,那么我们也可以称之为实现了JAX-RS标准。
自己也可以实现JAX-RS规范中的接口,并用@Provider注解标注,达到自定义Provider的目的。
上面提到过,JAX-RS只提供了最基本的接口,因为这些基本接口只是大家的共识。但是离真正在应用上使用,还需要实现这些接口才可以。如果大家都去各自实现这些接口,那么每个使用Restful风格的应用代码量也很多,例如最最原始的Servlet中,我们能拿到的就是request和response,如果想要处理各种请求和响应,是不是要对request做各种解析,对response的写入又做各种适配?为了帮助开发者专注于业务,降低开发Restful风格应用的难度,RestEasy出现了。它完全实现了JSR370标准,使我们普通的业务开发者,无需关心Request到各种类型的转换以及Response的各种响应处理等这些功能。直接在它之上做我们想要做的业务即可
本示例将使用Tomcat9作为JavaEE应用的部署容器。Tomcat是一个Servlet容器,并不是一个完全支持JavaEE标准的JavaEE容器。但是可以通过简单改造,可以使得Tomcat也可以部署JavaEE应用。
由于Tomcat9支持Servlet4,而RestEasy在Servlet3以上版本可以很轻松的配置。所以这里选择Tomcat9
@ApplicationPath("/auth")
public class AuthorizationServerApplication extends Application {
}
仅仅需要上述一行代码,便可以让RestEasy自动完成Restful应用的启动配置,而传统的Servlet下的应用,还有可能是需要配置web.xml。
ApplicationPath代表这个Web应用的起始路径。
注意:也有一些文章,例如上面介绍Application时文档给的示例那样。在介绍RestEasy入门使用的时候,会告知读者,需要在web.xml中配置带有ApplicationPath的类的完全限定名,但那都是针对Servlet3以前的web应用了。对于支持Servlet3以上的Servlet容器,使用RestEasy的时候是不需要配置的.有关更详细的信息请移步RestEasy官方文档关于Servlet容器配置
答案在于RestEasy实现了ServletContainerInitializer这个接口
这个接口是Servlet3.0添加的,可以查看最新的Servlet4.0规范文档的第四章Servlet Context
可以简单理解为在Servlet容器初始化时候做一些工作,可以参考详细的Javadoc
简单放几张截图来说明ResteasyServletInitializer做的事情
可以从这几张图里基本可以看出来,ResteasyServletInitializer做的事情就是注册Resource和Provider,并添加一个Servlet.
读者想了解更多,则直接去看ResteasyServletInitializer类的源码即可,很简洁的代码,不到200行。并且RestEasy是通过SPI的形式,将ResteasyServletInitializer注册进去
这样在容器启动完毕的时候,就注册了Resource、Provider并且添加了一个全局处理HTTP的Servlet
拓展:
在JAX-RS中, 被@Path注解的方法被定义为Resource.我们平时通过浏览器或其他方式(例如curl)发起的HTTP请求,请求服务器的时候,被视作对一个资源的访问。
接下来就给出几个基本的RestEasy的例子,来说明具体注解的使用。如果有SpringMVC的使用经验,应该很好理解
基本使用中给出普通GET、POST、DELETE的使用
/**
* basic use for RestEasy
*/
@Log
@Path("hello")
public class HelloResource {
private static List<UserVO> users = new ArrayList<>();
/**
* curl http:localhost:8080/auth/hello/jack
*/
@GET
@Path("{name}")
@Produces(MediaType.TEXT_PLAIN)
public String hello(@PathParam("name") String name) {
return "Hello " + name.toUpperCase();
}
/**
* RestEasy提供了高级的@PathParam注解,可以不不用声明path的值,只要变量名字和路径变量一致即可
* 使用文档
* 搭配Maven compiler插件使用
* curl http:localhost:8080/auth/hello/advanced/jack
*/
@GET
@Path("advanced/{name}")
@Produces(MediaType.TEXT_PLAIN)
@DenyAll
public String helloRestEasy(@org.jboss.resteasy.annotations.jaxrs.PathParam String name) {
return "Hi,there " + name;
}
/**
* curl -X POST -H 'Content-Type: application/json' -d '{"userName":"jack", "age":18"}' http://localhost:8080/auth/hello/users
*/
@POST
@Path("users")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response createUser(@Valid UserVO userVO) {
users.add(userVO);
return Response.ok().entity(userVO).build();
}
/**
* curl http:localhost:8080/auth/hello/users/0
*/
@GET
@Path("users/{index}")
@Produces(MediaType.APPLICATION_JSON)
public Response queryUser(@PathParam("index") Integer index) {
checkParameter(index);
UserVO targetUser = users.get(index);
return Response.ok().entity(targetUser).build();
}
/**
* curl -X DELETE http://localhost:8080/auth/hello/users/0
*/
@DELETE
@Path("users/{index}")
public Response deleteUser(@PathParam("index") Integer index) {
checkParameter(index);
users.remove(index);
return Response.ok().build();
}
private void checkParameter(Integer index) {
if (users.size() == 0 || Objects.isNull(index) || index < 0 || index >= users.size()) {
throw new WebApplicationException("user Not Found, please enter valid index", Response.Status.NOT_FOUND);
}
}
}
进阶使用给出处理表单和文件上传、下载的示例
private final String UPLOADED_FILE_PATH = "D:\\tmp\\";
/**
* 接收表单类型数据
* curl -X POST -d 'username=jack&age=18' http://localhost/auth/hello/users
*/
@POST
@Path("users")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response createFromFrom(@Form UserVO userVO) {
if (Objects.isNull(userVO)) {
throw new WebApplicationException(Status.BAD_REQUEST);
}
users.add(userVO);
return Response.ok().build();
}
/**
* 接受一个或多个文件.
* curl -F 'fileName=@pictureLocation/upload.png' http://localhost:8080/auth/hello/file
*/
@POST
@Path("file")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadImage(MultipartFormDataInput input) {
//Get API input data
Map<String, List<InputPart>> uploadForm = input.getFormDataMap();
List<InputPart> inputParts = uploadForm.get("fileName");
if (Objects.isNull(inputParts)) {
throw new WebApplicationException("no upload file, please upload file", Status.BAD_REQUEST);
}
StringBuilder uploadFileName = new StringBuilder();
for (InputPart inputPart : inputParts) {
// convert the uploaded file to inputstream
try(InputStream inputStream = inputPart.getBody(InputStream.class, null)) {
//Use this header for extra processing if required
MultivaluedMap<String, String> header = inputPart.getHeaders();
String fileName = getFileName(header);
uploadFileName.append(fileName).append(",");
// 有内存溢出的危险
byte[] bytes = IOUtils.toByteArray(inputStream);
// constructs upload file path
fileName = UPLOADED_FILE_PATH + fileName;
writeFile(bytes, fileName);
log.info("Success !!!!!");
} catch (Exception e) {
log.log(Level.WARNING, "upload file failed", e);
throw new WebApplicationException(e.getMessage(), Status.INTERNAL_SERVER_ERROR);
}
}
return Response.status(200)
.entity("File uploaded successfully.Uploaded file name : "+ uploadFileName.substring(0, uploadFileName.length()-1)).build();
}
/**
* header sample
* {
* Content-Type=[image/png],
* Content-Disposition=[form-data; name="file"; filename="filename.extension"]
* }
**/
private String getFileName(MultivaluedMap<String, String> header) {
String[] contentDisposition = header.getFirst("Content-Disposition").split(";");
for (String filename : contentDisposition) {
if ((filename.trim().startsWith("filename"))) {
String[] name = filename.split("=");
return name[1].trim().replaceAll("\"", "");
}
}
return "unknown";
}
private void writeFile(byte[] content, String filename) throws IOException {
File file = new File(filename);
if (!file.exists()) {
file.createNewFile();
}
FileOutputStream fop = new FileOutputStream(file);
fop.write(content);
fop.flush();
fop.close();
}
/**
* curl -o download.png http://localhost:8080/auth/hello/file/upload.png
* @param fileName
* @return
*/
@GET
@Path("file/{fileName}")
@Produces("image/png")
public Response downLoadImage(@org.jboss.resteasy.annotations.jaxrs.PathParam String fileName) {
if(fileName == null || fileName.isEmpty()) {
ResponseBuilder response = Response.status(Status.BAD_REQUEST);
return response.build();
}
//Prepare a file object with file to return
File file = new File(UPLOADED_FILE_PATH + fileName);
ResponseBuilder response = Response.ok(file);
response.header("Content-Disposition", "attachment; filename="+fileName);
return response.build();
}
相信看过上面的示例,读者可以很轻松的完成基本业务的开发了
更进一步,在上面的示例中,面对有问题的请求,我们是直接抛了一个异常,那么异常抛出之后,为了对客户端进行友好的提示,我们需要一个处理这种异常的机制。在SpringBoot中处理全局异常,我们使用@ExceptionHandler和@RestControllerAdvice来处理。JAX-RS也提供了类似的方式来处理,那就是实现ExceptionMapper接口,同时使用@Provider注解。结合这个示例是不是可以更好的理解了Provider在整个JAX-RS中的角色
@Provider
public class GlobalWebExceptionMapper implements ExceptionMapper<WebApplicationException> {
@Override
public Response toResponse(WebApplicationException e) {
return Response.status(e.getResponse().getStatus())
.type(MediaType.APPLICATION_JSON)
.entity(new ExceptionData(e.getMessage()))
.build();
}
}