WebService——JAX-RS2(Jersey)扩展点详解

Jersey

1、Jersey的AOP机制

Jersey自身支持AOP,可以不依赖于Spring等支持AOP的框架。

Jersey支持AOP的功能来自于GlashFish项目集的HK2项目。Jersey通用包jersey-.common依赖HK2(轻量级DI架构),包括hk2-apihk2-locator。其中hk2-locator依赖于javax.inject包、asm-all-repackaged包和cglib包。从这些包名不难看出,hk2-locator包是个纯粹玩AOP的。

javax.inject包出自Java依赖注入规范(JSR-330),cglib包是Spring用户熟识的动态代码生成工具,是Spring两种AOP实现方式的一种,其底层依赖于ASM(htp:/asm.ow2.org)。ASM来自于开源软件国际联盟OW2(http://www.ow2.org),OW2的前身是来自法国的ObjectWeb和来自中国的Orient Ware两个中间件开源组织。

Jersey提供的REST过滤器和拦截器为开发者提供了很贴心的切面扩展点,开发者无须像在Spring中为了针对某个类的方法进行AOP扩展,而写配置文件。在Jersey中,只要实现相应扩展点的接口,即可实现REST请求流程中特定事件点的拦截、扩展,其他工作由底层的HK2帮我们做。典型的应用包括请求和响应的过滤和读写拦截。

2、Providers

javax.ws.rs.ext.Providers是JAX-RS2定义的一种辅助接口,其实现类用于辅助REST框架完成过滤和读写拦截等功能。使用注解@Provider来标注这些实现类,可以被JAX-RS2的运行时自动探测、加载。Provider实例可以通过@Context注解被依赖注入到其他实例中。

Providers接口定义了4个方法,分别用来获取MessageBodyReaderMessageBodyWriterExceptionMapperContextResolver实例。

2.1、实体Providers

Jersey之所以可以支持那么多种表述的类型,即响应实体的传输格式,是因为其底层实体Providers具备的对不同格式的处理能力。Jersey内部提供了非常丰富的MessageBodyReader接口和MessageBodyWriter接口实现类,用于处理不同格式的表述,比如字节数组、ML、文件和流等。

1)、MessageBodyReader

消息体读处理器接口MessageBodyReader<T>用于将传输流转换为Java类型的对象。MessageBodyReader接口定义了一个泛型,接口的实现类为这个泛型定义一个具体类型,该类型即是该实现类所支持的转换类型。

实现类被业务系统启用有两种方式:

  • 一是使用注解@Provider定义实现类,业务系统在启动时自动探测并加载。
  • 另一种方式是通过编码注册到Application类或其子类中,业务系统在启动时,加载Application类或其子类时一并加载。

MessageBodyReader<T>接口定义了两个方法。第一个方法isReadable()是用来判断实现类是否支持将当前请求的数据类型反序列化。

//type:要生成的实例的类
//genericType:要生成的实例的类型
//annotations:将使用生成的实例初始化的方法声明上的注释数组
//mediaType:HTTP实体的媒体类型,如果在请求中没有指定媒体类型,则使用application/octet-stream
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType);

MessageBodyReader<T>接口定义的第二个方readFrom()是用于处理反序列化,是处理读取流并转换为Java类型对象的核心方法。该方法定义如下。

//type:要从实体流中读取的类型
//genericType:要生成的实例的类型
//annotations:将使用生成的实例初始化的方法声明上的注释数组
//mediaType:HTTP实体的媒体类型
//httpHeaders:请求头
//entityStream:HTTP实体的输入流
public T readFrom(
	Class<T> type, 
	Type genericType, 
	Annotation[] annotations, 
	MediaType mediaType, 
	MultivaluedMap<String, String> httpHeaders, 	
	InputStream entityStream) 
throws java.io.IOException, javax.ws.rs.WebApplicationException;
2)、MessageBodyWriter

消息体写处理器接口MessageBody Writer<T>用于将Java类型的对象转换为流,它是序列化的过程和MessageBodyReader<T>接口实现的反序列化的逆过程。两个接口的设计原理是相同的。对应地,MessageBody Writer<T>定义了两个方法isWriteable()
writeTo()

其实,解析一种传输类型的Provider类通常会同时实现MessageBodyReader和MessageBodyWriter这两个接口。

isWriteable()方法用于检测实现类是否支持序列化当前请求的类型,如果不可写,Jersey框架会放弃使用这个实现类来处理当前的表述。

//type:要写的实例类
//genericType:要写入的实例的类型
//annotations:附加到消息实体实例的注释数组
//mediaType:HTTP实体的媒体类型
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType);

writeTo()方法是将请求对象写入流的序列化过程。

