整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

Spring MVC之LocaleResolver详

Spring MVC之LocaleResolver详解

对于LocaleResolver,其主要作用在于根据不同的用户区域展示不同的视图,而用户的区域也称为Locale,该信息是可以由前端直接获取的。通过这种方式,可以实现一种国际化的目的,比如针对美国用户可以提供一个视图,而针对中国用户则可以提供另一个视图。本文主要讲解如果使用LocaleResolver来实现对用户不同视图切换的目的。

LocaleResolver是Spring提供的一个接口,其声明如下:

public interface LocaleResolver {
 // 根据request对象根据指定的方式获取一个Locale,如果没有获取到,则使用用户指定的默认的Locale
	Locale resolveLocale(HttpServletRequest request);
 
 // 用于实现Locale的切换。比如SessionLocaleResolver获取Locale的方式是从session中读取,但如果
 // 用户想要切换其展示的样式(由英文切换为中文),那么这里的setLocale()方法就提供了这样一种可能
	void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, 
 @Nullable Locale locale);
}

针对LocaleResolver,Spring提供了几种实现方式,分别如下:

  • FixedLocaleResolver:在声明该resolver时,需要指定一个默认的Locale,在进行Locale获取时,始终返回该Locale,并且调用其setLocale()方法也无法改变其Locale;
  • CookieLocaleResolver:其读取Locale的方式是在session中通过Cookie来获取其指定的Locale的,如果修改了Cookie的值,页面视图也会同步切换;
  • SessionLocaleResolver:其会将Locale信息存储在session中,如果用户想要修改Locale信息,可以通过修改session中对应属性的值即可;
  • AcceptHeaderLocaleResolver:其会通过用户请求中名称为Accept-Language的header来获取Locale信息,如果想要修改展示的视图,只需要修改该header信息即可。

需要说明的是,Spring虽然提供的几个不同的获取Locale的方式,但这些方式处理FixedLocaleResolver以外,其他几个也都支持在浏览器地址栏中添加locale参数来切换Locale。对于Locale的切换,Spring是通过拦截器来实现的,其提供了一个LocaleChangeInterceptor,在该拦截器中的preHandle()方法中,Spring会读取浏览器参数中的locale参数,然后调用LocaleResolver.setLocale()方法来实现对Locale的切换。

这里我们以CookieLocaleResolver为例来讲解如何通过不同的Locale展示不同的视图。首先是我们的xml文件配置:

<context:component-scan base-package="mvc"/>
 <mvc:annotation-driven/>
 <mvc:interceptors>
 <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
 <bean class="mvc.interceptor.MyHandlerInterceptor"/>
 </mvc:interceptors>
 <bean class="org.springframework.web.servlet.view.ResourceBundleViewResolver"/>
 <bean id="localeResolver" 
 class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
 <property name="defaultLocale" value="zh_CN"/>
 </bean>

关于上述配置有三点需要说明:

  • 指定了使用的LocaleResolver为CookieLocaleResolver,并且defaultLocale指定为zh_CN,需要注意的是,Spring中LocaleResolver的bean名称必须为localeResolver,因为Spring读取该bean时是通过该名称读取的;
  • 上述配置总还指定了ViewResolver为ResourceBundleViewResolver,这里不能使用InternalResourceViewResolver,因为其不支持通过不同的Locale进行不同的视图切换,而ResourceBundleViewResolver是支持的;
  • 配置中添加了LocaleChangeInterceptor的拦截器,用于对Locale的切换,如果不需要Locale切换的功能,可以不指定该拦截器。

对于后台接口的声明,其与一般的接口声明是没有区别的。如下是我们声明的一个接口:

@Controller
@RequestMapping("/user")
public class UserController {
 @Autowired
 private UserService userService;
 @RequestMapping(value="/detail", method=RequestMethod.GET)
 public ModelAndView detail(@RequestParam("id") long id, 
 @ModelAttribute("message") String message, Locale locale) {
 System.out.println(message);
 ModelAndView view=new ModelAndView("user");
 User user=userService.detail(id);
 view.addObject("user", user);
 view.addObject("locale", locale);
 return view;
 }
}

上述接口返回的视图为user,并且将当前用户的Locale信息返回给了前端,可以看到,这里获取Locale数据的方式就只需要简单的声明一个类型为Locale的参数即可。

关于视图的展示,由于我们需要根据不同的Locale展示不同的视图,而在上述接口中,我们暂时没发现这样的路由。实际上,这个路由是通过ResourceBundleViewResolver类实现的,在使用该ViewResovler时,其会到class路径下查找名称为views的Resource Bundle,并且通过用户指定的Locale,唯一定位到某个Resource Bundle。然后在该Resource Bundle中查找指定的视图信息,比如这里接口返回的视图为user,那么就会在获取到的Resource Bundle查找user.(class)和user.url信息,这里user.(class)指定了展示该视图所需要的View对象的,而user.url则指定了具体的视图位置。比如如下配置的就是Locale分别为zh_CN和en_US的视图:

# views_zh_CN.properties
user.(class)=org.springframework.web.servlet.view.InternalResourceView
user.url=/WEB-INF/view/user_zh_CN.jsp
# views_en_US.properties
user.(class)=org.springframework.web.servlet.view.InternalResourceView
user.url=/WEB-INF/view/user_en_US.jsp

通过这种方式,ResourceBundleViewResolver就实现了针对不同的Locale来展示不同的视图的目的。如下是我们编写的两个分别用于zh_CN和en_US视图展示的jsp页面:

<!-- user_zh_CN.jsp -->
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
 <title>User Jsp-zh CN</title>
</head>
<body>
${user.id} ${user.name} ${user.age} ${locale}
</body>
</html>
<!-- user_en_US.jsp -->
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
 <title>User Jsp-en US</title>
</head>
<body>
${user.id} ${user.name} ${user.age} ${locale}
</body>
</html>

启动上述程序,我们在浏览器中键入http://localhost:8080/user/detail?id=1,可以看到其展示了如下视图:

1 Bob 27 zh_CN

如果我们添加名称为org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE,值为en_US的cookie,那么其展示的页面切换如下:

1 Bob 27 en_US

这说明我们成功使用Cookie对Locale进行了切换。如果我们在浏览器地址栏中添加locale=zh_CN的参数,可以看到,页面展示又切换为了前面那种情况。

本文主要对LocaleResolver进行了讲解,并且演示了如何通过配置不同的LocaleResolver来达到实现展示不同的视图的目的。需要注意的是,我们的LocaleResolver的bean名称必须为localeResolver,并且需要指定的ViewResolver辅以支持,否则切换的视图可能无法正常工作。

度分析

虽然但是听到这个消息的时候,内心还是挺震惊的,毕竟是一个完整的管理系统,功能界面还不能太过简陋。而且从数据库设计到整个系统的交付全由自己一人完成,挑战效果直接拉满!但是冷静下来思考一下,其实也并不是很难,整体的项目流程即为:设计——>文档——>编码——>交付。整体的流程划清之后,就开始一步步从无到有的实现,没想到到最后一步的时候,我竟然才用一天半的时间!!后面又用了半天的时间对整体的项目做了一个优化处理!

项目回顾

最终效果演示:

技术选型:

  • SpringBoot
  • Thymeleaf
  • Mybatis-Plus
  • MySQL
  • PageHelper
  • Lombok
  • Redis(后期页面优化使用)

项目业务流程简介

登录模块、用户模块管理以及对用户的角色分配,新闻公告模块的管理、商品模块(包括对商品、商品分类、订单)的管理、角色模块的管理;对于前端某资源是否有权限操作该资源,使用的是thymeleaf模板语法进行判断鉴别以及文件上传等基本功能。

项目搭建(使用模板引擎)

1. 首先创建Maven项目

引入相应的依赖,构建所需文件目录

2. 编写yaml配置文件

server:
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/supplier?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
    username: root
    password: root
  # thymeleaf 配置
  thymeleaf:
    # 关闭缓存
    cache: false
    prefix: classpath:/templates/

mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml

3. 项目初期基本搭建

在搭建一个项目的初期,为了让系统显得更规范化,我一般会提前做好基础的配置和声明,一个项目从开始设想时所涉及到技术以及这些技术对应的一些基础配置,都要提前规划清楚(个人习惯)。比如:异常处理、拦截器、过滤器、常量类等等。

①异常处理

@ControllerAdvice
public class ExceptionHandler {

    private final org.slf4j.Logger logger=LoggerFactory.getLogger(this.getClass());

    @org.springframework.web.bind.annotation.ExceptionHandler(Exception.class)
    public ModelAndView exception(HttpServletRequest request, Exception e ) throws Exception {
        logger.error("Request URL:{},Exception:{}",request.getRequestURL(),e);

        if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class )!=null){
            throw e;
        }

        ModelAndView mv=new ModelAndView();
        mv.addObject("url",request.getRequestURL());
        mv.addObject("exception",e);
        mv.setViewName("error/error");

        return mv;
    }
}

② 拦截器

拦截器主要是对一些资源做的处理,类似于某些资源需要用户登录后才能访问的,某些是不需要的,比如:登录功能就不需要有所拦截,而对用户的各种管理就需要添加拦截操作,这样才能使系统的安全性有所提高。

登录拦截

public class LoginInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (request.getSession().getAttribute("user")==null){
            response.sendRedirect("/api");
            return false;
        }
        return true;
    }
}

资源放行

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api","/api/doLogin");
    }
}

4. 编写Controller前端控制器代码

首先创建一个FileController类

① 跳转文件上传的页面

