In the last tutorial we built a simple forum application using the java servlet api.
The servlet api provided us with the HttpServlet
, HttpServletRequest
and HttpServletResponse
classes.
It was enough to build a basic webapp, but there were some downsides:
- we had to write a lot of html in java strings (yuck!)
- writing data on disk took a lot of manual work
- getting the Request-URI from the request object was unintuitive
- sharing objects between servlets was not trivial
- the application was started using maven goals (
jetty:run
)
In this tutorial we're not going to add much new functionality. Instead we're going to ditch the servlet api and move to a more complex and powerful api - the Spring Framework.
We'll be using the code written in the last practice session. Follow these steps to migrate the code to Spring:
- clone this repository
- copy the files from
src/main/java
andsrc/main/resources
to this repository - copy the files from
src/main/webapp
tosrc/main/resources/public
in this repository - make sure not to overwrite the pom.xml - it has changed considerably
- move the servlet classes to the
app.controllers
package (optional)
In the servlet api, the class that was handing the requests was called a servlet. In Spring, the classes are called controllers.
To make your servlets Spring friendly, follow these steps:
- remove the
@WebServlet
annotation - remove the
extends HttpServlet
declaration - remove the
@Override
fromdoGet
anddoPost
- replace
throws ServletException, IOException
withthrows Exception
ondoGet
anddoPost
(and remove any unwanted catch blocks) - add
@Controller
to the class - add
@RequestMapping
to bothdoGet
anddoPost
. set the RequestMapping value to whatever was in the@WebServlet
annotation. set the RequestMapping method to eitherRequestMethod.GET
orRequestMethod.POST
. see theSampleController
class for examples.
Note that the doGet
and doPost
methods no longer implement any interface methods.
When the server is starting up, Spring will look for any classes with the @Controller
annotation.
From these classes, Spring will look for any methods with the @RequestMapping
annotations (the methods are called request handlers).
When the server receives a request, Spring will go through all the methods it found earlier:
- if the Request-URI matches any request handler, then Spring will call that method
- if no methods match, then Spring will try to find a file with a matching name (it will search the /public folder in the classpath).
Since Spring only looks at the @RequestMapping
, you can use any method name you want.
Indeed, you should rename doGet
to something more descriptive, such as listThreads
/listPostsInThread
.
Finally, the request handlers can return different values and take different parameters - see the RequestMapping docs.
You will no longer need to use jetty:run
.
Find the class Application
and start the main method.
The server will start at the usual localhost:8080 address.
You can debug the server like any regular java application.
Migrate your code and make sure the application is still working.
Our thread viewing servlet had a mapping "/thread/threadName", where threadName is a variable.
To get the thread name, we had to use the getRequestURI
method and do some string magic on it.
This is rather clumsy and Spring agrees.
You should have a request handler with @RequestMapping(value = "/threads/*")
.
Replace it with @RequestMapping(value = "/threads/{threadName}")
.
The curly braces indicate to Spring that this is a variable.
Next, add a new parameter to the method: @PathVariable String threadName
(the parameter itself is annotated).
As long as the parameter name matches the variable in the RequestMapping, Spring can match them and automatically provide the parameter value.
See the Spring docs if needed.
Change your code to use @PathVariable
where appropriate.
The Location header with the 3xx status code can be used to redirect the browser to another page. Setting the header and status manually is a lot of work. In Spring, you can redirect more easily:
@RequestMapping("/this/page")
public String redirected(HttpServletRequest req) {
return "redirect:/other/page";
}
Update your controllers to use Spring redirect.
Currently you likely use code like this:
PrintWriter pw = resp.getWriter();
pw.print("<!DOCTYPE html><html><head></head><body><h1>Threads</h1><ul>");
for (String thread : threads) {
pw.print("<li>" + thread + "</li>");
}
pw.print("</ul></body></html>");
This is not very fun or pretty and Spring agrees. Instead of writing html snippets into the controller class, most real web applications use html templates. A template is a html file with placeholders. We will use the Thymeleaf template engine to get rid of the html in our controllers.
This is how it works. First you create a html template for the page - an almost regular html file:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head></head>
<body>
<h1>Threads</h1>
<ul>
<block th:each="thread : ${threads}">
<li th:text="${thread}"></li>
</block>
</ul>
</body>
</html>
The weird attributes th:text
and th:each
are thymeleaf's template magic.
The <block>
element is not a standard html element - it's a special thymeleaf element that tells thymeleaf to repeat the contents of the <block>
for each element of some collection.
The collection itself is specified by the th:each
attribute.
The th:text
attribute tells thymeleaf to replace the entire contents of the element with the specified text.
Where does thymeleaf get the threads collection? You must provide it in the request handler:
@RequestMapping
public ModelAndView doGet(HttpServletRequest req, HttpServletResponse resp) {
List<String> threads = loadThreads();
return new ModelAndView("templateName").addObject("threads", threads);
}
The above code does some new things. Some notes:
- we never touch the response object - writing the response is left for thymeleaf. you can remove the response parameter entirely, or keep it if you need to add any headers or like.
- we return a
ModelAndView
object. this specifies the template's name ("templateName" in the example) and the values that should be available in the template. - thymeleaf will search for the template from the
/templates
directory in the classpath. in this example, it would use the filesrc/main/resources/templates/templateName.html
if it existed. - you can add any objects to the
ModelAndView
. you can calladdObject
multiple times. you can add strings, numbers and objects of your own classes to theModelAndView
(and use them in the template:${myObject.myField}
).
Thymeleaf syntax allows you to do almost everything. This also makes it quite complicated. See the thymeleaf docs for details.
This repository already contains some thymeleaf configuration:
- the pom.xml contains the thymeleaf dependency
- the application.properties contains
spring.thymeleaf.cache=false
, which will allow you to reload thymeleaf templates without restarting the application (use "Build project").
Update your application to use thymeleaf:
- create the templates directory
- move all html from the controllers to separate files in the templates directory
- change the controllers to return ModelAndView
- change your html to use thymeleaf's features.
see
th:href
for links and css,th:action
for forms,th:text
for text. use the@{}
syntax for URLs and${}
syntax for variables. - make sure the application is still working like before
Your application allows users to post arbitary text to the forum threads.
What if someone posts html to the forum?
Try to post the following text to your forum <h1>surprise!</h1>
.
Is it rendered as html or as-written?
See the page sources in the developer tools.
Thymeleaf should automatically replace all <
and >
symbols with their html-encoded variants: >
and <
.
This is super important from security point of view: if random people can add html to your page, then they can also add javascript code and bad things will happen.
If you really need to add html to the template, then use th:utext
, but be very careful!
What does Model and View mean? It's rather common to split web applications into three components:
- models are classes that represents your data (forum threads, user account, orders, bills, etc)
- views are the html templates that render some part of the model to the browser
- controllers are the classes that accept requests, change the model objects and choose the right view
The critical part here is that the model classes should know nothing about the controllers and views. Similarly, the views should know nothing about the controller logic, nor should they ever change a model object. Views only render the objects that are passed to them. Finally, controllers know about how to change the model objects, but not how to render them to the browser.
Such a split is called the MVC (Model-View-Controller) pattern and its purpose is to make the code more maintainable. Note that MVC is just one (and not always the best) option for organizing your application.