//type:要从实体流中读取的类型
//genericType:要生成的实例的类型
//annotations:将使用生成的实例初始化的方法声明上的注释数组
//mediaType:HTTP实体的媒体类型
//httpHeaders:请求头
//entityStream:HTTP实体的输出流
public void writeTo(
	T t, Class<?> type, 
	Type genericType, 
	Annotation[] annotations,
    MediaType mediaType,
    MultivaluedMap<String, Object> httpHeaders,
    OutputStream entityStream)
throws java.io.IOException, javax.ws.rs.WebApplicationException;
3)、MessageBodyWorkers

MessageBodyReader和MessageBodyWriter的实现类非常多,编写选择哪个实现类作为当前请求的读写处理器的
算法是非常繁重的工作,MessageBodyWorkers接口旨在抽象这一遴选工作,其实现类可以通过@Context依赖注入到使用MessageBodyWorkers的类中MessageBodyFactory
是MessageBodyWorkers接口的实现类。

在这里插入图片描述

2.2、上下文Providers

除了处理实体的Provider,处理上下文的Provider也非常重要。ContextResolver<T>接口是用于提供资源类和其他Provider上下文信息的接口。

1)、ContextResolver

ContextResolver<T>定义了一个方法getContext(),输入参数是表述对象的类型,输出是上下文泛型。

public interface ContextResolver<T> {

    T getContext(Class<?> type);
}
2)、自定义

根据对象的不同,返回不同的JettisonJaxbContext实例,以实现不同的JSON解析效果。

@Provider
public class JettisonJsonContextResolver implements ContextResolver<JAXBContext> {

    private final JAXBContext c1;
    private final JAXBContext c2;

    public JettisonJsonContextResolver() throws JAXBException {
        Class[] clz = new Class[]{Student1.class, Student2.class};
        this.c1 = new JettisonJaxbContext(JettisonConfig.DEFAULT, clz);
        this.c2 = new JettisonJaxbContext(JettisonConfig.badgerFish().build(), clz);
    }

    @Override
    public JAXBContext getContext(Class<?> type) {
        if (type == Student1.class) {
            System.out.println("mapped");
            return c1;
        } else {
            System.out.println("badgerfish");
            return c2;
        }
    }
}

3、REST请求流程

请求流程中存在3种角色,分别是用户REST客户端REST服务端

请求始于请求的发送,止于调用Response类的readEntity()方法,获取响应实体。

在这里插入图片描述

  1. 用户提交请求数据,客户端接收请求,进入第一个扩展点:“客户端请求过滤器ClientRequestFilter实现类”的filter()方法
  2. 请求过滤处理完毕后,流程进人第二个扩展点:“客户端写拦截器WriterInterceptor实现类”的aroundWriteTo()方法,实现对客户端序列化操作的拦截。
  3. “客户端消息体写处理器MessageBodyWriter”执行序列化,流程从客户端过渡到服务器端。
  4. 服务器接收请求,流程进入第三个扩展点:“服务器前置请求过滤器ContainerRequestFilter实现类"的filter()方法
  5. 过滤处理完毕后,服务器根据请求匹配资源方法,如果匹配到相应的资源方法,流程进入第四个扩展点:“服务器后置请求过滤器ContainerRequestFilter实现类”的filter() 方法
  6. 后置请求过滤处理完毕后,流程进入第五个扩展点:“服务器读拦截器ReaderInterceptor实现类”的aroundReadFrom()方法,拦截服务器端反序列化操作。
  7. “服务器消息体读处理器MessageBodyReader”完成对客户端数据流的反序列化。服务器执行匹配的资源方法。
  8. REST请求资源的处理完毕后,流程进入第六个扩展点:“服务器响应过滤器ContainerResponseFilter实现类”的filter()方法
  9. 过滤处理完毕后,流程进入第七个扩展点:“服务器写拦截器WriterInterceptor实现类”的aroundWriteTo()方法,对服务器端序列化到客户端这个操作的拦截。
  10. “服务器消息体写处理器MessageBodyWriter”执行序列化,流程返回到客户端一侧。
  11. 客户端接收响应,流程进入第八个扩展点:“客户端响应过滤器ClientResponseFilter实现类”的filter()方法
  12. 过滤处理完毕后,客户端响应实例response返回到用户一侧,用户执行response.readEntity()流程进人第九个扩展点:“客户端读拦截器ReaderInterceptor实现类”的aroundReadFrom()方法,对客户端反序列化进行拦截。
  13. “客户端消息体读处理器MessageBodyReader”执行反序列化,将Java类型的对象最终作为readEntity()方法的返回值。到此,一次REST请求处理的完整流程完毕。这期间,如果出现异常或资源不匹配等情况,会从出错点开始结束流程。

