以除了application/json之外的Content-Type将HATEOAS对象作为WebSocket消息广播。

huangapple go评论154阅读模式
英文:

Broadcast a HATEOAS object as a websocket message with a Content-Type other than application/json

问题

I need to render the class below (among several other classes that extends RepresentationModel) both on a RESTController and as a broadcast message to a websocket.

@Getter
@JsonPropertyOrder(alphabetic=true)
@Setter
public class MessageWrapper extends RepresentationModel<MessageWrapper> {
    private Object message;
    private String simpleName;

    public MessageWrapper(Object message) {
        this.message = message;
        this.simpleName = message.getClass().getSimpleName();
    }
}

I render it to the response body using the code below:

@RequestMapping(
    headers = {
        "Content-Type=application/json",
        "X-Requested-With=XMLHttpRequest"
    },
    method = RequestMethod.POST,
    produces = "application/hal+json",
    value = "/thread/{threadId}/message")
public HttpEntity<MessageWrapper> post(...) {
    MessageWrapper wrapper = ...

    HttpEntity<MessageWrapper> entity =
        new ResponseEntity<MessageWrapper>(wrapper, HttpStatus.OK);

    return entity;
}

And it produces the following truncated response:

HTTP/1.1 200 
...
Content-Type: application/hal+json

{
    "_links": {
        "previous": {
            "href": "/thread/1044/message/86"
        },
        "self": {
            "href": "/thread/1044/message/87"
        }
    },
    "message": {
        ...
    },
    "simpleName":"..."
}

Notice the Content-Type of the response and the _links object attribute above. I created a JavaScript handler for that Content-Type, and that _links attribute is critical. Now on a separate event sometime later, I need to broadcast the exact same JSON String to a websocket. So I did this:

@Autowired
private SimpMessagingTemplate messagingTemplate;

public void broadcastToUser(StompPrincipal principal, MessageWrapper wrapper) {
    SimpMessageHeaderAccessor simpHeaderAccessor =
        SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);

    simpHeaderAccessor.setUser(principal);
    simpHeaderAccessor.setLeaveMutable(true);

    messagingTemplate.convertAndSendToUser(
        principal.getUsername(),
        "/broadcast",
        wrapper,
        simpHeaderAccessor.getMessageHeaders());
}

And it produces the following message:

a["MESSAGE
destination:/user/broadcast
content-type:application/json
subscription:sub-0
message-id:tluhntae-0
content-length:267

{
    "links": [
        {
            "rel": "previous",
            "href": "/thread/1044/message/86"
        },
        {
            "rel": "self",
            "href": "/thread/1044/message/87"
        }
    ],
    "message": {...},
    "simpleName": "..."
}

"]

Notice that it has the links attribute instead of _links. Also notice that instead of having an object value like the one above, it has an array of objects as the value. I am supposed to use the same JavaScript callback to the RESTController response above but cannot since the links attribute has a different structure. I can marshal it on the JavaScript side, but I'd rather not and instead attempted to configure it to render it as what I expected it to be rendered. I read that this is because I am rendering the websocket message as application/json instead of application/hal+json, that's why it uses links instead of _links. So I set the Content-Type of the message as HAL before broadcasting it:

MimeType applicationJson;
    
if (RepresentationModel.class.isAssignableFrom(body.getClass())) {
    applicationJson = new MimeType("application", "hal+json");
} else {
    applicationJson = new MimeType("application", "json");
}

simpHeaderAccessor.setContentType(applicationJson);

It throws the following error though:

2020-10-26 23:40:40.799 ERROR 6200 --- [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.messaging.converter.MessageConversionException: Unable to convert payload with type='MessageWrapper', contentType='application/hal+json', converter=[CompositeMessageConverter[converters=[org.springframework.messaging.converter.StringMessageConverter@66389c40, org.springframework.messaging.converter.ByteArrayMessageConverter@724493b5, org.springframework.messaging.converter.MappingJackson2MessageConverter@1228bacc]]]] with root cause

org.springframework.messaging.converter.MessageConversionException: Unable to convert payload with type='MessageWrapper', contentType='application/hal+json', converter=[CompositeMessageConverter[converters=[org.springframework.messaging.converter.StringMessageConverter@66389c40, org.springframework.messaging.converter.ByteArrayMessageConverter@724493b5, org.springframework.messaging.converter.MappingJackson2MessageConverter@1228bacc]]]
    at org.springframework.messaging.core.AbstractMessageSendingTemplate.doConvert(AbstractMessageSendingTemplate.java:187) ~[spring-messaging-5.3.0-M1.jar:5.3.0-M1]
    at org.springframework.messaging.core.AbstractMessageSendingTemplate.convertAndSend(AbstractMessageSendingTemplate.java:150) ~[spring-messaging-5.3.0-M1.jar:5.3.0-M1]
    at org.springframework.messaging.simp.SimpMessagingTemplate.convertAndSendToUser(SimpMessagingTemplate.java:230) ~[spring-messaging-5.3.0-M1.jar:5.3.0-M1]
    at org.springframework.messaging.simp.SimpMessagingTemplate.convertAndSendToUser(SimpMessagingTemplate.java:211) ~[spring-messaging-5.3.0-M1.jar:5.3.0-M1]
    at dev.gideon.chatapp.service.BroadcastService.broadcastToUser(BroadcastService.java:54) ~[classes/:na]
    ...

I really need to set the websocket message's Content-Type to HAL for it to be marshalled the way I expected it to be. Is there any way to implement this? I am asking this question because, aside from me not wanting to write separate JavaScript code that handles the JSON object with the links attribute with an array value and the _links attribute with the object value, I also believe that it is indeed proper for it to be broadcasted with a HAL Content-Type so that other clients aside from my own will treat it as such and not just a normal JSON broadcasted message.

英文:

I need to render the class below (among several other classes that extends RepresentationModel) both on a RESTController and as a broadcast message to a websocket.

@Getter
@JsonPropertyOrder(alphabetic=true)
@Setter
public class MessageWrapper extends RepresentationModel&lt;MessageWrapper&gt; {
	private Object message;
	private String simpleName;

	public MessageWrapper(Object message) {
		this.message = message;
		this.simpleName = message.getClass().getSimpleName();
	}
}

I render it to the response body using the code below:

@RequestMapping(
	headers = {
		&quot;Content-Type=application/json&quot;,
		&quot;X-Requested-With=XMLHttpRequest&quot;
	},
	method = RequestMethod.POST,
	produces = &quot;application/hal+json&quot;,
	value = &quot;/thread/{threadId}/message&quot;)
public HttpEntity&lt;MessageWrapper&gt; post(...) {
	MessageWrapper wrapper = ...

	HttpEntity&lt;MessageWrapper&gt; entity =
		new ResponseEntity&lt;HateoasWrapper&gt;(wrapper, HttpStatus.OK);

	return entity;
}

And it produces the following truncated response:

HTTP/1.1 200 
...
Content-Type: application/hal+json

{
	&quot;_links&quot;: {
		&quot;previous&quot;: {
			&quot;href&quot;: &quot;/thread/1044/message/86&quot;
		},
		&quot;self&quot;: {
			&quot;href&quot;: &quot;/thread/1044/message/87&quot;
		}
	},
	&quot;message&quot;: {
		...
	},
	&quot;simpleName&quot;:&quot;...&quot;
}

Notice the Content-Type of the response and the _links object attribute above. I created a javascript handler for that Content-Type and that _links attribute is critical. Now on a separate event sometime later, I need to broadcast the exact same JSON String to a websocket. So I did this:

@Autowired
private SimpMessagingTemplate messagingTemplate;

public void broadcastToUser(StompPrincipal principal, MessageWrapper wrapper) {
	SimpMessageHeaderAccessor simpHeaderAccessor =
		SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);

	simpHeaderAccessor.setUser(principal);
	simpHeaderAccessor.setLeaveMutable(true);

	messagingTemplate.convertAndSendToUser(
		principal.getUsername(),
		&quot;/broadcast&quot;,
		wrapper,
		simpHeaderAccessor.getMessageHeaders());
}

