490 likes | 505 Views
Высшая школа ИТИС. Лекция 7 - Spring Web MVC Часть 2 23 октября 2013. Веб-разработка на Java. Алина Витальевна Васильева доцент факультета Компьютерных Наук Латвийский Университет инженер-разработчик, Одноклассники, Mail.ru Group alina.vasiljeva@gmail.com. Компоненты Spring MVC.
E N D
Высшая школа ИТИС Лекция 7 - Spring Web MVC Часть 2 23 октября 2013 Веб-разработка на Java Алина Витальевна Васильева доцент факультета Компьютерных Наук Латвийский Университет инженер-разработчик, Одноклассники, Mail.ru Group alina.vasiljeva@gmail.com
Компоненты Spring MVC Spring MVC состоит из нескольких компонентов, между которыми происходит взаимодействие в процессе обработки клиентского запроса Рассмотрим подробнее контроллеры
Demo Example View (single page) • a table displaying all existing users • a form for adding new users to a system Service Layer • storing data in-memory MVC Controller • the main topic, will experiment with it public interface UserService { public List<User> getUsers(); public void addUser(User user); }
User Entity public class User { private long id; private String firstName; private String lastName; public long getId() { return id; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } /* ... setters ... */ }
View: users.jsp (part-1: display) <c:choose> <c:when test="${not empty users}" > <h3>Users</h3> <table border="1"> <c:forEach var="user" items="${users}"> <tr> <td><c:out value="${user.id}"/></td> <td><c:out value="${user.firstName}"/></td> <td><c:out value="${user.lastName}"/></td> </tr> </c:forEach> </table> </c:when> <c:otherwise> <h3>No users in a system</h3> </c:otherwise> </c:choose>
View: users.jsp (part-2: create) <h3>Add a new user</h3> <form action="/addUser" method="POST"> First name: <input id="firstName" name="firstName"/><br/> Last name: <input id="lastName" name="lastName"/><br/> <input type="submit" value="Add"/> </form> Something like that
Service Layer: UserServiceImpl @Component public class UserServiceImpl implements UserService { private AtomicLong idGenerator = new AtomicLong(0L); private static final List<User> users = new ArrayList<User>(); public List<User> getUsers() { return Collections.unmodifiableList(users); } public void addUser(User user) { user.setId(idGenerator.getAndIncrement()); users.add(user); } }
Application Deployment <plugin> <groupId>org.mortbay.jetty</groupId> <artifactId>jetty-maven-plugin</artifactId> <version>8.1.13.v20130916</version> <configuration> <scanIntervalSeconds>10</scanIntervalSeconds> <webApp> <contextPath>/my-app</contextPath> </webApp> </configuration> </plugin> <servlet> <servlet-name>spring</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>spring</servlet-name> <url-pattern>/spring/*</url-pattern> </servlet-mapping> Jetty server configured in pom.xml to context path: /my-app Spring’s DispatcherServlet configured in web.xml to URL pattern: /spring/*
Application URLs • As a result, DispatcherServlet will handle HTTP requests to the following base URL: http://localhost:8080/my-app/spring • Task 1: create a controller, map it to /users http://localhost:8080/my-app/spring/users and forward a request for rendering to users.jsp
Making /users a main app page • Create a file \spring-app\src\main\webapp\index.jsp <% response.sendRedirect("/my-app/spring/users"); %> • As a result, requests to http://localhost:8080/my-app will be redirected to http://localhost:8080/my-app/spring/users
Current Status http://localhost:8080/my-app/spring/users
Implementing Controllers • Controllers interpret user input and transform it into a model that is rendered by the view • Spring introduces an annotation-based programming model for MVC controllers, using annotations such as • @Controller • @RequestMapping • @RequestParam • @ModelAttribute • etc
Simplest Controller @Controller public class HelloWorldController { @RequestMapping("/helloWorld") public String helloWorld(Model model) { model.addAttribute("message", "Hello World!"); return "helloWorld"; } } • In this case, the method accepts a Model and returns a view name as a String • However, various other method parameters and return values can be used
EnablingAnnotation Support To enable autodetection of annotated controllers, add component scanning to your configuration: <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-3.0.xsd"> <!-- Enable autodetection of annotated components --> <context:component-scan base-package="ru.kfu.itis" /> <!-- ... --> </beans>
Annotation-based controllers • @Controllerannotation acts as a stereotype for the annotated class, indicating its role • The dispatcher scans such annotated classes for mapped methods and detects @RequestMapping annotations • @RequestMapping annotation maps URLs such as /helloWorldonto a particular controller method
Mapping Requests • @RequestMapping annotation can map a request onto an entire class or a particular handler method • Typically the class-level annotation (not required) maps a specific request path onto a form controller • Additional method-level annotations narrow the primary mapping for a specific HTTP method request method ("GET", "POST", etc.)
First Attempt @Controller @RequestMapping("/users") public class UsersController { @Autowired private UserService userService; public ModelAndView displayUsers() { ModelAndView mav = new ModelAndView("users"); List<User> users = userService.getUsers(); mav.addObject("users", users); return mav; } }
Result [INFO] Configuring Jetty for project: spring-app ... [INFO] Context path = /my-app ... 2013-10-23 11:37:26.435:INFO:/my-app:Initializing Spring FrameworkServlet 'spring' ... INFO: Loading XML bean definitions from ServletContext resource [/WEB-INF/spring-servlet.xml] ... INFO: FrameworkServlet 'spring': initialization completed in 395 ms ... Okt 23, 2013 11:57:11 AM org.springframework.web.servlet.DispatcherServlet noHandlerFound WARNING: No mapping found for HTTP request with URI [/my-app/spring/users] in DispatcherServlet with name 'spring' Jetty console output And still ...
Second Attempt @Controller @RequestMapping("/users") public class UsersController { @Autowired private UserService userService; @RequestMapping public ModelAndView displayUsers() { ModelAndView mav = new ModelAndView("users"); List<User> users = userService.getUsers(); mav.addObject("users", users); return mav; } }
Result Okt 23, 2013 11:58:59 AM org.springframework.web.servlet.handler.AbstractUrlHandlerMapping registerHandler INFO: Mapped URL path [/users] onto handler 'usersController‘ Okt 23, 2013 11:58:59 AM org.springframework.web.servlet.handler.AbstractUrlHandlerMapping registerHandler INFO: Mapped URL path [/users.*] onto handler 'usersController‘ Okt 23, 2013 11:58:59 AM org.springframework.web.servlet.handler.AbstractUrlHandlerMapping registerHandler INFO: Mapped URL path [/users/] onto handler 'usersController' Jetty console output Success !!!
Conclusions • Controller annotated with @RequestMapping("/url") on a class-level only DOES NOT work • Adding @RequestMappingalso on a method-level WORKED successfully for HTTP GET request
Interesting Fact • Controller method configured in such a way handles also HTTP POST requests • You can test it by adding debug print in a method <form action="/my-app/spring/users" method="POST"> First name: <input id="firstName" name="firstName"/><br/> Last name: <input id="lastName" name="lastName"/><br/> <input type="submit" value="Add"/> </form> @RequestMapping public ModelAndView displayUsers() { System.out.println("Inside displayUsers()"); ... }
Separation of GET and POST @Controller @RequestMapping("/users") public class UsersController { @RequestMapping(method = RequestMethod.GET) public ModelAndView displayUsers() { ModelAndView mav = ... // business logic return mav; } @RequestMapping(method = RequestMethod.POST) public ModelAndView addUser() { ModelAndView mav = ... // business logic return mav; } } Obviously, we need two separate handler methods for GET (displaying a page) and POST (adding a new user)
Implementing POST • We need to extract form parameters from HTTP request in some way • It can be achieved by adding HttpServletRequestto method signature @RequestMapping(method = RequestMethod.POST) public ModelAndView addUser(HttpServletRequest request) { User user = new User(); user.setFirstName( request.getParameter("firstName")); user.setLastName( request.getParameter("lastName")); userService.addUser(user); return new ModelAndView("users"); }
Result • It works, but there are few issues: • Users table does not appear on the screen after form submit • Page Refresh (F5) leads to repeated POST request (the same user is created again) How to fix it ?
Redirect After Submit pattern • There is a common problem with the handling the display of the confirmation page • The success view is rendered in the same request as the initial POST, leaving the browser in a state with the ability to replay the form submit • The user can simply reload the page, resubmitting the form
Redirect After Submit pattern • Redirect After Submit pattern simply redirects the user to the success view instead of internally forwarding the request • A client redirect is not the same as a RequestDispatcher.forward() • Forwarding method internally redirect a request to another handler inside the servlet container • A client redirect instructs the client to issue another GET request
Spring’s Support for Redirect • Special class exists for this purpose org.springframework.web.servlet.view.RedirectView • Spring MVC provides a shorthand for redirect views • Or even shorter: @RequestMapping(method = RequestMethod.POST) public ModelAndView addUser(HttpServletRequest request) { // create a user return new ModelAndView("redirect:users"); } @RequestMapping(method = RequestMethod.POST) public String addUser(HttpServletRequest request) { // create a user return "redirect:users"; }
Success! • Users web-page is successfully implemented
Домашнее задание 3 Реализовать веб-приложение «Калькулятор», используя Spring MVC фреймворк. Фактически цель задания – прочувствовать разницу с подходом программирования с использованием Java Servlet (1 домашняя работа). Базовая страница должна содержать форму, которая включает в себя: • два поля, куда пользователь вводит два числа • четыре кнопки арифметических действий: + - * / Результат выполнения арифметической операции должен быть отображён на том же экране. Решение до 25 октября 17:00 = 5 пунктов Решение до 27 октября 17:00 = 4 пункта
Домашнее задание 4 • «Неофициальное» (без пунктов), но необходимое для выполнения следующего задания • Вычекать, настроить и запустить проект «News Feed» • Инструкции по настройке проекта: • Детали и проблемы обсуждаем в Google Group gitclone https://github.com/avasiljeva/lab02-newsfeed.git \lab02-newsfeed\README.md
More about controller methods • @RequestMappinghandler method can have a very flexible signatures • Arbitrary method name • Different types of arguments • Different options for return values • Most types of method arguments can be used in arbitrary order • BindingResult is the only exception
Method arguments and return types • Servlet API: Request andResponse objects (ServletRequestor HttpServletRequest) • Servlet API: Session object (HttpSession) • @RequestParam annotated parameters for access to specific Servlet request parameters • @PathVariableannotated parameters for access to URI template variables • Many more: WebRequest,NativeWebRequest, Locale, Principal, @RequestHeader, @RequestBody, @InitBinder . . .
@RequestParam @RequestMapping(method = RequestMethod.POST) public String addUser( @RequestParam String firstName, @RequestParam String lastName){ User user = new User(); user.setFirstName(firstName); user.setLastName(lastName); userService.addUser(user); return "redirect:users"; }
Data Binding: @ModelAttribute • @ModelAttributeindicates that the argument should be retrieved from the model • If not present, the argument will be instantiated and added to the model • Then, the argument's fields will be populated from all request parameters that have matching names @RequestMapping(method = RequestMethod.POST) public String addUser( @ModelAttribute User user){ userService.addUser(user); return "redirect:users"; }
Form Data Validation • A typical task for a web-application is to validate user data entered into a form • Spring MVC has its own solution for the data validation • Approach: • Implement the Validator interface • Invoke validator in a controller method • Implement custom Spring form on UI
Implementing a Validator @Component public class UserValidator implements Validator { public boolean supports(Class<?> clazz) { return clazz.equals(User.class); } public void validate(Object target, Errors errors) { User user = (User)target; if (!StringUtils.hasText(user.getFirstName())){ errors.rejectValue("firstName", "error.empty", "Empty first name!"); } if (!StringUtils.hasText(user.getLastName())){ errors.rejectValue("lastName", "error.empty", "Empty last name!"); } } }
Invoking a Validator @Controller@RequestMapping("/users") public class UsersController { @Autowiredprivate UserValidator validator; @RequestMapping(method = RequestMethod.POST) public String addUser(Model model, @ModelAttribute User user, BindingResult result){ validator.validate(user, result); if (result.hasErrors()) { // we'd like to display users anyway model.addAttribute("users", userService.getUsers()); return "users"; } userService.addUser(user); return "redirect:users"; } }
Re-implementing a JSP form <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> <form:form commandName="user" method="POST" action="/my-app/spring/users"> First name: <form:input path="firstName" /> <form:errors path="firstName" cssStyle="color: red;" /><br/> Last name: <form:input path="lastName" /> <form:errors path="lastName" cssStyle="color: red;" /><br/> <input type="submit" value="Add"/> </form:form> Special Spring tag library
Instantiating a Command Object @RequestMapping(method = RequestMethod.GET) public ModelAndView displayUsers() { ModelAndView mav = new ModelAndView("users"); List<User> users = userService.getUsers(); mav.addObject("users", users); // empty command object for a form mav.addObject("user", new User()); return mav; } • JSP form now is associated with a command object named "user" • Need to explicitly instantiate it in a controller
URI Template Patterns • URI templates can be used for convenient access to selected parts of a URL in a@RequestMappingmethod • URI Template: http://www.example.com/users/{userId} contains the variable: userId @RequestMapping(value="/owners/{ownerId}", method=RequestMethod.GET) public String findOwner(@PathVariable String ownerId, Model model) { Owner owner = ownerService.findOwner(ownerId); model.addAttribute("owner", owner); return "displayOwner"; }
Template Patterns & Relative URLs @RequestMapping(value="/owners/{ownerId}/pets/{petId}", method=RequestMethod.GET) public String findPet(Model model, @PathVariable String ownerId, @PathVariable String petId) { Owner owner = ownerService.findOwner(ownerId); Pet pet = owner.getPet(petId); model.addAttribute("pet", pet); return "displayPet"; } Equivalent ways for implementing a handler for: /owners/42/pets/21 @Controller @RequestMapping("/owners/{ownerId}") public class RelativePathUriTemplateController { @RequestMapping("/pets/{petId}") public void findPet(Model model @PathVariable String ownerId, @PathVariable String petId) { // implementation omitted } }
Customizing data binding It is possible to use@InitBinder to configure a CustomDateEditor for all java.util.Date form properties @Controller public class MyFormController { @InitBinder public void initBinder(WebDataBinder binder) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); dateFormat.setLenient(false); binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false)); } }
Interceptors • Useful when you want to apply specific functionality before/after certain requests • Contains twointerceptionmethod: preHandleunpostHandle • Contains onecallbackmethod: afterCompletion • Can be assigned to a set of controllers via configuration ofHandlerMapping
Interceptor configuration <beans> <bean id="handlerMapping" class="org.springframework.web.servlet.mvc.method .annotation.RequestMappingHandlerMapping"> <property name="interceptors"> <list> <ref bean="officeHoursInterceptor"/> </list> </property> </bean> <bean id="officeHoursInterceptor" class="samples.TimeBasedAccessInterceptor"> <property name="openingTime" value="9"/> <property name="closingTime" value="18"/> </bean> <beans> Assign the interceptor to all requests
Interceptor code public class TimeBasedAccessInterceptor extends HandlerInterceptorAdapter { . . . public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Calendar cal = Calendar.getInstance(); int hour = cal.get(HOUR_OF_DAY); if (openingTime <= hour < closingTime) { return true; } else { response.sendRedirect( "http://host.com/outsideOfficeHours.html"); return false; } } }
Resources Web MVC Framework documentation http://docs.spring.io/spring/docs/3.2.4.RELEASE/spring-framework-reference/html/mvc.html Tutorial "Designing and Implementing a Web Application with Spring" http://spring.io/guides/tutorials/web/ Spring MVC: Validator and @InitBinder http://fruzenshtein.com/spring-mvc-validator-initbinder/