4、REST过滤器

JAX-RS2定义的4种过滤器扩展点(Extension
Poit)接口,供开发者实现业务逻辑,按请求处理流程的先后顺序为:

  1. 客户端请求过滤器(ClientRequestFilter)
  2. 服务器请求过滤器(ContainerRequestFilter)
  3. 服务器响应过滤器(ContainerResponseFilter)
  4. 客户端响应过滤器(ClientResponseFilter)

4.1、ClientRequestFilter

public interface ClientRequestFilter {

    public void filter(ClientRequestContext requestContext) throws IOException;
}

客户端请求过滤器(ClientRequestFilter)定义的过滤方法filter()包含一个输入参数,是客户端请求的上下文类ClientRequestContext。

从该上下文中可以获取请求信息:

//获取请求方法
requestContext.getMethod();
//获取请求资源地址
requestContext.getUri();
//获取请求头信息
requestContext.getHeaders();
//获取Cookie
requestContext.getCookies();
...

示例:自定义Http Base认证过滤器

@Provider
public class HttpAuthenticationFilter implements ClientRequestFilter {
    @Override
    public void filter(ClientRequestContext requestContext) throws IOException {
        MultivaluedMap<String, Object> headers = requestContext.getHeaders();
        if (!headers.containsKey("Authorization")) {
            String username = "tom";
            String password = "123456";
            String authentication = "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes());
            headers.add("Authorization", authentication);
        }
    }
}

4.2、ContainerRequestFilter

public interface ContainerRequestFilter {

    public void filter(ContainerRequestContext requestContext) throws IOException;
}

针对过滤切面,服务器请求过滤器接口ContainerRequestFilter的实现类可以定义为预处理后处理。默认情况下,采用后处理方式。

  • 后处理:即先执行容器接收请求操作,当服务器接收并处理请求后,流程才进入过滤器实现类的filter()方法。
  • 预处理:是在服务器处理接收到的请求之前就执行过滤。如果希望实现一个预处理的过滤器实现类,需要在类名上定义注解@PreMatching

示例:自定义CSRF保护过滤器

//优先级
@Priority(Priorities.AUTHENTICATION)
public class CsrfProtectionFilter implements ContainerRequestFilter {
    
    public static final String HEADER_NAME = "X-Requested-By";
    
    private static final Set<String> METHODS_TO_IGNORE;
    
    static {
        HashSet<String> mti = new HashSet<>();
        mti.add("GET");
        mti.add("OPTIONS");
        mti.add("HEAD");
        METHODS_TO_IGNORE = Collections.unmodifiableSet(mti);
    }
    
    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        if (!METHODS_TO_IGNORE.contains(requestContext.getMethod()) && !requestContext.getHeaders().containsKey(HEADER_NAME)) {
            throw new BadRequestException();
        }
    }
}

在这段代码中,CsrfProtectionFilter定义了一个特殊的头信息"X-Requested-By"和CSRF忽略监控的方法集合。在过滤器的filter()方法中,首先从上下文中获取头信息和请求方法信息,然后判断头信息是否包含"X-Requested-By",方法信息是否是安全的请求方法,即"GET"、“OPTIONS"或"HEAD”。如果两个条件都不成立,过滤器会抛出一个运行时异常BadRequestException。通过CsrfProtectionFilter过滤器,可以确保请求是CSRF安全的。

4.3、ContainerResponseFilter

public interface ContainerResponseFilter {

    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
            throws IOException;
}

服务器响应过滤器接口ContainerResponseFilter定义的过滤方法filter()包含两个输人参数,一个是容器请求上下文类ContainerRequestContext,另一个是容器响应上下文类ContainerResponseContext

