Статьи

Выбор уровня детализации, возвращаемого путем изменения типа контента, часть II

В моей предыдущей статье мы рассмотрели использование функции MOXy для управления уровнем вывода данных для конкретной сущности. В этом посте рассматривается абстракция, предоставленная в Jersey 2.x, которая позволяет вам определять собственный набор аннотаций, чтобы иметь тот же эффект.

Как и прежде, у нас есть почти тривиальный ресурс, который возвращает объект, который Джерси будет для нас преобразовать в JSON, обратите внимание, что на данный момент в этом коде нет ничего для фильтрации — я не собираюсь передавать аннотации для
Объект Response как в примерах на Джерси:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
 
@Path("hello")
public class SelectableHello {
 
  @GET
  @Produces({ "application/json; level=detailed", "application/json; level=summary", "application/json; level=normal" })
  public Message hello() {
 
    return new Message();
 
  }
}

В моем дизайне я собираюсь определить четыре аннотации: NoView , SummaryView , NormalView и NormalView . Все корневые объекты должны реализовывать аннотацию NoView, чтобы не открывать аннотированные поля — вы можете не чувствовать, что это необходимо в вашем проекте. Все эти классы выглядят одинаково, поэтому я собираюсь показать только один. Обратите внимание, что фабричный метод, создающий AnnotationLiteral , должен использоваться вместо фабрики, которая создала бы динамический прокси, чтобы иметь тот же эффект. В версии 2.5 есть код, который будет игнорировать любые аннотации, реализованные объектом java.lang.reflect.Proxy , включая любые аннотации, которые вы, возможно, получили из класса. Я работаю над отправкой исправления для этого.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
import javax.enterprise.util.AnnotationLiteral;
 
import org.glassfish.jersey.message.filtering.EntityFiltering;
 
@Target({ ElementType.TYPE,
  ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EntityFiltering
public @interface NoView {
 
  /**
   * Factory class for creating instances of the annotation.
   */
  public static class Factory extends AnnotationLiteral<NoView>
    implements NoView {
 
    private Factory() {
    }
 
    public static NoView get() {
      return new Factory();
    }
  }
 
}

Теперь мы можем быстро взглянуть на наш бин Message, это немного сложнее, чем в моем предыдущем примере, чтобы показать фильтрацию подграфов в очень простой форме. Как я уже говорил, до того, как класс будет аннотирован аннотацией NoView в корне — это должно означать, что privateData никогда не возвращается клиенту, поскольку он специально не аннотируется.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import javax.xml.bind.annotation.XmlRootElement;
 
@XmlRootElement
@NoView
public class Message {
 
  private String privateData;
  @SummaryView
  private String summary;
  @NormalView
  private String message;
  @DetailedView
  private String subtext;
  @DetailedView
  private SubMessage submessage;
 
  public Message() {
    summary = "Some simple summary";
    message = "This is indeed the message";
    subtext = "This is the deep and meaningful subtext";
    submessage = new SubMessage();
    privateData = "The fox is flying tonight";
  }
 
  // Getters and setters not shown
}
 
public class SubMessage {
 
  private String message;
 
  public SubMessage() {
    message = "Some sub messages";
  }
 
  // Getters and setters not shown
}

Как отмечалось ранее, в классе ресурсов нет кода для работы с фильтрацией — я посчитал это сквозной проблемой, поэтому абстрагировал это в WriterInterceptor. Обратите внимание на исключение, которое выдается, если используется объект, на котором нет аннотации NoView.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import java.io.IOException;
 
import java.lang.annotation.Annotation;
 
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
 
import javax.ws.rs.ServerErrorException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.ext.WriterInterceptor;
import javax.ws.rs.ext.WriterInterceptorContext;
 
@Provider
public class ViewWriteInterceptor implements WriterInterceptor {
 
  private HttpHeaders httpHeaders;
 
  public ViewWriteInterceptor(@Context HttpHeaders httpHeaders) {
    this.httpHeaders = httpHeaders;
  }
 
  @Override
  public void aroundWriteTo(WriterInterceptorContext writerInterceptorContext)
    throws IOException,
           WebApplicationException {
 
    // I assume this case will never happen, just to be sure
    if (writerInterceptorContext.getEntity() == null) {
 
      writerInterceptorContext.proceed();
      return;
    }
    else
    {
      Class<?> entityType = writerInterceptorContext.getEntity()
          .getClass();
      String entityTypeString = entityType.getName();
 
      // Ignore any Jersey system classes, for example wadl
      //
      if (entityType == String.class  || entityType.isArray()
        || entityTypeString.startsWith("com.sun")
        || entityTypeString.startsWith("org.glassfish")) {
        writerInterceptorContext.proceed();
        return;
      }
 
      // Fail if the class doesn't have the default NoView annotation
      // this prevents any unannotated fields from showing up
      //
      else if (!entityType.isAnnotationPresent(NoView.class)) {
        throw new ServerErrorException("Entity type should be tagged with @NoView annotation " + entityType, Response.Status.INTERNAL_SERVER_ERROR);
 
      }
    }
 
    // Get hold of the return media type:
    //
 
    MediaType mt = writerInterceptorContext.getMediaType();
    String level = mt.getParameters().get("level");
 
    // Get the annotations and modify as required
    //
 
    Set<Annotation> current = new LinkedHashSet<>();
    current.addAll(Arrays.asList(
      writerInterceptorContext.getAnnotations()));
 
    switch (level != null ? level : "") {
      default:
      case "detailed":
        current.add(com.example.annotation.DetailedView.Factory.get());
      case "normal":
        current.add(com.example.annotation.NormalView.Factory.get());
      case "summary":
        current.add(com.example.annotation.SummaryView.Factory.get());
 
    }
 
    writerInterceptorContext.setAnnotations(
      current.toArray(new Annotation[current.size()]));
 
    //
 
    writerInterceptorContext.proceed();
  }
}

Наконец, вы должны включить EntityFilterFeature вручную, чтобы сделать это, вы можете просто зарегистрировать его в своем классе Application

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.lang.annotation.Annotation;
 
import javax.ws.rs.ApplicationPath;
 
import org.glassfish.jersey.message.filtering.EntityFilteringFeature;
import org.glassfish.jersey.server.ResourceConfig;
 
@ApplicationPath("/resources/")
public class SelectableApplication extends ResourceConfig {
  public SelectableApplication() {
 
    packages("...");
 
    // Set entity-filtering scope via configuration.
    property(EntityFilteringFeature.ENTITY_FILTERING_SCOPE,
    new Annotation[] {
      NormalView.Factory.get(), DetailedView.Factory.get(),
      NoView.Factory.get(), SummaryView.Factory.get()
    });
    register(EntityFilteringFeature.class);
  }
 
}

Как только вы все это запустите, приложение ответит, как и раньше:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
GET .../hello Accept application/json; level=detailed or application/json
{
  "message" : "This is indeed the message",
  "submessage" : {
    "message" : "Some sub messages"
  },
  "subtext" : "This is the deep and meaningful subtext",
  "summary" : "Some simple summary"
}
 
GET .../hello Accept application/json; level=normal
{
  "message" : "This is indeed the message",
  "summary" : "Some simple summary"
}
 
GET .../hello Accept application/json; level=summary
{
  "summary" : "Some simple summary"
}

Похоже, что это лучшая альтернатива непосредственному использованию аннотаций MOXy — использование пользовательских аннотаций должно значительно облегчить портирование приложения на реализацию, даже если вы должны предоставить собственный фильтр. Наконец, стоит также изучить расширение Джерси, которое позволяет фильтровать на основе ролей, что я считаю полезным в аспекте безопасности.