【问题标题】:Spring Data REST: projection representation of single resourceSpring Data REST:单一资源的投影表示
【发布时间】:2017-03-10 10:28:19
【问题描述】:

我有一个简单的UserRepository,它使用Spring Data REST 公开。 这是User 实体类:

@Document(collection = User.COLLECTION_NAME)
@Setter
@Getter
public class User extends Entity {

    public static final String COLLECTION_NAME = "users";

    private String name;
    private String email;
    private String password;
    private Set<UserRole> roles = new HashSet<>(0);
}

我创建了一个UserProjection 类,其外观如下:

@JsonInclude(JsonInclude.Include.NON_NULL)
@Projection(types = User.class)
public interface UserProjection {

    String getId();

    String getName();

    String getEmail();

    Set<UserRole> getRoles();
}

这是存储库类:

@RepositoryRestResource(collectionResourceRel = User.COLLECTION_NAME, path = RestPath.Users.ROOT,
        excerptProjection = UserProjection.class)
public interface RestUserRepository extends MongoRepository<User, String> {

    // Not exported operations

    @RestResource(exported = false)
    @Override
    <S extends User> S insert(S entity);

    @RestResource(exported = false)
    @Override
    <S extends User> S save(S entity);

    @RestResource(exported = false)
    @Override
    <S extends User> List<S> save(Iterable<S> entites);
}

我还在配置中指定了用户投影以确保它会被使用。

config.getProjectionConfiguration().addProjection(UserProjection.class, User.class);

所以,当我在 /users 路径上执行 GET 时,我得到以下响应(应用了投影):

