Выберите JsonView в Spring MVC Controller
В настоящее время я пишу REST API, используя Jackson (2.4.0-rc3) и Spring MVC (4.0.3), и я пытаюсь сделать его безопасным.
Таким образом, я пытаюсь использовать JsonView для выбора частей объектов, которые можно сериализовать.
Я нашел решение (которое не для меня), чтобы аннотировать мой метод Controller желаемым представлением. Но я бы хотел на лету выбрать вид внутри контроллера.
Можно ли расширить класс ResponseEntity, чтобы указать, какой JsonView я хочу?
Небольшой кусочек кода:
Вот класс аккаунта
public class Account {
@JsonProperty(value = "account_id")
private Long accountId;
@JsonProperty(value = "mail_address")
private String mailAddress;
@JsonProperty(value = "password")
private String password;
@JsonProperty(value = "insert_event")
private Date insertEvent;
@JsonProperty(value = "update_event")
private Date updateEvent;
@JsonProperty(value = "delete_event")
private Date deleteEvent;
@JsonView(value = PublicView.class)
public Long getAccountId() {
return accountId;
}
@JsonView(value = PublicView.class)
public void setAccountId(Long accountId) {
this.accountId = accountId;
}
@JsonView(value = OwnerView.class)
public String getMailAddress() {
return mailAddress;
}
@JsonView(value = OwnerView.class)
public void setMailAddress(String mailAddress) {
this.mailAddress = mailAddress;
}
@JsonIgnore
public String getPassword() {
return password;
}
@JsonView(value = OwnerView.class)
public void setPassword(String password) {
this.password = password;
}
@JsonView(value = AdminView.class)
public Date getInsertEvent() {
return insertEvent;
}
@JsonView(value = AdminView.class)
public void setInsertEvent(Date insertEvent) {
this.insertEvent = insertEvent;
}
@JsonView(value = AdminView.class)
public Date getUpdateEvent() {
return updateEvent;
}
@JsonView(value = AdminView.class)
public void setUpdateEvent(Date updateEvent) {
this.updateEvent = updateEvent;
}
@JsonView(value = AdminView.class)
public Date getDeleteEvent() {
return deleteEvent;
}
@JsonView(value = OwnerView.class)
public void setDeleteEvent(Date deleteEvent) {
this.deleteEvent = deleteEvent;
}
@JsonProperty(value = "name")
public abstract String getName();
}
Вот контроллер аккаунта
@RestController
@RequestMapping("/account")
public class AccountCtrlImpl implements AccountCtrl {
@Autowired
private AccountSrv accountSrv;
public AccountSrv getAccountSrv() {
return accountSrv;
}
public void setAccountSrv(AccountSrv accountSrv) {
this.accountSrv = accountSrv;
}
@Override
@RequestMapping(value = "/get_by_id/{accountId}", method = RequestMethod.GET, headers = "Accept=application/json")
public ResponseEntity<Account> getById(@PathVariable(value = "accountId") Long accountId) {
try {
return new ResponseEntity<Account>(this.getAccountSrv().getById(accountId), HttpStatus.OK);
} catch (ServiceException e) {
return new ResponseEntity<Account>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Override
@RequestMapping(value = "/get_by_mail_address/{mail_address}", method = RequestMethod.GET, headers = "Accept=application/json")
public ResponseEntity<Account> getByMailAddress(@PathVariable(value = "mail_address") String mailAddress) {
try {
return new ResponseEntity<Account>(this.getAccountSrv().getByMailAddress(mailAddress), HttpStatus.OK);
} catch (ServiceException e) {
return new ResponseEntity<Account>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Override
@RequestMapping(value = "/authenticate/{mail_address}/{password}", method = RequestMethod.GET, headers = "Accept=application/json")
public ResponseEntity<Account> authenticate(@PathVariable(value = "mail_address") String mailAddress, @PathVariable(value = "password") String password) {
return new ResponseEntity<Account>(HttpStatus.NOT_IMPLEMENTED);
}
}
4 ответа
Я решил мою проблему расширения ResponseEntity следующим образом:
public class ResponseViewEntity<T> extends ResponseEntity<ContainerViewEntity<T>> {
private Class<? extends BaseView> view;
public ResponseViewEntity(HttpStatus statusCode) {
super(statusCode);
}
public ResponseViewEntity(T body, HttpStatus statusCode) {
super(new ContainerViewEntity<T>(body, BaseView.class), statusCode);
}
public ResponseViewEntity(T body, Class<? extends BaseView> view, HttpStatus statusCode) {
super(new ContainerViewEntity<T>(body, view), statusCode);
}
}
и ContainerViewEntity инкапсулируют объект и выбранное представление
public class ContainerViewEntity<T> {
private final T object;
private final Class<? extends BaseView> view;
public ContainerViewEntity(T object, Class<? extends BaseView> view) {
this.object = object;
this.view = view;
}
public T getObject() {
return object;
}
public Class<? extends BaseView> getView() {
return view;
}
public boolean hasView() {
return this.getView() != null;
}
}
После этого мы конвертировали только объект с хорошим обзором.
public class JsonViewMessageConverter extends MappingJackson2HttpMessageConverter {
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
if (object instanceof ContainerViewEntity && ((ContainerViewEntity) object).hasView()) {
writeView((ContainerViewEntity) object, outputMessage);
} else {
super.writeInternal(object, outputMessage);
}
}
protected void writeView(ContainerViewEntity view, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
JsonEncoding encoding = this.getJsonEncoding(outputMessage.getHeaders().getContentType());
ObjectWriter writer = this.getWriterForView(view.getView());
JsonGenerator jsonGenerator = writer.getFactory().createGenerator(outputMessage.getBody(), encoding);
try {
writer.writeValue(jsonGenerator, view.getObject());
} catch (IOException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
}
}
private ObjectWriter getWriterForView(Class<?> view) {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
return mapper.writer().withView(view);
}
}
И чтобы закончить, я включаю конвертер
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="wc.handler.view.JsonViewMessageConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
И это все, я могу выбрать View в контроллере
@Override
@RequestMapping(value = "/get_by_id/{accountId}", method = RequestMethod.GET, headers = "Accept=application/json")
public ResponseViewEntity<Account> getById(@PathVariable(value = "accountId") Long accountId) throws ServiceException {
return new ResponseViewEntity<Account>(this.getAccountSrv().getById(accountId), PublicView.class, HttpStatus.OK);
}
Мне действительно нравится решение, представленное здесь, чтобы динамически выбирать представление json внутри вашего метода контроллера.
По сути, вы возвращаете MappingJacksonValue
который вы создаете со значением, которое вы хотите вернуть. После этого вы звоните setSerializationView(viewClass)
с правильным классом представления. В моем случае использования я вернул другое представление в зависимости от текущего пользователя, что-то вроде этого:
@RequestMapping("/foos")
public MappingJacksonValue getFoo(@AuthenticationPrincipal UserDetails userDetails ) {
MappingJacksonValue value = new MappingJacksonValue( fooService.getAll() );
if( userDetails.isAdminUser() ) {
value.setSerializationView( Views.AdminView.class );
} else {
value.setSerializationView( Views.UserView.class );
}
return value;
}
Кстати: если вы используете Spring Boot, вы можете контролировать, сериализуются ли свойства, не имеющие представления, или нет, установив это в своем application.properties
:
spring.jackson.mapper.default_view_inclusion=true
К вашему сведению, Spring 4.1 уже поддерживает использование @JsonView непосредственно на @ResponseBody и ResponseEntity:
@JsonView Джексона поддерживается непосредственно в методах контроллера @ResponseBody и ResponseEntity для сериализации различного количества деталей для одного и того же POJO (например, сводка и страница подробностей). Это также поддерживается при рендеринге на основе представлений путем добавления типа представления сериализации в качестве атрибута модели под специальным ключом.
И в http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html вы можете найти гораздо более простое решение:
@RestController
public class UserController {
@RequestMapping(value = "/user", method = RequestMethod.GET)
@JsonView(User.WithoutPasswordView.class)
public User getUser() {
return new User("eric", "7!jd#h23");
}
}
public class User {
public interface WithoutPasswordView {};
public interface WithPasswordView extends WithoutPasswordView {};
private String username;
private String password;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
@JsonView(WithoutPasswordView.class)
public String getUsername() {
return this.username;
}
@JsonView(WithPasswordView.class)
public String getPassword() {
return this.password;
}
}
Это прекрасно работает:
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public void getZone(@PathVariable long id, @RequestParam(name = "tree", required = false) boolean withChildren, HttpServletResponse response) throws IOException {
LOGGER.debug("Get a specific zone with id {}", id);
Zone zone = zoneService.findById(id);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
if (withChildren) {
response.getWriter().append(mapper.writeValueAsString(zone));
} else {
response.getWriter().append(mapper.writerWithView(View.ZoneWithoutChildren.class).writeValueAsString(zone));
}
}