Spring数据REST:单个资源的投影表示

Spring数据REST:单个资源的投影表示,spring,rest,spring-data,spring-data-rest,spring-hateoas,Spring,Rest,Spring Data,Spring Data Rest,Spring Hateoas,我有一个简单的UserRepository,它使用springdatarest公开。 以下是用户实体类: @Document(collection = User.COLLECTION_NAME) @Setter @Getter public class User extends Entity { public static final String COLLECTION_NAME = "users"; private String name; private Stri

我有一个简单的
UserRepository
,它使用
springdatarest
公开。 以下是
用户
实体类:

@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);
}
以下是存储库类:

@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);
}
因此,当我在/users路径上执行GET时,我得到以下响应(应用投影):

但是,当我尝试调用单个资源(例如,/users/5812193156ae116256a33d4)时,我得到以下响应:

{
  "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的一篇文章,关于节选预测为什么不能自动应用于单个资源。但是,在我的情况下仍然非常不方便,当我得到一个资源时,我希望返回相同的
UserProjection
。在不创建自定义控制器或用
@JsonIgnore
标记字段的情况下,是否有可能做到这一点?

我最近看到了类似的东西,但在试图从Spring数据/Jackson方面接近它时,结果却绕了一圈

另一种非常简单的解决方案是从不同的角度来处理它,并确保HTTP请求中的投影参数始终存在。这可以通过使用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
类,按照中的建议对任何资源应用预测。其工作方式如下:如果在URL中指定了投影参数,则将应用指定的投影,如果未指定,则将返回名为default的投影,将应用应用的第一个找到的投影。另外,我必须添加自定义
ProjectingResource
,忽略链接,否则返回的JSON中有两个
\u链接

/**
 * 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();
    }
}
/**
*用于{@link ProjectingProcessor}的投影资源。不包括JSON中的空链接,否则为两个
*返回的JSON中存在链接键。
*
*@param
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
类ProjectingResource扩展了资源{
ProjectingResource(最终T内容){
超级(内容);
}
}
/**
*所有资源的资源处理器,为单个资源应用投影。默认情况下,预测
*不是
*在使用单个资源时应用,例如。http://127.0.0.1:8080/users/580793f642d54436e921f6ca. 看见
*相关问题
*/
@组成部分
公共类ProjectingProcessor实现ResourceProcessor{
私有静态最终字符串投影\u参数=“投影”;
私人最终项目工厂项目工厂;
私有最终RepositoryRestConfiguration RepositoryRestConfiguration;
私有最终HttpServletRequest;
公共项目处理器(@Autowired final RepositoryRestConfiguration RepositoryRestConfiguration,
@自动连线最终项目工厂项目工厂,
@自动连线最终HttpServletRequest(请求){
this.repositoryRestConfiguration=repositoryRestConfiguration;
this.projectionFactory=projectionFactory;
this.request=请求;
}
@凌驾
公共资源流程(最终资源){
if(AopUtils.isAopProxy(resource.getContent())){
返回资源;
}
最终可选>FindProjectType(最终对象内容){
最终字符串projectionParameter=request.getParameter(PROJECTION\u参数);
最终映射projectionClass=ProjectionForType.get(projectionParameter);
if(projectionClass!=null){
返回可选的.of(projectionClass);
}
}else if(projectionsForType.containsKey(ProjectionName.DEFAULT)){
//存在默认投影
返回可选的.of(projectionsForType.get(ProjectionName.DEFAULT));
}
//未指定投影参数
返回可选的.of(projectionsForType.values().iterator().next());
}
返回可选的.empty();
}
}

您可以使用
ResourceProcessor
来实现这一点(或类似效果)。在@CollinD的评论中有一些潜在的有用的讨论,谢谢你的链接!似乎使用
ResourceProcessor
ResourceAssembler
的解决方案非常适合这种情况。然而,我仍然想知道为什么SpringDataREST中没有针对这个的注释或配置。ResourceProcessor现在是RepresentationModelProcessor:这个解决方案不太适合我所寻找的,因为我感兴趣的是没有传递投影参数的默认资源表示。对我来说,它看起来更像是一个“黑客”,最好使用
ResourceProcessor
,例如@CollinD提供的链接。不过还是要感谢您的建议。解决方案避免了传递投影参数的需要,因为它会自动应用默认投影,就像它已在请求中传递一样。是的,这是一个有点黑客,但它很简单,它的工作。如果你能得到一个解决方案,作为一个答案发布,我很想看到定制资源处理器工作。是的,我的错,它确实阻止了传递投影参数的需要。但是,您仍然需要在
shouldApply
方法中对路径进行硬编码,或者它可以应用于所有资源?稍后我将检查资源过程
{
  "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
    }
  }
}
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);
        }
    }
}
/**
 * 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();
    }
}