Adding RSS to Spring Boot Application With ROME
- access_time 3 years ago
- date_range 02/10/2015
- comment 0
- share 2
- label_outlineSpring Boot, Spring MVC
I have recently added an RSS feed to the site and thought I'm going to explain how it can be done. This is not a rocket science though it'll be good to discuss how to fit it into modern Spring Boot app using Java Configuration. In the process maybe one can also learn about creating custom views and some inner workings of Spring MVC.
RSS feed
An RSS feed is an XML document containing elements adhering to specs. The simplest one may look like this:
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Channel title</title>
<link>http://someurl/feed/</link>
<description>Some description</description>
<pubDate>Mon, 13 Jul 2015 12:51:08 GMT</pubDate>
<item>
<title>Item title</title>
<link>http://someurl/</link>
<description>Description</description>
<pubDate>Mon, 13 Jul 2015 12:51:08 GMT</pubDate>
</item>
<item>
<!-- there can be more items like the one above -->
</item>
</channel>
</rss>
As you see in the RSS world there is a <channel>
with multiple <item>
tags inside it.
Both <title>
and <link>
are required for <channel>
, whereas <item>
requires either <title>
or <description>
.
Having <pubDate>
set is nice for clients as they may choose to check it and not to update their local copy of the channel
if it's the same as what they've seen before. There are other possible elements, maybe you might want to read the specs,
but above is sufficient for a feed to work.
Domain model
Say I have a domain object Document
in the system that I want to appear in the feed:
public class Document {
private String id;
private String title;
private String description;
private String contents;
private Date datePublished;
// getters, setters
}
Nothing special here, standard stuff, this can be an @Entity
and be fetched from a database as explained in other posts on this site.
It's also obvious that it should map to RSS <item>
quite well.
What is a View in Spring MVC?
It is defined by this interface:
public interface View {
String getContentType();
void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
}
As you can see it's something that roughly takes a model Map
and HttpServletRequest
as an input and
uses HttpServletResponse
to render something fancy out of it.
There are of course many implementations of a View that come with Spring. Some of them render a view from JSP pages (JstlView
), some serialize the model to JSON (MappingJackson2JsonView
),
whatever your client needs.
Knowing all of the above we could build an RSS feed using views provided with Spring in a same way as we serve HTML,
i.e. we can build a feed from JSP page that will be served with application/rss+xml
content type. Or even implement own View
that
will render the feed directly, it's just text after all. But why re-invent a wheel as we can have something dedicated to the task.
It also would allow us to abstract out the creation process in case we'd like to switch RSS versions or move to Atom feeds entirely.
Enter ROME library
There's a couple of libraries to choose from, however Spring provides integration with ROME, which narrows down the choice. This is not a problem though as ROME supporting both RSS and Atom feeds in different versions seems to have what it takes.
So, as usual we need to add a dependency to pom.xml
:
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.5.0</version>
</dependency>
Whole strategy of using ROME gets down to creating a View containing our feed that will be served by a Controller method in a response to a request.
Creating a View for RSS feed
Spring provides AbstractRssFeedView
abstract class which is an implementation of View
that is using ROME underneath to create a RSS feed.
It has simple interface, exposes some methods to be overridden:
public abstract class AbstractRssFeedView extends AbstractFeedView<Channel> {
// I've skipped parts for clarity, left only those that are meant to be overridden
public AbstractRssFeedView() {
setContentType("application/rss+xml");
}
protected Channel newFeed() {
return new Channel("rss_2.0");
}
protected abstract List<Item> buildFeedItems(Map<String, Object> model,
HttpServletRequest request,
HttpServletResponse response)
throws Exception;
}
All we need to do is to extend it to create a Channel
and a List
containing Item
objects and leave the rest to Spring and ROME.
What I have actually done looks more or less like this:
@Component("documentRssFeedView")
public class DocumentRssFeedView extends AbstractRssFeedView {
private final DocumentService documentService;
@Value("${application.base-url}")
private String baseUrl;
@Autowired
public DocumentRssFeedView(DocumentService documentService) {
this.documentService = documentService;
}
@Override
protected Channel newFeed() {
Channel channel = new Channel("rss_2.0");
channel.setLink(baseUrl + "/feed/");
channel.setTitle(CHANNEL_TITLE);
channel.setDescription(CHANNEL_DESCRIPTION);
documentService.getOneMostRecent().ifPresent(d -> channel.setPubDate(d.getDatePublished()));
return channel;
}
@Override
protected List<Item> buildFeedItems(Map<String, Object> model,
HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse) throws Exception {
return documentService.getRecent(NUMBER_OF_ITEMS).stream()
.map(this::createItem)
.collect(Collectors.toList());
}
private Item createItem(Document document) {
Item item = new Item();
item.setLink(baseUrl + document.getId());
item.setTitle(document.getTitle());
item.setDescription(createDescription(document));
item.setPubDate(document.getDatePublished());
return item;
}
private Description createDescription(Document document) {
Description description = new Description();
description.setType(Content.HTML);
description.setValue(document.getDescription());
return description;
}
}
This requires an explanation:
-
@Component("documentRssFeedView")
- this makes a Spring component out of it, with specific name set. This will cause Spring Boot to 'notice it' and specific name will allow us to select this one view apart from others. -
DocumentService
is a service I made to deal withDocument
, has two relevant methods:public interface DocumentService { Collection<Document> getRecent(int count); Optional<Document> getOneMostRecent(); }
One returns a
Collection
of most recently publishedDocument
entities, the other optional most recent document (empty if there is none). -
baseUrl
property gets populated fromapplication.properties
and is used to construct URLs. -
In
newFeed()
theChannel
gets created, withlink
,title
anddescription
properties set, which are required for the feed to be valid.pubDate
property is set to a publication date of a most recentDocument
. -
In
buildFeedItems()
theCollection
ofDocument
entities is pulled fromDocumentService
, and is mapped to aList
ofItem
objects, and mapping myself is quite straightforward here. -
I'm not overriding the constructor, as it already does what I want - it sets the right content type this view will provide.
Looking at this class one may ask why all the parameters of buildFeedItems
are ignored here. Well, I'm not really interested in
reading request
and modifying response
here as the feed contents will be the same for each request, and
the job to modify response
belongs to parent classes.
As for the model
I could have made it so the Collection of Document
entities would have been given in a Map
, presumably set by a Controller 'speaking' to DocumentService
as it's supposed to be.
However that would require some tedious code to check if it's actually there, and given the Object
type for Map
values also checking and casting the value to required type.
Too much hassle I'd say for no obvious gain as this implementation is too concrete for that.
Plugging the View into the Controller
Most of the plumbing is done by Spring Boot, so to make use of this view a Controller method can be written like that:
@Controller
public class DocumentController {
@RequestMapping(value = "/feed/", produces = "application/*")
public String getFeed() {
return "documentRssFeedView";
}
}
This should be enough to map /feed/
request path to our DocumentRssFeedView
and got it rendered. Enjoy.
However it would be nice to know why that works, and even nicer when the view fails to render to find out what's going on. So let me explain:
- When Spring encounters a bean implementing
View
interface, exactly like the one we've created, it 'knows' it's one of available views rendering content ofapplication/xml+rss
type. - The request comes for
/feed/
and we're returningdocumentRssFeedView
as a view name. - This is where
ViewResolver
kicks in - based on the request (content types accepted by client, request path) and view name returned from the method it tries to figure which view out of available views will be used. - There is actually a chain of
ViewResolver
objects, they executed in sequence, and the first one that figures the view 'wins'. - One of view resolvers is
BeanNameViewResolver
, which knows "documentRssFeedView" is a view name. It looks for a bean with the same name implementingView
. That's how it finds ourDocumentRssFeedView
, and that's exactly what we hope for to happen.
What may cause possible problems is that the chain of ViewResolver
is setup and ordered by Spring Boot based on what's on the classpath. Moreover BeanNameViewResolver
ends up somewhere at the end of the chain.
Imagine the client makes a request to /feed/
with an usual header saying it accepts [text/html, application/xhtml+xml, image/webp, application/xml;q=0.9, */*;q=0.8]
.
This may lead to ambiguities. For example InternalViewResolver
, which is earlier in the chain, may believe that documentRssFeedView
refers to internal resource with content type text/html
and it will try to read it. This will end up in client getting 404 NOT FOUND
status.
This is prevented here by adding produces = "application/*"
to @RequestMapping
on the Controller method as it eliminates views producing content types other than that from being considered.
Other way to make our view applicable would be to have the method on a path indicating content type, i.e. /feed.xml
. It narrows down what Spring thinks can be accepted by the client, effectively accomplishing the same.
The ultimate solution for this kind of problem would be to inject DocumentRssFeedView
straight into the Controller and just return it from the method:
@Autowired
private DocumentRssFeedView view;
@RequestMapping(value = "/feed/")
public DocumentRssFeedView getFeed() {
return view;
}
That would just bypass ViewResolver
chain entirely, probably also saving some processing time.
Conclusion
Yup, it's nice and relatively easy to add RSS feed to a Spring Boot application. If you want to look at the code - the source code of Akanke can serve as an example of such integration.
One last tip maybe - RSS feeds are usually a nice candidate to be cached. They are relatively static, don't update so frequently, but may be accessed quite often by people's RSS readers. Why waste resources to build them each time. With upcoming Spring 1.3 it can be done with annotations alone.