示例:EncodingFilter,完成内容协商中编码匹配的工作

    public void filter(ContainerRequestContext request, ContainerResponseContext response) throws IOException {
        if (!response.hasEntity()) {
            return;
        }

        // add Accept-Encoding to Vary header
        List<String> varyHeader = response.getStringHeaders().get(HttpHeaders.VARY);
        if (varyHeader == null || !varyHeader.contains(HttpHeaders.ACCEPT_ENCODING)) {
            response.getHeaders().add(HttpHeaders.VARY, HttpHeaders.ACCEPT_ENCODING);
        }

        // if Content-Encoding is already set, don't do anything
        if (response.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING) != null) {
            return;
        }

        // retrieve the list of accepted encodings
        List<String> acceptEncoding = request.getHeaders().get(HttpHeaders.ACCEPT_ENCODING);

        // if empty, don't do anything
        if (acceptEncoding == null || acceptEncoding.isEmpty()) {
            return;
        }

        // convert encodings from String to Encoding objects
        List<ContentEncoding> encodings = Lists.newArrayList();
        for (String input : acceptEncoding) {
            String[] tokens = input.split(",");
            for (String token : tokens) {
                try {
                    ContentEncoding encoding = ContentEncoding.fromString(token);
                    encodings.add(encoding);
                } catch (ParseException e) {
                    // ignore the encoding that could not parse
                    // but log the exception
                    Logger.getLogger(EncodingFilter.class.getName()).log(Level.WARNING, e.getLocalizedMessage(), e);
                }
            }
        }
        // sort based on quality parameter
        Collections.sort(encodings);
        // make sure IDENTITY_ENCODING is at the end (since it accepted if not explicitly forbidden
        // in the Accept-Content header by assigning q=0
        encodings.add(new ContentEncoding(IDENTITY_ENCODING, -1));

        // get a copy of supported encoding (we'll be modifying this set, hence the copy)
        SortedSet<String> acceptedEncodings = Sets.newTreeSet(getSupportedEncodings());

        // indicates that we can pick any of the encodings that remained in the acceptedEncodings set
        boolean anyRemaining = false;
        // final resulting value of the Content-Encoding header to be set
        String contentEncoding = null;

        // iterate through the accepted encodings, starting with the highest quality one
        for (ContentEncoding encoding : encodings) {
            if (encoding.q == 0) {
                // ok, we are down at 0 quality
                if ("*".equals(encoding.name)) {
                    // no other encoding is acceptable
                    break;
                }
                // all the 0 quality encodings need to be removed from the accepted ones (these are explicitly
                // forbidden by the client)
                acceptedEncodings.remove(encoding.name);
            } else {
                if ("*".equals(encoding.name)) {
                    // any remaining encoding (after filtering out q=0) will be acceptable
                    anyRemaining = true;
                } else {
                    if (acceptedEncodings.contains(encoding.name)) {
                        // found an acceptable one -> we are done
                        contentEncoding = encoding.name;
                        break;
                    }
                }
            }
        }

        if (contentEncoding == null) {
            // haven't found any explicit acceptable encoding, let's see if we can just pick any of the remaining ones
            // (if there are any left)
            if (anyRemaining && !acceptedEncodings.isEmpty()) {
                contentEncoding = acceptedEncodings.first();
            } else {
                // no acceptable encoding can be sent -> return NOT ACCEPTABLE status code back to the client
                throw new NotAcceptableException();
            }
        }

        // finally set the header - but no need to set for identity encoding
        if (!IDENTITY_ENCODING.equals(contentEncoding)) {
            response.getHeaders().putSingle(HttpHeaders.CONTENT_ENCODING, contentEncoding);
        }
    }

EncodingFilter过滤器的filter()方法通过对请求头信息“Accept-Encoding”的分析,先后为响应头信息“Vary”和“Content-Encoding”赋值,以实现编码部分的内容协商。

4.4、ClientResponseFilter

public interface ClientResponseFilter {

    public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext)
            throws IOException;
}

客户端响应过滤器(ClientResponseFilter)定义的过滤方法filter()包含两个输人参数,一个是客户端请求的上下文类ClientRequestContext,另一个是客户端响应的上下文类
ClientResponseContext。

示例:HTTP摘要认证过滤器类HttpAuthenticationFilter

 @Override
    public void filter(ClientRequestContext request) throws IOException {
        if ("true".equals(request.getProperty(REQUEST_PROPERTY_FILTER_REUSED))) {
            return;
        }

        if (request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
            return;
        }

        Type operation = null;
        if (mode == HttpAuthenticationFeature.Mode.BASIC_PREEMPTIVE) {
            basicAuth.filterRequest(request);
            operation = Type.BASIC;
        } else if (mode == HttpAuthenticationFeature.Mode.BASIC_NON_PREEMPTIVE) {
            // do nothing
        } else if (mode == HttpAuthenticationFeature.Mode.DIGEST) {
            if (digestAuth.filterRequest(request)) {
                operation = Type.DIGEST;
            }
        } else if (mode == HttpAuthenticationFeature.Mode.UNIVERSAL) {

            Type lastSuccessfulMethod = uriCache.get(getCacheKey(request));
            if (lastSuccessfulMethod != null) {
                request.setProperty(REQUEST_PROPERTY_OPERATION, lastSuccessfulMethod);
                if (lastSuccessfulMethod == Type.BASIC) {
                    basicAuth.filterRequest(request);
                    operation = Type.BASIC;
                } else if (lastSuccessfulMethod == Type.DIGEST) {
                    if (digestAuth.filterRequest(request)) {
                        operation = Type.DIGEST;
                    }
                }
            }
        }

        if (operation != null) {
            request.setProperty(REQUEST_PROPERTY_OPERATION, operation);
        }
    }