//跳转文件上传的页面
@RequestMapping("/file-upload")
public String userList(){
  return "file-upload";
}

② 实现文件上传的功能

@RequestMapping("/doAddForUser")
public String doAdd(User user, @RequestParam("file") MultipartFile files, HttpServletRequest request) throws IOException {
  //String path=null;
  if (files !=null && !files.isEmpty()){
    String name=UUID.randomUUID().toString().replace("-","");
    //获取文件的扩展名
    String ext=FilenameUtils.getExtension(files.getOriginalFilename());
    //设置文件上传的路径
    String url=request.getSession().getServletContext().getRealPath("/upload/");

    File file=new File(url);
    if (!file.exists()){
      file.mkdir();
    }
    //测试路径
    System.out.println(request.getServletPath()+ "/upload");
    System.out.println(request.getContextPath() + "/upload/");
    //以绝对路径保存重命名后的文件
    files.transferTo(new File(url+"/"+name+"."+ext));
    user.setAvatar(request.getContextPath() + "/upload/"+name+"."+ext);
  }

  user.setId(UUID.randomUUID().toString());
  String salt=PasswordUtils.getSalt();
  String password=user.getPassword();
  String encode=PasswordUtils.encode(password, salt);
  user.setSalt(salt) ;
  user.setPassword(encode);
  user.setCreateTime(new Date());
  userService.save(user);
  return "redirect:/api/users";
}

注:如何想要实现多文件上传需要更改的地方如下:

③ 实现多文件上传功能

在这个项目中并未实现多文件上传功能

private void commons(Object obj, @RequestParam("file") CommonsMultipartFile[] files, HttpServletRequest request) throws IOException {
    //String path=null;
    for (int i=0; i < files.length; i++) {

        if (files[i] !=null && !files[i].isEmpty()){
            String name=UUID.randomUUID().toString().replace("-","");
            //获取文件的扩展名
            String ext=FilenameUtils.getExtension(files[i].getOriginalFilename());
            //设置文件上传的路径
            String url=request.getSession().getServletContext().getRealPath("/upload/");

            File file=new File(url);
            if (!file.exists()){
                file.mkdir();
            }
            //测试路径
            System.out.println(request.getServletPath()+ "/upload");
            System.out.println(request.getContextPath() + "/upload/");
            //以绝对路径保存重命名后的文件
            files[i].transferTo(new File(url+"/"+name+"."+ext));

            if (i==0){
                obj.setUrl1(request.getContextPath() + "/upload/"+name+"."+ext);
            }
            if (i==1){
                obj.setUrl2(request.getContextPath() + "/upload/"+name+"."+ext);
            }
            if (i==2){
                obj.setUrl3(request.getContextPath() + "/upload/"+name+"."+ext);
            }
            if (i==3){
                obj.setUrl4(request.getContextPath() + "/upload/"+name+"."+ext);
            }
            if (i==4){
                obj.setUrl5(request.getContextPath() + "/upload/"+name+"."+ext);
            }
        }
    }
}

5. 项目优化

对于前后端不分离的项目,多数使用的是页面缓存优化,当系统某一瞬间遭受巨大流量时,当第一个用户进行页面访问时可以将该页面数据进行缓存,这样,后来的用户访问到的页面都是从缓存中获取的,这样就减少了 对数据库的操作,减轻了数据库的压力,从而达到优化的处理。

① 导入依赖

<!--Redis-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--commons-pools2 对象池依赖-->
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-pool2</artifactId>
</dependency>

② yaml配置

## Redis配置
  redis:
    # 服务器地址
    host: localhost
    # 端口
    port: 6379
    # 数据库
    database: 0
    # 超时时间
    connect-timeout: 10000ms
    lettuce:
      pool:
        # 最大连接数
        max-active: 8
        # 最大连接阻塞等待时间 默认 -1
        max-wait: 10000ms
        # 最大空闲时间 默认8
        max-idle: 200
        # 最小空闲连接 默认8
        min-idle: 5

④ Redis序列化处理

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String,Object>  redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String,Object> redisTemplate=new RedisTemplate<>();
        //key序列化
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //value序列化
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        //hash类型key的序列化
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        //hash类型value的序列化
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}