And it produces the following message:

a[&quot;MESSAGE
destination:/user/broadcast
content-type:application/json
subscription:sub-0
message-id:tluhntae-0
content-length:267

{
	&quot;links&quot;: [
		{
			&quot;rel&quot;: &quot;previous&quot;,
			&quot;href&quot;: &quot;/thread/1044/message/86&quot;
		},
		{
			&quot;rel&quot;: &quot;self&quot;,
			&quot;href&quot;: &quot;/thread/1044/message/87&quot;
		}
	],
	&quot;message&quot;: {...},
	&quot;simpleName&quot;: &quot;...&quot;
}

&quot;]

Notice that it has the links attribute instead of _links. Also notice that instead of having an object value like the one above, it has an array of objects as value. I am suppose to use the same javascript callback to the RESTController response above but cannot since the links attribute has a different structure. I can marshall it on the javascript side but I rather not an instead attempted to configure it to render it as what I expected it to be rendered. I read that this is because I am rendering the websocket message as application/json instead of application/hal+json thats why it uses links instead of _links. So I set the Content-Type of the message as HAL before broadcasting it:

MimeType applicationJson;
	    
if(RepresentationModel.class.isAssignableFrom(body.getClass())) {
	applicationJson = new MimeType(&quot;application&quot;, &quot;hal+json&quot;);
} else {
	applicationJson = new MimeType(&quot;application&quot;, &quot;json&quot;);
}

simpHeaderAccessor.setContentType(applicationJson);

It throws the following error though:

2020-10-26 23:40:40.799 ERROR 6200 --- [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.messaging.converter.MessageConversionException: Unable to convert payload with type=&#39;MessageWrapper&#39;, contentType=&#39;application/hal+json&#39;, converter=[CompositeMessageConverter[converters=[org.springframework.messaging.converter.StringMessageConverter@66389c40, org.springframework.messaging.converter.ByteArrayMessageConverter@724493b5, org.springframework.messaging.converter.MappingJackson2MessageConverter@1228bacc]]]] with root cause

org.springframework.messaging.converter.MessageConversionException: Unable to convert payload with type=&#39;MessageWrapper&#39;, contentType=&#39;application/hal+json&#39;, converter=[CompositeMessageConverter[converters=[org.springframework.messaging.converter.StringMessageConverter@66389c40, org.springframework.messaging.converter.ByteArrayMessageConverter@724493b5, org.springframework.messaging.converter.MappingJackson2MessageConverter@1228bacc]]]
	at org.springframework.messaging.core.AbstractMessageSendingTemplate.doConvert(AbstractMessageSendingTemplate.java:187) ~[spring-messaging-5.3.0-M1.jar:5.3.0-M1]
	at org.springframework.messaging.core.AbstractMessageSendingTemplate.convertAndSend(AbstractMessageSendingTemplate.java:150) ~[spring-messaging-5.3.0-M1.jar:5.3.0-M1]
	at org.springframework.messaging.simp.SimpMessagingTemplate.convertAndSendToUser(SimpMessagingTemplate.java:230) ~[spring-messaging-5.3.0-M1.jar:5.3.0-M1]
	at org.springframework.messaging.simp.SimpMessagingTemplate.convertAndSendToUser(SimpMessagingTemplate.java:211) ~[spring-messaging-5.3.0-M1.jar:5.3.0-M1]
	at dev.gideon.chatapp.service.BroadcastService.broadcastToUser(BroadcastService.java:54) ~[classes/:na]
	...

I really need to set the websocket message's Content-Type to HAL for it to be marshalled the way I expected it to be. Is there anyway to implement this? I am asking this question because aside from me not wanting to do separate javascript codes that handles the json object with the links attribute with an array value and the _links attribute with the object value, I also believe that it is indeed proper for it to be broadcasted with a HAL Content-Type so that other clients aside from my own will treat it as such and not just a normal JSON broadcasted message.

答案1

得分: 1

没有默认的MessageConverter适用于Content-Typeapplication/hal+json的WebSocket广播消息。我需要为自定义的MIME类型实现自己的MessageConverter。我创建了一个扩展了AbstractMessageConverter的转换器:

public class HalMessageConverter extends AbstractMessageConverter {
    public HalMessageConverter() {
        MimeType hal = new MimeType("application", "hal+json");
        this.addSupportedMimeTypes(hal);
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return RepresentationModel.class.isAssignableFrom(clazz);
    }

    @Override
    protected Object convertToInternal(
        Object payload,
        MessageHeaders headers,
        Object conversionHint) {

        TypeConstrainedMappingJackson2HttpMessageConverter converter = 
            new TypeConstrainedMappingJackson2HttpMessageConverter(RepresentationModel.class);

        Module module = new Jackson2HalModule();
        LinkRelationProvider provider = new DefaultLinkRelationProvider();

        Jackson2HalModule.HalHandlerInstantiator instantiator =
            new Jackson2HalModule.HalHandlerInstantiator(
                provider,
                CurieProvider.NONE,
                MessageResolver.DEFAULTS_ONLY);

        ObjectMapper mapper = converter.getObjectMapper();
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        mapper.registerModule(module);
        mapper.setHandlerInstantiator(instantiator);

        try {
            String payloadJson = mapper.writeValueAsString(payload);
            byte[] bytes = payloadJson.getBytes();
            return bytes;
        } catch(JsonProcessingException exception) {
            return null;
        }
    }
}

由于addSupportedMimeTypes方法是protected的,我需要创建一个自定义类。convertToInternalMethod包含了转换逻辑。Jackson2HalModule类正是我一直在寻找的。这个类是为什么在使用Content-Typeapplication/hal+json的情况下,links会变成_links的原因。

创建了这个自定义的MessageConverter之后,我将其添加到了应用程序的默认转换器中:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
        messageConverters.add(new HalMessageConverter());
        return false;
    }
}
英文:

There are no default MessageConverter for websocket broadcast message of Content-Type application/hal+json. I need to implement my own MessageConverter for my custom MIME type. I created a converter that extends AbstractMessageConverter:

public class HalMessageConverter extends AbstractMessageConverter {
	public HalMessageConverter() {
		MimeType hal = new MimeType(&quot;application&quot;, &quot;hal+json&quot;);
		this.addSupportedMimeTypes(hal);
	}

	@Override
	protected boolean supports(Class&lt;?&gt; clazz) {
		return RepresentationModel.class.isAssignableFrom(clazz);
	}

	@Override
	protected Object convertToInternal(
		Object payload,
		MessageHeaders headers,
		Object conversionHint) {

		TypeConstrainedMappingJackson2HttpMessageConverter converter = 
			new TypeConstrainedMappingJackson2HttpMessageConverter(RepresentationModel.class);

		Module module = new Jackson2HalModule();
		LinkRelationProvider provider = new DefaultLinkRelationProvider();

		Jackson2HalModule.HalHandlerInstantiator instantiator =
			new Jackson2HalModule.HalHandlerInstantiator(
				provider,
				CurieProvider.NONE,
				MessageResolver.DEFAULTS_ONLY);

		ObjectMapper mapper = converter.getObjectMapper();
		mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
		mapper.registerModule(module);
		mapper.setHandlerInstantiator(instantiator);

		try {
			String payloadJson = mapper.writeValueAsString(payload);
			byte[] bytes = payloadJson.getBytes();
			return bytes;
		} catch(JsonProcessingException exception) {
			return null;
		}
	}
}

I need to create a custom class since the method addSupportedMimeTypes is protected. The convertToInternalMethod holds the conversion logic. The Jackson2HalModule class is what I've been looking for all this time. This class is the reason why links becomes _links when a Response is sent with a Content-Type of application/hal+json.

After creating this custom MessageConverter, I added this to the default converters of the application:

@Configuration @EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
	@Override
	public boolean configureMessageConverters(List&lt;MessageConverter&gt; messageConverters) {
		messageConverters.add(new HalMessageConverter());
		return false;
	}
}

huangapple
  • 本文由 发表于 2020年10月26日 23:55:43
  • 转载请务必保留本文链接:https://go.coder-hub.com/64540632.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定