4.5、自定义日志过滤器

日志过滤器:

@Provider
//前置处理
@PreMatching
public class AirLogFilter implements ContainerRequestFilter, ClientRequestFilter, ContainerResponseFilter, ClientResponseFilter {
    private static final Log LOGGER = LogFactory.getLog(AirLogFilter.class);
    private static final String NOTIFICATION_PREFIX = "* ";
    private static final String SERVER_REQUEST = "> ";
    private static final String SERVER_RESPONSE = "< ";
    private static final String CLIENT_REQUEST = "/ ";
    private static final String CLIENT_RESPONSE = "\\ ";
    private static final AtomicLong logSequence = new AtomicLong(0);

    @Override
    public void filter(ClientRequestContext context) throws IOException {
        long id = logSequence.incrementAndGet();
        StringBuilder b = new StringBuilder();
        //获取请求方法和地址
        printRequestLine(CLIENT_REQUEST, b, id, context.getMethod(), context.getUri());
        //获取请求头信息
        printPrefixedHeaders(CLIENT_REQUEST, b, id,/*HeadersFactory*/HeaderUtils.asStringHeaders(context.getHeaders()));
        LOGGER.info(b.toString());
    }

    @Override
    public void filter(ClientRequestContext requestContext,
                       ClientResponseContext responseContext) throws IOException {
        long id = logSequence.incrementAndGet();
        StringBuilder b = new StringBuilder();
        printResponseLine(CLIENT_RESPONSE, b, id, responseContext.getStatus());
        printPrefixedHeaders(CLIENT_RESPONSE, b, id, responseContext.getHeaders());
        LOGGER.info(b.toString());
    }

    @Override
    public void filter(ContainerRequestContext context) throws IOException {
        long id = logSequence.incrementAndGet();
        StringBuilder b = new StringBuilder();
        printRequestLine(SERVER_REQUEST, b, id, context.getMethod(), context.getUriInfo().getRequestUri());
        printPrefixedHeaders(SERVER_REQUEST, b, id, context.getHeaders());
        LOGGER.info(b.toString());
    }

    @Override
    public void filter(ContainerRequestContext requestContext,
                       ContainerResponseContext responseContext) throws IOException {
        long id = logSequence.incrementAndGet();
        StringBuilder b = new StringBuilder();
        //获取容器响应状态
        printResponseLine(SERVER_RESPONSE, b, id, responseContext.getStatus());
        printPrefixedHeaders(SERVER_RESPONSE, b, id,/*HeadersFactory*/HeaderUtils.asStringHeaders(responseContext.getHeaders()));
        LOGGER.info(b.toString());
    }

    private StringBuilder prefixId(StringBuilder b, long id) {
        b.append(Long.toString(id)).append(" ");
        return b;
    }

    private void printRequestLine(final String prefix,
                                  StringBuilder b, long id, String method, URI uri) {
        prefixId(b, id).append(NOTIFICATION_PREFIX)
                .append("AirLog - Request received on thread ")
                .append(Thread.currentThread().getName()).append("\n");
        prefixId(b, id).append(prefix).append(method)
                .append(" ").append(uri.toASCIIString()).append("\n");
    }

    private void printResponseLine(final String prefix,
                                   StringBuilder b, long id, int status) {
        prefixId(b, id).append(NOTIFICATION_PREFIX)
                .append("AirLog - Response received on thread ")
                .append(Thread.currentThread().getName()).append("\n");
        prefixId(b, id).append(prefix)
                .append(Integer.toString(status)).append("\n");
    }

    private void printPrefixedHeaders(final String prefix, StringBuilder b, long id, MultivaluedMap<String, String> headers) {
        for (Map.Entry<String, List<String>> e : headers.entrySet()) {
            List<?> val = e.getValue();
            String header = e.getKey();

            if (val.size() == 1) {
                prefixId(b, id).append(prefix).append(header)
                        .append(": ").append(val.get(0)).append("\n");
            } else {
                StringBuilder sb = new StringBuilder();
                boolean add = false;
                for (Object s : val) {
                    if (add) {
                        sb.append(',');
                    }
                    add = true;
                    sb.append(s);
                }
                prefixId(b, id).append(prefix).append(header)
                        .append(": ").append(sb.toString()).append("\n");
            }
        }
    }
}

服务端注册日志过滤器:

@Component
@ApplicationPath("/ws")
public class JerseyConfig extends ResourceConfig {

