Spring数据REST:单一资源的投影表示
Spring Data REST: projection representation of single resource
我有一个简单的 UserRepository
,它使用 Spring Data REST
公开。
这是 User
实体 class:
@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
class,它看起来如下所示:
@JsonInclude(JsonInclude.Include.NON_NULL)
@Projection(types = User.class)
public interface UserProjection {
String getId();
String getName();
String getEmail();
Set<UserRole> getRoles();
}
这是存储库 class:
@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
}
}
}
如您所见,密码字段正在 returned 并且未应用投影。我知道有 @JsonIgnore
注释可用于隐藏资源的敏感数据。但是,我的 User
对象位于不同的应用程序模块中,该模块不知道 API 或 JSON 表示,因此用 @JsonIgnore
注释标记字段没有意义.
我看过@Oliver Gierke 的 post 关于为什么摘录预测没有自动应用于单个资源的文章。但是,这对我来说仍然很不方便,我想在获得单个资源时 return 同样 UserProjection
。是否可以在不创建自定义控制器或使用 @JsonIgnore
?
标记字段的情况下以某种方式做到这一点
我最近在看类似的东西,但在试图从 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
class,它按照 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();
}
}
我有一个简单的 UserRepository
,它使用 Spring Data REST
公开。
这是 User
实体 class:
@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
class,它看起来如下所示:
@JsonInclude(JsonInclude.Include.NON_NULL)
@Projection(types = User.class)
public interface UserProjection {
String getId();
String getName();
String getEmail();
Set<UserRole> getRoles();
}
这是存储库 class:
@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
}
}
}
如您所见,密码字段正在 returned 并且未应用投影。我知道有 @JsonIgnore
注释可用于隐藏资源的敏感数据。但是,我的 User
对象位于不同的应用程序模块中,该模块不知道 API 或 JSON 表示,因此用 @JsonIgnore
注释标记字段没有意义.
我看过@Oliver Gierke UserProjection
。是否可以在不创建自定义控制器或使用 @JsonIgnore
?
我最近在看类似的东西,但在试图从 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
class,它按照 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();
}
}