{
  "_embedded" : {
    "users" : [ {
      "name" : "Yuriy Yunikov",
      "id" : "5812193156aee116256a33d4",
      "roles" : [ "USER", "ADMIN" ],
      "email" : "yyunikov@gmail.com",
      "points" : 0,
      "_links" : {
        "self" : {
          "href" : "http://127.0.0.1:8080/users/5812193156aee116256a33d4"
        },
        "user" : {
          "href" : "http://127.0.0.1:8080/users/5812193156aee116256a33d4{?projection}",
          "templated" : true
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://127.0.0.1:8080/users"
    },
    "profile" : {
      "href" : "http://127.0.0.1:8080/profile/users"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 1,
    "totalPages" : 1,
    "number" : 0
  }
}

但是,当我尝试对单个资源进行 GET 调用时,例如/users/5812193156aee116256a33d4,我收到以下回复:

{
  "name" : "Yuriy Yunikov",
  "email" : "yyunikov@gmail.com",
  "password" : "123456",
  "roles" : [ "USER", "ADMIN" ],
  "_links" : {
    "self" : {
      "href" : "http://127.0.0.1:8080/users/5812193156aee116256a33d4"
    },
    "user" : {
      "href" : "http://127.0.0.1:8080/users/5812193156aee116256a33d4{?projection}",
      "templated" : true
    }
  }
}

如您所见,密码字段正在返回并且未应用投影。我知道有@JsonIgnore 注释可以用来隐藏资源的敏感数据。但是,我的 User 对象位于不了解 API 或 JSON 表示的不同应用程序模块中,因此在那里用 @JsonIgnore 注释标记字段是没有意义的。

我看过@Oliver Gierke here 的帖子,关于为什么摘录投影不会自动应用于单个资源。但是,就我而言,这仍然很不方便,我想在获得单个资源时返回相同的UserProjection。是否有可能在不创建自定义控制器或使用@JsonIgnore 标记字段的情况下做到这一点?

【问题讨论】:

标签: spring rest spring-data spring-data-rest spring-hateoas


【解决方案1】:

我能够创建一个ResourceProcessor 类,它按照DATAREST-428 中的建议对任何资源应用投影。它的工作方式如下:如果在 URL 中指定了投影参数 - 将应用指定的投影,如果没有 - 将返回名称为 default 的投影,将应用应用的第一个找到的投影。此外,我必须添加忽略链接的自定义 ProjectingResource,否则返回的 JSON 中有两个 _links 键。

/**
 * Projecting resource used for {@link ProjectingProcessor}. Does not include empty links in JSON, otherwise two 
 * _links keys are present in returning JSON.
 *
 * @param <T>
 */
@JsonInclude(JsonInclude.Include.NON_EMPTY)
class ProjectingResource<T> extends Resource<T> {

    ProjectingResource(final T content) {
        super(content);
    }
}

/**
 * Resource processor for all resources which applies projection for single resource. By default, projections
 * are not
 * applied when working with single resource, e.g. http://127.0.0.1:8080/users/580793f642d54436e921f6ca. See
 * related issue <a href="https://jira.spring.io/browse/DATAREST-428">DATAREST-428</a>
 */
@Component
public class ProjectingProcessor implements ResourceProcessor<Resource<Object>> {

    private static final String PROJECTION_PARAMETER = "projection";

    private final ProjectionFactory projectionFactory;

    private final RepositoryRestConfiguration repositoryRestConfiguration;

    private final HttpServletRequest request;

    public ProjectingProcessor(@Autowired final RepositoryRestConfiguration repositoryRestConfiguration,
                               @Autowired final ProjectionFactory projectionFactory,
                               @Autowired final HttpServletRequest request) {
        this.repositoryRestConfiguration = repositoryRestConfiguration;
        this.projectionFactory = projectionFactory;
        this.request = request;
    }

    @Override
    public Resource<Object> process(final Resource<Object> resource) {
        if (AopUtils.isAopProxy(resource.getContent())) {
            return resource;
        }

        final Optional<Class<?>> projectionType = findProjectionType(resource.getContent());
        if (projectionType.isPresent()) {
            final Object projection = projectionFactory.createProjection(projectionType.get(), resource
                    .getContent());
            return new ProjectingResource<>(projection);
        }

        return resource;
    }

    private Optional<Class<?>> findProjectionType(final Object content) {
        final String projectionParameter = request.getParameter(PROJECTION_PARAMETER);
        final Map<String, Class<?>> projectionsForType = repositoryRestConfiguration.getProjectionConfiguration()
                .getProjectionsFor(content.getClass());

        if (!projectionsForType.isEmpty()) {
            if (!StringUtils.isEmpty(projectionParameter)) {
                // projection parameter specified
                final Class<?> projectionClass = projectionsForType.get(projectionParameter);
                if (projectionClass != null) {
                    return Optional.of(projectionClass);
                }
            } else if (projectionsForType.containsKey(ProjectionName.DEFAULT)) {
                // default projection exists
                return Optional.of(projectionsForType.get(ProjectionName.DEFAULT));
            }

            // no projection parameter specified
            return Optional.of(projectionsForType.values().iterator().next());
        }

        return Optional.empty();
    }
}

【讨论】:

    【解决方案2】:

    我最近在看类似的东西,当我试图从 Spring Data /Jackson 方面接近它时,我最终陷入了困境。

    另一种非常简单的解决方案是从不同的角度处理它,并确保 HTTP 请求中的 Projection 参数始终存在。这可以通过使用 Servlet 过滤器来修改传入请求的参数来完成。

    这看起来像下面这样:

    public class ProjectionResolverFilter extends GenericFilterBean {
    
        private static final String REQUEST_PARAM_PROJECTION_KEY = "projection";
    
        @Override
        public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
                throws IOException, ServletException {
    
            HttpServletRequest request = (HttpServletRequest) req;
    
            if (shouldApply(request)) {
                chain.doFilter(new ResourceRequestWrapper(request), res);
            } else {
                chain.doFilter(req, res);
            }
        }
    
        /**
         * 
         * @param request
         * @return True if this filter should be applied for this request, otherwise
         *         false.
         */
        protected boolean shouldApply(HttpServletRequest request) {
            return request.getServletPath().matches("some-path");
        }
    
        /**
         * HttpServletRequestWrapper implementation which allows us to wrap and
         * modify the incoming request.
         *
         */
        public class ResourceRequestWrapper extends HttpServletRequestWrapper {
    
            public ResourceRequestWrapper(HttpServletRequest request) {
                super(request);
            }
    
            @Override
            public String getParameter(final String name) {
                if (name.equals(REQUEST_PARAM_PROJECTION_KEY)) {
                    return "nameOfDefaultProjection";
                }
    
                return super.getParameter(name);
            }
        }
    }
    

    【讨论】:

    • 这个解决方案不太适合我正在寻找的东西,因为我感兴趣的是没有传递投影参数的默认资源表示。此外,它对我来说更像是一个“黑客”,使用ResourceProcessor 会更好,例如就像@CollinD 提供的链接一样。不过还是谢谢你的建议。
    • 该解决方案无需传递投影参数,因为它自动应用默认投影 - 就好像它已在请求中传递一样。是的,这有点像 hack,但它很简单而且很有效。如果您可以在获得解决方案时发布作为答案,我将有兴趣看到自定义资源处理器正常工作。
    • 是的,我的错,它确实阻止了传递投影参数的需要。但是,您仍然需要在shouldApply 方法中对路径进行硬编码,或者它可以应用于所有资源?我稍后会与资源处理器核实,如果可行,我会发布答案。
    • 是的,或者您可以注入必要的 SDR 配置来动态查找资源路径。
    • 谢谢@Yuriy。了解有用。
    猜你喜欢
    • 2017-09-27
    • 2017-07-27
    • 2015-04-06
    • 1970-01-01
    • 2018-04-18
    • 2016-01-20
    • 2015-05-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多