    public JerseyConfig() {
        //注册单个资源类
        register(BookResourceImpl.class);
        //注册过滤器
        register(AirLogFilter.class);
    }
}

客户端:

@Test
public void testLog() {
    Client client = ClientBuilder.newClient();
    WebTarget webTarget = client.target("http://127.0.0.1:8080/ws");

    Response response = webTarget.register(AirLogFilter.class)
            .path("book")
            .request()
            .get();

    System.out.println(response.readEntity(String.class));
}

服务端输出:

1 > GET http://127.0.0.1:8080/ws/book
1 > user-agent: Jersey/2.32 (HttpUrlConnection 1.8.0_211)
1 > host: 127.0.0.1:8080
1 > accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
1 > connection: keep-alive

2 * AirLog - Response received on thread http-nio-8080-exec-1
2 < 200
2 < Content-Type: text/html

客户端输出:

1 / GET http://127.0.0.1:8080/ws/book

21:31:49.684 [main] INFO pers.zhang.filter.AirLogFilter - 2 * AirLog - Response received on thread main
2 \ 200
2 \ Keep-Alive: timeout=60
2 \ Connection: keep-alive
2 \ Content-Length: 4
2 \ Date: Tue, 26 Dec 2023 13:31:49 GMT
2 \ Content-Type: text/html

150M

5、REST拦截器

拦截器和过滤器的相同点是都是一种在请求——响应模型中,用做切面处理的Provider。两者的不同除了功能性上的差异(一个用于过滤消息,一个用于拦截处理)之外,形式上也不同。拦截器通常读写成对,而且没有服务器端和客户端的区分。

5.1、ReaderInterceptor

public interface ReaderInterceptor {

    public Object aroundReadFrom(ReaderInterceptorContext context) throws java.io.IOException, javax.ws.rs.WebApplicationException;
}

在这里插入图片描述

读拦截器接口ReaderInterceptor定义的拦截方法是aroundReadFrom(),该方法包含一个输人参数,即读拦截器的上下文接口ReaderInterceptorContext,从中可以获取头信息、输人流以及父接口InterceptorContext提供的媒体类型等上下文信息。

5.2、WriterInterceptor

public interface WriterInterceptor {

    void aroundWriteTo(WriterInterceptorContext context) throws java.io.IOException, javax.ws.rs.WebApplicationException;
}

在这里插入图片描述

写拦截器接口WriterInterceptor定义的拦截方法是aroundWriteTo(),该方法包含一个输入参数,写拦截器上下文接口WriterInterceptorContext,从中可以获取头信息、输出流以及父接口InterceptorContext提供的媒体类型等上下文信息。

5.3、编解码约束拦截器

编解码约束拦截器类ContentEncoder是一个位于org.glassfish.jersey.spi包中的拦截器,SPI包下的工具是可插拔的。ContentEncoder拦截器用于约束序列化和反序列化的过程中,编解码的内容协商。

@Priority(Priorities.ENTITY_CODER)
@Contract
public abstract class ContentEncoder implements ReaderInterceptor, WriterInterceptor {
    private final Set<String> supportedEncodings;


    protected ContentEncoder(String... supportedEncodings) {
        if (supportedEncodings.length == 0) {
            throw new IllegalArgumentException();
        }
        this.supportedEncodings = Collections.unmodifiableSet(Arrays.stream(supportedEncodings).collect(Collectors.toSet()));
    }


    public final Set<String> getSupportedEncodings() {
        return supportedEncodings;
    }

    public abstract InputStream decode(String contentEncoding, InputStream encodedStream) throws IOException;

    public abstract OutputStream encode(String contentEncoding, OutputStream entityStream) throws IOException;

    @Override
    public final Object aroundReadFrom(ReaderInterceptorContext context) throws IOException, WebApplicationException {
        String contentEncoding = context.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING);
        //判断是否包含Content-Encoding头信息
        if (contentEncoding != null && getSupportedEncodings().contains(contentEncoding)) {
        	//解码处理
            context.setInputStream(decode(contentEncoding, context.getInputStream()));
        }
        //继续连接链的下一个拦截处理
        return context.proceed();
    }

    @Override
    public final void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
        // must remove Content-Length header since the encoded message will have a different length

        String contentEncoding = (String) context.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING);
        //判断是否包含Content-Encoding头信息
        if (contentEncoding != null && getSupportedEncodings().contains(contentEncoding)) {
        	//编码处理
            context.setOutputStream(encode(contentEncoding, context.getOutputStream()));
        }
        //继续连接链的下一个拦截处理
        context.proceed();
    }
}

