RestEasy的入门与使用

文章目录

  • RestEasy的入门与使用
    • 背景
      • 传统Servlet
    • JAX-RS和JSR
      • 为什么要看规范?
      • 简单解读JSR370
        • Applications
        • Resources
        • Providers
    • RestEasy的基本使用
      • 建立Restful风格的JavaEE应用
        • RestEasy是如何接管了应用呢?
      • 接受Restful风格的HTTP请求
        • 基本使用
        • 进阶使用
      • 全局异常的处理
    • 备注
    • 更新日志

RestEasy的入门与使用

对于Restful风格应用的框架,平时用的最多的应该就是SpringBoot,SpringMVC了。但是也有另外一派,就是使用完全实现JavaEE标准的框架。所以本文会在完全实现JavaEE(相关JSR标准)的框架上进行讨论,不引入Spring。

背景

以前在JavaEE(Java Enterprise Edition)中如何处理HTTP请求呢?

是使用Servlet,具体来说在所谓的“控制层”通过继承HttpServlet来进行处理.例如

传统Servlet

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和JSR

JAX-RS是JavaEE规范的一部分,对应的JSR(Java Specification Request)-Java规范提案最新的是JSR370,对应JAX-RS版本2.1

在线查看JSR370地址

为什么要看规范?

  • 在我们使用一个具体实现了某个JSR的框架时,提前了解规范,能够帮助我们更好得理解具体框架的原理以及思路。
  • 一般JSR规范都是以接口形式定义。了解相关接口,有助于我们快速找到具体实现框架的对应位置。例如,我们知道JSR的某个接口A是处理Servlet初始化相关的,那么我们可以根据这个接口查找其具体实现,可以快速定位到我们想要或者应该使用的具体框架的类上。同时。了解某个JSR规范的接口,可以更好的帮助我们定位什么时候该用什么接口及其实现,而不是去网上各种百度,XXX要怎么做

简单解读JSR370

RestEasy的入门与使用_第1张图片

在JSR370目录中,我们可以看到整体的内容大概有12章。作为入门,我们简单了解第2、3、4章即可

Applications

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的入口点。

使用方式
RestEasy的入门与使用_第2张图片

  • 继承Application
  • 在web.xml中声明

可以看到这里只有一个servlet了,而不像传统的servlet项目,有多少个“控制器”,就要在web.xml中配置多少个servlet

Resources

一句话解释什么是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

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的目的。

RestEasy的基本使用

上面提到过,JAX-RS只提供了最基本的接口,因为这些基本接口只是大家的共识。但是离真正在应用上使用,还需要实现这些接口才可以。如果大家都去各自实现这些接口,那么每个使用Restful风格的应用代码量也很多,例如最最原始的Servlet中,我们能拿到的就是request和response,如果想要处理各种请求和响应,是不是要对request做各种解析,对response的写入又做各种适配?为了帮助开发者专注于业务,降低开发Restful风格应用的难度,RestEasy出现了。它完全实现了JSR370标准,使我们普通的业务开发者,无需关心Request到各种类型的转换以及Response的各种响应处理等这些功能。直接在它之上做我们想要做的业务即可

建立Restful风格的JavaEE应用

本示例将使用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是如何接管了应用呢?

答案在于RestEasy实现了ServletContainerInitializer这个接口
RestEasy的入门与使用_第3张图片

这个接口是Servlet3.0添加的,可以查看最新的Servlet4.0规范文档的第四章Servlet Context
RestEasy的入门与使用_第4张图片

可以简单理解为在Servlet容器初始化时候做一些工作,可以参考详细的Javadoc

简单放几张截图来说明ResteasyServletInitializer做的事情

RestEasy的入门与使用_第5张图片
在这里插入图片描述
RestEasy的入门与使用_第6张图片

可以从这几张图里基本可以看出来,ResteasyServletInitializer做的事情就是注册Resource和Provider,并添加一个Servlet.

读者想了解更多,则直接去看ResteasyServletInitializer类的源码即可,很简洁的代码,不到200行。并且RestEasy是通过SPI的形式,将ResteasyServletInitializer注册进去
RestEasy的入门与使用_第7张图片

这样在容器启动完毕的时候,就注册了Resource、Provider并且添加了一个全局处理HTTP的Servlet

拓展:

  • 其实Spring-web也实现了这个接口,感兴趣的可以去看下SpringServletContainerInitializer这个类里面做的事情,可以结合关键的SpringBootServletInitializer这个类一起来看
  • 关于ServletContainerInitializer的自定义实现可以参考这个示例
  • 关于javax.servlet.Registration#setInitParameter方法的作用

接受Restful风格的HTTP请求

在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();
    }
}

备注

  • 完整示例代码仓库
  • 在线查看某一个jar包的Javadoc网址.原来我也觉得这种Javadoc看起来好无聊。但是随着编程越来越多,特别是不熟悉的jar包让你使用时,或者你不知道自己要实现的功能需要哪个接口、类时,查找Javadoc是非常快速的解决办法

更新日志

  • 源码的JDK已经升级到JDK11
  • Servlet4升级到Servlet6,包名从javax全部变更为jakarta,web.xml的描述符也已经修改
  • JSTL升级至2.0
  • Tomcat9升级到Tomcat10
  • Hibernate、Jackson均已升级
  • Weld CDI已经升级至最新,并更新了webapp/META-INF/context.xml下的相关配置

你可能感兴趣的:(Java,EE,restful,java,java-ee,spring,restful,servlet)