2019 https://www.linkedin.com/learning/spring-spring-mvc-2 Incoming HTTP Request is Processed by DispatcherServlet->HandlerMapping->Controller (Front Controller DP) ->Controller->...processing...->back-to-DispatcherServlet ->ViewResolver Instead of String method at Controller we can use ModelAndView .setViewName .setStatus .addObject(name, value) SearchController { @GetMapping("/search") public String search(@RequestParam("search") String search, Model model) { ... List products = searchRepository.searchByName(search); model.addAttribute("products", products); ... } } RegistrationController { @PostMapping("/registeruser") public String registerUser(@ModelAttribute("newuser") User user) { //
} } HomeController { ... @ModelAttribute("newuser") public User getDefaultUser() { return new User(); } @ModelAttribute("genderItems") public List getGenderItems() { return List.of("Male", "Female", "Other"); } } javax.validation -> hibernate validator annotate with @Valid our beans at controller and add BindingResult param right after the annotated one and add to our form elements in order to localize error messages, add resources/ValidationMessages.properties, add msg.key=msg.value and use ... message="{msg.key}" binders extend ModelAttribute-s User { private Date dateOfBirth; // } RegistrationController { @InitBinder public void initBinder(WebDataBinder binder) { binder.registerCustomEditor(Date.class, "dateOfBirth", new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"))); } } converters extend binders concept, transform request data to desired types built in converters present, but we can provide custom ones public enum Gender { MALE, FEMALE, OTHER } User { @Enumerated(EnumType.STRING) private Gender gender; } public class StringToEnumConverter implements .core.convert.Converter { @Override public Gender convert(String s) { ... regurn Gender.GGG ...; } } ApplicationConfig { ... @Override protected void addFormatters(FormatterRegistry registry) { registry.addConverter(new StringToEnumConverter()); } } HandlerMapping - maps request to handler HandlerAdapter - invokes the handler ViewResolver - helps resolve views LocaleResolver - helps with l10n and i18n support ThemeResolver HandlerExceptionResolver - helps with exception handling ExceptionHandlerExceptionResolver - define ex-handling methods in controllers (annotated by @ExceptionHandler) SimpleMappingExceptionResolver - maps each ex-n class with an error page DefaultHandlerExceptionResolver - default, maps exceptions to error codes ResponseStatusExceptionResolver - resolves custom exceptions using status code defined in @ResponseStatus (for ex-ns) to disable standard error resolver, we need to add property spring.autoconfigure.exclude=ord.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration public class Login { private String login; private String password; } HomeController { ... @ModelAttribute("login") public Login getDefaultLogin() { return new Login(); } } public class ApplicationException extends RuntimeException { ... } LoginController { @PostMapping("/login") public String login(@ModelAttribute("login") Login login) { User user = userRepository.searchByName(login.getUsername()); if (user == null) { throw new ApplicationException("User not found"); } } @ExceptionHandler(ApplicationException.class) public String handleException(ApplicationException ex, WebRequest request) { return "error"; } } for global handler @ControllerAdvice public class ApplicationExceptionHandler { @ExceptionHandler(ApplicationException.class) public String handleException(ApplicationException ex, WebRequest request) { return "error"; } } ... @ControllerAdvice public class DefaultModelAttributeController { @ModelAttribute("newuser") public User getDefaultUser() { return new User(); } @ModelAttribute("genderItems") public List getGenderItems() { return List.of("Male", "Female", "Other"); } ... } @EnableAsync https://stackoverflow.com/questions/36768234/spring-enableasync-breaks-bean-initialization-order https://www.baeldung.com/spring-async ...ApplicationAsync { @Override protected void configureAsyncSupport(AsyncSupportConfigurer configurer) configurer.setTaskExecutor(mvcTaskExecutor()); configurer.setDefaultTimeout(5000); } @Bean public AsyncTaskExecutor mvcTaskExecutor() { ThreadPoolTaskExecutor... } } ... SearchController { public Callable search(..., HttpServletRequest request) { log.info(request.isAsyncSupported(), Thread.currentThread().getName()); return () -> { log.info(Thread.currentThread().getName()); all the business logic } we can also use DeferredResult @Autowired private AsyncTaskExecutor executor; public DeferredResult search(..., HttpServletRequest request) { DeferredResult defferedResult = new DeferredResult<>(); ... executor.execute(() -> { ... defferedResult.setResult(search); }); return defferedResult; } } View ViewResolver - resp by resolving views by names InternalResourceViewResolver ResourceBundleViewResolver XmlViewResolver VelocityViewResolver, FreeMakerViewResolver, ... ChainedViewResolver DispatcherServlet HandlerMapping, HandlerAdapter (calls interceptors) Pre-process Interceptors 1..n and then (iff all are OK) - calls a Controller after all business-logic - Post-process Impl-s of HandlerInterceptorAdapter base class preHandle postHandle afterCompletion - after response is completed to the client ThemeChangeInterceptor LocaleChangeInterceptor public class LoggingInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ... } public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { ... } } at ApplicationConfig { @Override protected void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoggingInterceptor()).addPathPatterns("/*") } } ... @SessionAttributes, @SessionAttribute @RequestAttribute @Controller @SessionAttributes("login") // @ModelAttribute should exist public class LoginController { // already exposed by ControllerAdvice // @ModelAttribute("login") public Login getDefaultLogin() { return new Login(); } } @Controller public class UserProfileController { @PostMapping("/userprofile") public String getUserProfile(@SessionAttribute("login") Login login, Model model) { // and similar for @RequestAttribute model.setAttribute("username"); // or use ${sessionScope.username} at jsp return "profile"; // use login in jsp } } LoginController { ... @PostMapping("/login") { session.setMaxInactiveTimeout(...) ... ... return "forward:/userprofile" } } @Controller public class LogoutController { @GetMapping("/logout") public String logout(HttpSession session) { session.invalidate(); return "login"; } } @Controller public class RedirectionController { @GetMapping("/redirectToLinkedIn") public String redirectToLinkedIn() { return "redirect:http://www.linkedin.com"; // absolute URL is needed } } ThemeResolver facilitates theme resolution ThemeSource and default impl - ResourceBundleThemeSource CookieThemeResolver SessionThemeResolver FixedThemeResolver resources/client-theme1.properties styleSheet=/css/style1.css resources/client-theme2.properties styleSheet=/css/style2.css ... ApplicationConfig { @Bean public ThemeResolver themeResolver() { CookieThemeResolver cookieThemeResolver = new CookieThemeResolver(); cookieThemeResolver.setCookieName("theme"); cookieThemeResolver.setDefaultThemeName("client-theme1"); return cookieThemeResolver; } ... // also it's important to add an interceptor @Override protected void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoggingInterceptor()).addPathPatterns("/*") registry.addInterceptor(new ThemeChangeInterceptor()); } } // next thing - define at our jsp <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> ... ... // we can check with ...?theme=client-theme2... ... resolver will store this in cookie l10n - is to use props for msgs i10n - is more than l10n, including locale treatment LocaleResolver AcceptHeaderLocaleResolver - default one CookieLocaleResolver SessionLocaleResolver resources/messages.properties label.home=home label.search=search label.login=login label.linkedin=linkedin resources/messages_sv.properties // make sure to have UTF-8 label.home=hem label.search=sok label.login=logga in label.linkedin=... // at jsp - use // default resolver takes locale from client settings of browser ... ApplicationConfig { @Bean public LocaleResolver localeResolver() { CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver(); cookieLocaleResolver.setCookieName("locale"); cookieLocaleResolver.setDefaultLocale(Locale.US); return cookieLocaleResolver; } ... // also it's important to add an interceptor @Override protected void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoggingInterceptor()).addPathPatterns("/*") registry.addInterceptor(new ThemeChangeInterceptor()); registry.addInterceptor(new LocaleChangeInterceptor()); // can customize with setParamName if need non-default } } REST: spring uses HttpMessageConvertors for JSON (with Jackson), no view name needed @Controller public class ProductsRestController { ... @GetMapping("/rest/products") @ResponseBody public List getProducts() { return productRepository.findAll(); } } @RestController // implies @ResponseBody for every method public class ProductsRestController { ... @GetMapping("/rest/products") public ResponseEntity> getProductsByParam(@RequestParam("name") String name) { List products = productRepository.searchByName(name) return new ResponseEntity<>(products, HttpStatus.OK); } @GetMapping("/rest/products/{id}") public ResponseEntity> getProductsByPathVar(@PathVariable("name") String name) { List products = productRepository.searchByName(name) return new ResponseEntity<>(products, HttpStatus.OK); } } public class LoginFailureException extends Exception { ... } @RestController public class LoginRestController { ... @PostMapping("/rest/loginuser") public ResponseEntity loginUser(@RequestBody Login login) { User user = userRepository.searchByName(login.getUsername()); if (user == null) { return new ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } if (user.getUsername().equals(login.getUsername)) && ... pwd) { return new ResponseEntity<>("Welcome " + user.getUsername(), HttpStatus.OK); } else { throw new LoginFailureException("username or password is incorrect") } } @ExceptionHandler(LoginFailureException.class) public ResponseEntity handle Login(LoginFailureException ex) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ex.getMessage()); } }