③ 优化处理

    @Autowired
    private NewsService newsService;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private ThymeleafViewResolver viewResolver;

    @RequestMapping(value="/news",produces="text/html;charset=utf-8")
    @ResponseBody
    public String roles(Model model, @RequestParam(value="pageNo",defaultValue="1")Integer pageNo
            , @RequestParam(value="pageSize",defaultValue="10")Integer pageSize
            , HttpServletRequest request, HttpServletResponse response){
    //Redis中获取页面,如果不为空,则直接返回页面
        ValueOperations valueOperations=redisTemplate.opsForValue();
        String html=(String) valueOperations.get("news-list");
        if (!StringUtils.isEmpty(html)){
            return html;
        }
        PageHelper.startPage(pageNo,pageSize);
        List<News> list=newsService.list();
        PageInfo<News> pageInfo=new PageInfo<>(list);
        model.addAttribute("news",list);
        model.addAttribute("pageInfo",pageInfo);
       //如果为空,手动渲染,存入Redis中并返回
        WebContext context=new WebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap());
        html=viewResolver.getTemplateEngine().process("news-list", context);
        if (!StringUtils.isEmpty(html)){
           //给缓存设置过期时间
            valueOperations.set("news-list",html,60, TimeUnit.SECONDS);
        }
        return html;
    }

④ Redis查看

6. 注意事项

注意@Controller和@RestController的区别,本项目使用的是模板渲染页面,而@Controller就是用来响应页面的;而@RestController是用来返回Json

在项目优化阶段需要在方法上添加注解@ResponseBody,因为我们是将整个页面进行缓存 ,所以要将页面转换成JSON进行存储。

注入Thymeleaf解析器,将具体的 页面进行解析成Json字符串进行存储

将存入Redis中的数据加上过期时间,因为页面中的数据要和数据库保持一致,如果用户看到是几十秒之前或一分钟之前的数据还是勉强可以接受的。

目前代码已经同步到Gitee:

https://gitee.com/gao-wumao/supplier


来源:blog.csdn.net/Gaowumao?type=blog

图解析的实现基础

视图解析器(ViewResolver)是 Spring MVC 的重要组成部分,负责将逻辑视图名解析为具体的视图对象。
Spring MVC 提供了很多视图解析类,其中每一项都对应着 Java Web 应用中特定的某些视图技术。

viewResolver组件会将viewName解析成view对象,view对象会调用render完成结果的处理。

  • ViewResolver 与 View 接?
    • AbstractCachingViewResolver
      • UrlBasedViewResolver
        • FreeMarkerViewResolver
        • InternalResourceViewResolver
      • ContentNegotiatingViewResolver 根据请求文件名或Accept头解析视图。

一些可参阅的博客:

http://c.biancheng.net/spring_mvc/view-resolver.html

https://blog.csdn.net/fengyuhan123/article/details/79723310

DispatcherServlet 中的视图解析逻辑

  • initStrategies()
    • initViewResolvers() 初始化了对应 ViewResolver
  • doDispatch()
    • processDispatchResult()
    • 没有返回视图的话,尝试 RequestToViewNameTranslator
    • resolveViewName() 解析 View 对象

一些可参阅的博客:

https://blog.csdn.net/u013541707/article/details/108511017

使? @ResponseBody 的情况

  • 在 HandlerAdapter.handle() 的中完成了 Response 输出
    • RequestMappingHandlerAdapter.invokeHandlerMethod()
      • HandlerMethodReturnValueHandlerComposite.handleReturnValue()
        • RequestResponseBodyMethodProcessor.handleReturnValue()

转发与重定向

  • redirect: 重定向
  • forward: 转发

Spring MVC 中的常?视图

Spring MVC ?持的视图

官方文档:https://docs.spring.io/spring-framework/docs/5.1.5.RELEASE/spring-framework-reference/web.html#mvc-view

?持的视图列表:

  • Jackson-based JSON / XML
  • Thymeleaf & FreeMarker

配置 HttpMessageConverter

在之前的RequestMapping的Handler也有提到过Converter,在视图的部分也有自己对应的HttpMessageConverter

  • 通过 WebMvcConfigurer 的 configureMessageConverters()
  • Spring Boot ?动查找 HttpMessageConverter 进?注册

Spring Boot 对 Jackson 的?持

  • JacksonAutoConfiguration
    • Spring Boot 通过 @JsonComponent 注册 JSON 序列化组件
    • Jackson2ObjectMapperBuilderCustomizer
  • JacksonHttpMessageConvertersConfiguration
    • 增加 jackson-dataformat-xml 以?持 XML 序列化

Thymeleaf

“Thymeleaf is a modern server-side Java template engine for both web and standalone environments.” – https://www.thymeleaf.org/

  • 添加 Thymeleaf 依赖
    • org.springframework.boot:spring-boot-starter-thymeleaf
  • Spring Boot 的?动配置
    • ThymeleafAutoConfiguration
    • ThymeleafViewResolver
  • Thymeleaf 的?些默认配置
    • spring.thymeleaf.cache=true
    • spring.thymeleaf.check-template=true
    • spring.thymeleaf.check-template-location=true
    • spring.thymeleaf.enabled=true
    • spring.thymeleaf.encoding=UTF-8
    • spring.thymeleaf.mode=HTML
    • spring.thymeleaf.servlet.content-type=text/html
    • spring.thymeleaf.prefix=classpath:/templates/
    • spring.thymeleaf.suffix=.html