在这段代码中,分别给出了ContentEncoder拦截器的读、写拦截处理,只有当头信息包含“Content-Encoding”信息,编解码才被执行。读取阶段进行解码,写入阶段进行编码。上下文的proceed()方法用于执行拦截器链的下一个拦截器。

6、绑定机制

这些容器级别的Providers,通常使用编码的方式注册到Application中,但这不是唯一的办法。默认情况下,过滤器和拦截器都是全局绑定的。也就是说,如下之一的过滤器或拦截器是全局有效的:

  • 通过手动注册到Application或者Configuration
  • 注解为@Provider,被自动探测

6.1、名称绑定

过滤器或拦截器可以使用特定的注解来指定其作用范围,这种特定的注解被称为名称绑定。

1)、名称绑定注解

使用@NameBinding注解可以定义一个运行时的自定义注解,该注解用于定义类级别名称和类的方法名。

@NameBinding
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface AirLog {
}
2)、绑定Provider

在定义了@AirLog注解后,既可以在Provider中使用该注解,示例代码如下所示:

@AirLog
@Priority(Priorities.USER)
public class AirNameBindingFilter implements ContainerRequestFilter, ContainerResponseFilter {

    private static final Log logger = LogFactory.getLog(AirNameBindingFilter.class);

    public AirNameBindingFilter() {
        logger.info("Air-NameBinding-Filter initialized");
    }

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        logger.info("Air-NameBinding-ContainerRequestFilter invoked:" + requestContext.getMethod());
        logger.info(requestContext.getUriInfo().getRequestUri());
    }

    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
        logger.info("Air-NameBinding-ContainerResponseFilter invoked:" + requestContext.getMethod());
        logger.info("status=" + responseContext.getStatus());
    }
}

不要忘记注册:

register(AirNameBindingFilter.class);
3)、绑定方法

在资源方法级别使用自定义注解@AirLog,来实现在资源类的指定方法上启用AirNameBindingFilter过滤器,示例代码如下所示。

@Path("books")
public class BooksResource {

    @AirLog
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_XML)
    public Book getBookWithLog(Book book) {
        return book;
    }

    @GET
    @Produces(MediaType.APPLICATION_XML)
    public Book getBookWithNoLog() {
        return new Book(22l, "红楼梦", 11.11);
    }

}

不要忘记注册:

register(BooksResource.class);
4)、测试
@Test
public void testNameBinding() {
    //访问@AirLog绑定了日志的接口
    Client client = ClientBuilder.newClient();
    WebTarget webTarget = client.target("http://127.0.0.1:8080/ws");

    Book book = new Book();
    book.setId(1l);
    book.setName("三国演义");
    book.setPrice(33.34);

    Response response = webTarget.path("books")
            .request()
            .post(Entity.entity(book, MediaType.APPLICATION_JSON_TYPE));
    System.out.println(response.readEntity(String.class));

    //访问没有绑定的接口
    Client client2 = ClientBuilder.newClient();
    WebTarget webTarget2 = client.target("http://127.0.0.1:8080/ws");

    Response response2 = webTarget.path("books")
            .request()
            .get();
    System.out.println(response2.readEntity(String.class));
}

客户端输出:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?><book><id>1</id><name>三国演义</name><price>33.34</price></book>
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><book><id>22</id><name>红楼梦</name><price>11.11</price></book>

服务端输出,只有标注了@AirLog注解绑定的POST接口有日志:

2023-12-26 23:33:26.945  INFO 18067 --- [nio-8080-exec-1] pers.zhang.filter.AirNameBindingFilter   : Air-NameBinding-Filter initialized
2023-12-26 23:33:27.049  INFO 18067 --- [nio-8080-exec-1] pers.zhang.filter.AirNameBindingFilter   : Air-NameBinding-ContainerRequestFilter invoked:POST
2023-12-26 23:33:27.049  INFO 18067 --- [nio-8080-exec-1] pers.zhang.filter.AirNameBindingFilter   : http://127.0.0.1:8080/ws/books
2023-12-26 23:33:27.106  INFO 18067 --- [nio-8080-exec-1] pers.zhang.filter.AirNameBindingFilter   : Air-NameBinding-ContainerResponseFilter invoked:POST
2023-12-26 23:33:27.106  INFO 18067 --- [nio-8080-exec-1] pers.zhang.filter.AirNameBindingFilter   : status=200

6.2、动态绑定

名称绑定需要通过自定义的注解名称来绑定Provider和扩展点方法或者类,相比而言,动态绑定无须新增注解,而是使用编码的方式,实现动态特征接口javax.ws.rs.container. DynamicFeature,定义扩展点方法的名称、请求方法类型等匹配信息。在运行期,一旦Provider匹配当前处理类或方法,面向切面的Provider方法即被触发。

1)、定义绑定Provider
@Provider
public class AirDynamicFeature implements DynamicFeature {
    @Override
    public void configure(ResourceInfo resourceInfo, FeatureContext context) {
        //判断此资源类是否为BooksResource
        boolean classMatched = BooksResource.class.isAssignableFrom(resourceInfo.getResourceClass());
        //判断是否为要增强的方法
        boolean methodNameMatched = resourceInfo.getResourceMethod().getName().contains("getBookWithLog");
        //判断请求方法是否为POST
        boolean methodTypeMatched = resourceInfo.getResourceMethod().isAnnotationPresent(POST.class);
        //匹配成功才注册AirDynamicBindingFilter
        if (classMatched && (methodNameMatched || methodTypeMatched)) {
            context.register(AirDynamicBindingFilter.class);
        }
    }
}
public class AirDynamicBindingFilter implements ContainerRequestFilter {

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        System.out.println("Air-Dynamic-Bindng-Filter invoked");
    }
}

注册DynamicFeature:

register(AirDynamicFeature.class);
2)、定义资源类
@Path("books")
public class BooksResource {

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_XML)
    public Book getBookWithLog(Book book) {
        return book;
    }

    @GET
    @Produces(MediaType.APPLICATION_XML)
    public Book getBookWithNoLog() {
        return new Book(22l, "红楼梦", 11.11);
    }

}
3)、测试
@Test
public void testNameBinding() {
    //访问@AirLog绑定了日志的接口
    Client client = ClientBuilder.newClient();
    WebTarget webTarget = client.target("http://127.0.0.1:8080/ws");

    Book book = new Book();
    book.setId(1l);
    book.setName("三国演义");
    book.setPrice(33.34);

    Response response = webTarget.path("books")
            .request()
            .post(Entity.entity(book, MediaType.APPLICATION_JSON_TYPE));
    System.out.println(response.readEntity(String.class));

    //访问没有绑定的接口
    Client client2 = ClientBuilder.newClient();
    WebTarget webTarget2 = client.target("http://127.0.0.1:8080/ws");

    Response response2 = webTarget.path("books")
            .request()
            .get();
    System.out.println(response2.readEntity(String.class));
}

服务端输出一次信息:

Air-Dynamic-Bindng-Filter invoked

7、优先级

对于同一个扩展点的多个Provider的执行的先后顺序是靠优先级排序的。优先级的定义使用注解@Priority,优先级的值是一个整型值,常量定义在javax.ws.rs.Priorities类中。

public final class Priorities {

    private Priorities() {
        // prevents construction
    }

    public static final int AUTHENTICATION = 1000;
    public static final int AUTHORIZATION = 2000;
    public static final int HEADER_DECORATOR = 3000;
    public static final int ENTITY_CODER = 4000;
    public static final int USER = 5000;
}
  • 对于ContainerRequestPreMatchContainerRequestClientRequest读写拦截器该数值采用升序策略,即数值越小N优先级越高
  • 对于ContainerResponseClientResponse该数值采用降序策略,即数越大优先级越高;

示例:ContainerRequestFilter采用升序策略,应该是AirDynamicBindingFilter1先被调用。

@Priority(Priorities.USER)
public class AirDynamicBindingFilter1 implements ContainerRequestFilter {

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        System.out.println("Air-Dynamic-Bindng-Filter1 invoked");
    }
}


@Priority(Priorities.USER + 1)
public class AirDynamicBindingFilter2 implements ContainerRequestFilter {

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        System.out.println("Air-Dynamic-Bindng-Filter2 invoked");
    }
}
@Provider
public class AirDynamicFeature implements DynamicFeature {
    @Override
    public void configure(ResourceInfo resourceInfo, FeatureContext context) {
        //判断此资源类是否为BooksResource
        boolean classMatched = BooksResource.class.isAssignableFrom(resourceInfo.getResourceClass());
        //判断是否为要增强的方法
        boolean methodNameMatched = resourceInfo.getResourceMethod().getName().contains("getBookWithLog");
        //判断请求方法是否为POST
        boolean methodTypeMatched = resourceInfo.getResourceMethod().isAnnotationPresent(POST.class);
        //匹配成功才注册AirDynamicBindingFilter
        if (classMatched && (methodNameMatched || methodTypeMatched)) {
            context.register(AirDynamicBindingFilter1.class);
            context.register(AirDynamicBindingFilter2.class);
        }
    }
}

PostMan测试:
在这里插入图片描述

服务端输出:

Air-Dynamic-Bindng-Filter1 invoked
Air-Dynamic-Bindng-Filter2 invoked
  • 28
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值