zoukankan      html  css  js  c++  java
  • SpringBoot:扩展SpringMVC、定制首页、国际化

    SpringBoot扩展使用SpringMVC、使用模板引擎定制首页及静态资源绑定、页面国际化

    扩展使用SpringMVC

    如何扩展SpringMVC

    How to do!

    ​ 如果你希望保留SpringBoot 中MVC的功能,并希望添加其他的配置(拦截器、格式化、视图控制器和其他功能),只需要添加自己的@Configuration配置类,并让该类实现 WebMvcConfigurer接口,但是不要在该类上添加 @EnableWebMvc注解,一旦添加了,该配置类就会全面接管SpringMVC中配置,不会再帮我们自动装配了!WebMvcAutoConfiguration这个自动装配类也就失效了!

    Action!

    新建一个包叫config,写一个类MyMvcConfig

    /**
     * 该类类型应为:webMvcConfigurer,所以我们实现其接口
     * 通过覆盖重写其中的方法实现扩展MVC的功能
     */
    @Configuration
    public class MyMvcConfig implements WebMvcConfigurer {
      
      /**
      * 添加视图控制器
    	*/
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            // 浏览器访问:localhost:8080/index.html或者localhost:8080/,都跳转到 classpath:/templates/index.html
            registry.addViewController("/").setViewName("index");
            registry.addViewController("/index.html").setViewName("index");
            // 浏览器访问:localhost:8080/main.html 跳转到 classpath:/templates/dashborad.html
            registry.addViewController("/main.html").setViewName("dashboard");
        }
    
        @Bean
        public LocaleResolver localeResolver() {
            return new MyLocaleResolver();
        }
    
    }
    

    为何这么做会生效(原理)

    1. WebMvcAutoConfiguration是SpringMVC的自动配置类,里面有一个类WebMvcAutoConfigurationAdapter
    2. 这个类上有一个注解,在做其他自动配置时会导入:@Import(EnableWebMvcConfiguration.class)
    3. 我们点击 EnableWebMvcConfiguration这个类看一下,它继承了一个父类:DelegatingWebMvcConfiguration,这个父类中有这样一段代码:
    /**
     * DelegatingWebMvcConfiguration 是 WebMvcConfigurationSupport 的子类,
     * 可以检测和委托给所有类型为:WebMvcConfigurer 的bean,
     * 允许它们自定义 WebMvcConfigurationSupport 提供的配置
     * 它是由 注解@EnableWebMvc 实际导入的类
     * @since 3.1
     */
    
    @Configuration
    // 委派webMvc配置类
    public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    	// webMvc配置综合类
    	private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
    
    	// 会从容器中获取所有的 webMvcConfigurer,自动装配
    	@Autowired(required = false)
    	public void setConfigurers(List<WebMvcConfigurer> configurers) {
    		if (!CollectionUtils.isEmpty(configurers)) {
          // 调用了WebMvcConfigurerComposite的addWebMvcConfigurers方法
    			this.configurers.addWebMvcConfigurers方法(configurers);
    		}
    	}
    }
    

    我们可以在 WebMvcConfigurerComposite 里Debug一下,看看是否会自动装配。

    1582356453561.png

    1582356384592.png

    1. 我们可以在DelegatingWebMvcConfiguration类中去寻找一个我们刚才设置的 addViewControllers() 当做参考,发现它调用了WebMvcConfigurerCompositeaddViewControllers()方法
    @Override
    protected void addViewControllers(ViewControllerRegistry registry) {
       this.configurers.addViewControllers(registry);
    }
    

    点进去:addViewControllers()

    public void addViewControllers(ViewControllerRegistry registry) {
      /*
      for循环,将所有的WebMvcConfigurer相关配置来一起调用!包括我们自己配置的和Spring给我们配置的
      */
       for (WebMvcConfigurer delegate : this.delegates) {
          delegate.addViewControllers(registry);
       }
    }
    

    5. 所以得出结论:所有的 WebMvcConfigurer 都会起作用,不止Spring的配置类,我们自己的配置类也会被调用。

    小结:

    • SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置的(如果用户自己配置@bean),如果有就用用户配置的,如果没有就用自动配置的;

    • 如果有些组件可以存在多个,比如我们的视图解析器,就将用户配置的和自己默认的组合起来


    全面接管SpringMVC

    当然,我们在实际开发中,不推荐使用全面接管SpringMVC

    但我们要明白这一点:为什么一旦添加了@EnableWebMvc注解,我们就会全面接管SpringMVC,它不会帮我自动装配了呢?

    先演示一下效果:

    首先创建一个配置类,添加@Configuration注解、实现WebMmvConfigurer接口,先不添加 @EnableWebMvc注解

    @Configuration
    public class MyMvcConfig implements WebMvcConfigurer {
    }
    

    访问在public目录下的 index.html

    1582343941443.png

    然后再添加@EnableWebMvc

    
    // 标记这个注解的类是一个配置类,本质也是一个 Component:组件
    
    @Configuration
    
    // 标记这个注解的类会全面接管SpringMVC,不会再自动装配 WebMvc配置
    
    @EnableWebMvc
    public class MyMvcConfig implements WebMvcConfigurer {
    }
    

    再次访问首页

    1582344179531.png

    可以看到自动配置失效了,回归到了最初的样子!

    说说为什么:

    我们先点击去这个:@EnableWebMvc注解看看

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    @Import(DelegatingWebMvcConfiguration.class)
    public @interface EnableWebMvc {
    
    }
    

    它导入了一个类:DelegatingWebMvcConfiguration

    再点进入看看

    @Configuration
    public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    
    }
    

    DelegatingWebMvcConfiguration它又继承了一个父类:WebMvcConfigurationSupport

    现在我们再回到:WebMvcAtuoConfiguration这个自动配置类

    // 代表这是一个配置类:Java Config
    @Configuration
    // 判断容器是否是 web容器
    @ConditionalOnWebApplication(type = Type.SERVLET)
    // 判断容器中有没有这些类
    @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
    /*
    @ConditionalOnMissingBean:判断容器中是否【没有】WebMvcConfigurationSupport 这个类,如果没有才会生效。
    如果容器中没有这个组件的时候,这个自动配置类才会生效!
    而我们的@EnableWebMvc注解导入的类,它最终继承了这个WebMvcConfigurationSupport配置类,所以一旦加上了@EnableWebMvc这个注解,SpringBoot对SpirngMVC的自动装配才会失效!
    */
    @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
    @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
          ValidationAutoConfiguration.class })
    public class WebMvcAutoConfiguration {
      
    }
    

    总结一句话:@EnableWebMvc将WebMvcConfigurationSupport组件导入进来了;而导入的WebMvcConfigurationSupport只是SpringMVC最基本的功能!

    在SpringBoot中会有非常多的 XXConfigurer帮助我们进行扩展配置,只要看见了这个,我们就应该多留心注意

    首页实现

    实现目的:默认访问首页

    方式一:通过Controller实现

    // 会解析到templates目录下的index.html页面
    @RequestMapping({"/","/index.html"})
    public String index(){
      return "index";
    }
    

    方式二:编写MVC扩展配置

    package com.rainszj.config;
    
    @Configuration
    public class MyMvcConfig implements WebMvcConfigurer {
    
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/").setViewName("index");
            registry.addViewController("/index.html").setViewName("index");
        }
    }
    

    为了保证资源导入稳定,我们建议在所有资源导入时候使用 th:去替换原有的资源路径!

    // 修改前
    <script type="text/javascript" src="js/bootstrap.min.js"></script>
    <link href="css/bootstrap.min.css" rel="stylesheet">
      
    // 修改后 使用 @{/...},其中 / 不能忘写,它代表当前项目本身
    // @{}它会自动帮我们到存放静态资源的文件夹下寻找相关资源 
    <script type="text/javascript" th:src="@{/js/bootstrap.min.js}"></script>
    <link th:href="@{/css/dashboard.css}" rel="stylesheet">
    

    修改项目名:在application.properties或者yaml

    server.servlet.context-path=/项目名
    

    修改完项目名后,访问地址变成:localhost:8080/项目名/

    使用 th:后,无论我们的项目名称如何变化,它都可以自动寻找到!

    页面国际化

    国际化:可以切换不同的语言显示

    首先在IDEA中,统一设置properties的编码问题,防止乱码

    1582366014576.png

    在resources目录下新建一个i18n(Internationalization)目录,新建一个login.properties 文件,还有一个 login_zh_CN.properties,到这一步IDEA会自动识别我们要做国际化的操作;文件夹变了!

    1582366198339.png

    1582366235069.png

    1582366259852.png

    第一步:编写页面对应的国际化配置文件

    1582367644040.png

    1582367768429.png

    login.properties:默认

    login.password=密码
    login.remeber=记住我
    login.sign=登录
    login.tip=请登录
    login.username=用户名
    

    login_zh_CN.properties:中文

    login.password=密码
    login.remeber=记住我
    login.sign=登录
    login.tip=请登录
    login.username=用户名
    

    login_en_US.properties:英文

    login.password=Password
    login.remeber=Remember me
    login.sign=Sign in
    login.tip=Please sign in
    login.username=Username
    

    第二步:我们去看一下SpringBoot对国际化的自动配置!

    这里又涉及到一个类: MessageSourceAutoConfiguration ,里面有一个方法,这里发现SpringBoot已经自动配置好了管理我们国际化资源文件的组件 ResourceBundleMessageSource

    @Configuration
    @ConditionalOnMissingBean(value = MessageSource.class, search = SearchStrategy.CURRENT)
    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
    @Conditional(ResourceBundleCondition.class)
    @EnableConfigurationProperties
    public class MessageSourceAutoConfiguration {
    
       private static final Resource[] NO_RESOURCES = {};
    
       @Bean
      // 绑定application.yaml中的spring.meeages
       @ConfigurationProperties(prefix = "spring.messages")
       public MessageSourceProperties messageSourceProperties() {
          return new MessageSourceProperties();
       }
    
       @Bean
       public MessageSource messageSource(MessageSourceProperties properties) {
          ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
          if (StringUtils.hasText(properties.getBasename())) {
             messageSource.setBasenames(StringUtils
                   .commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
          }
          if (properties.getEncoding() != null) {
             messageSource.setDefaultEncoding(properties.getEncoding().name());
          }
          messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
          Duration cacheDuration = properties.getCacheDuration();
          if (cacheDuration != null) {
             messageSource.setCacheMillis(cacheDuration.toMillis());
          }
          messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
          messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
          return messageSource;
       }
    
       protected static class ResourceBundleCondition extends SpringBootCondition {
    
          private static ConcurrentReferenceHashMap<String, ConditionOutcome> cache = new ConcurrentReferenceHashMap<>();
         
    			// 获取匹配结果
          @Override
          public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
             String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages");
             ConditionOutcome outcome = cache.get(basename);
             if (outcome == null) {
                outcome = getMatchOutcomeForBasename(context, basename);
                cache.put(basename, outcome);
             }
             return outcome;
          }
         
    			// 获取basename的匹配结果
          private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {
             ConditionMessage.Builder message = ConditionMessage.forCondition("ResourceBundle");
             for (String name : StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(basename))) {
                for (Resource resource : getResources(context.getClassLoader(), name)) {
                   if (resource.exists()) {
                      return ConditionOutcome.match(message.found("bundle").items(resource));
                   }
                }
             }
             return ConditionOutcome.noMatch(message.didNotFind("bundle with basename " + basename).atAll());
          }
    			// 获取资源
          private Resource[] getResources(ClassLoader classLoader, String name) {
             // 这就是为什么我们要写:i18n.login ,它会自动帮我们替换
             String target = name.replace('.', '/');
             try {
                return new PathMatchingResourcePatternResolver(classLoader)
                      .getResources("classpath*:" + target + ".properties");
             }
             catch (Exception ex) {
                return NO_RESOURCES;
             }
          }
       }
    }
    

    在applicaiont.properties中配置国际化的的路径:

    spring.messages.basename=i18n.login
    

    第三步:去页面获取管国际化的值

    thymeleaf中,取message的表达式为:#{}

    <h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
    

    行内写法:

    <div class="checkbox mb-3">
        <label>
            <input type="checkbox"> [[#{login.remeber}]]
        </label>
    </div>
    

    index.html

    注意:引入thymeleaf的头文件

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="description" content="">
        <meta name="author" content="">
        <title>Signin Template for Bootstrap</title>
        <!-- Bootstrap core CSS -->
        <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
        <!-- Custom styles for this template -->
        <link th:href="@{/css/signin.css}" rel="stylesheet">
    </head>
    
    <body class="text-center">
    
    <form class="form-signin" th:action="#">
        <img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72">
        <h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
    
        <p style="color: red;" th:text="${msg}"></p>
    
        <input type="text" class="form-control" name="username" th:placeholder="#{login.username}" required="" autofocus="">
        <input type="password" class="form-control" name="password" th:placeholder="#{login.password}" required="">
        <div class="checkbox mb-3">
            <label>
                <input type="checkbox"> [[#{login.remeber}]]
            </label>
        </div>
        <button class="btn btn-lg btn-primary btn-block" type="submit">[[#{login.sign}]]</button>
        <p class="mt-5 mb-3 text-muted">© 2017-2018</p>
        <a class="btn btn-sm" href="">中文</a>
        <a class="btn btn-sm" href="">English</a>
    </form>
    </body>
    </html>
    

    但是我们想要更好!可以根据按钮自动切换中文英文!

    在Spring中有一个国际化的 Locale (区域信息对象);里面有一个叫做LocaleResolver (获取区域信息对象)的解析器

    我们去我们webmvc自动配置文件,寻找一下!看到SpringBoot默认配置了

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
    public LocaleResolver localeResolver() {
      // 用户配置了就用优先用用户配置的,否则容器会基于 accept-language 配置 
      // accept-language 通常是由客户端浏览器决定,更进一步是由操作系统的语言决定
       if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
          return new FixedLocaleResolver(this.mvcProperties.getLocale());
       }
       AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
       localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
       return localeResolver;
    }
    

    AcceptHeaderLocaleResolver 这个类中有一个方法

    @Override
    public Locale resolveLocale(HttpServletRequest request) {
      // 默认的就是根据请求头带来的区域信息获取Locale进行国际化
       Locale defaultLocale = getDefaultLocale();
       if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
          return defaultLocale;
       }
       Locale requestLocale = request.getLocale();
       List<Locale> supportedLocales = getSupportedLocales();
       if (supportedLocales.isEmpty() || supportedLocales.contains(requestLocale)) {
          return requestLocale;
       }
       Locale supportedLocale = findSupportedLocale(request, supportedLocales);
       if (supportedLocale != null) {
          return supportedLocale;
       }
       return (defaultLocale != null ? defaultLocale : requestLocale);
    }
    

    那假如我们现在想点击链接让我们的国际化资源生效,就需要让我们自己的locale生效!

    我们去自己写一个自己的LocaleResolver,可以在链接上携带区域信息!

    修改一下前端页面的跳转连接;

    <a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
    <a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>
    <!--这里携带参数不用 ?,使用(key=value)-->
    

    去写一个处理区域信息的类,实现LocaleResolver 接口

    // 可以在链接上携带区域信息
    public class MyLocaleResolver implements LocaleResolver {
    // 解析请求
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
    
        String language = request.getParameter("l");
        Locale locale = Locale.getDefault(); // 如果没有获取到就使用系统默认的
        // 如果请求链接不为空
        if (!StringUtils.isEmpty(language)){
            // 分割请求参数
            String[] split = language.split("_");
            // 语言、国家
            locale = new Locale(split[0],split[1]);
        }
        return locale;
    }
    
    @Override
    public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {
    
    		}
    }
    

    为了让我们自己的区域化信息对象生效,我们需要在 MyMvcConfig 中注册它的Bean,把它交给Spring容器托管

    @Bean
    public LocaleResolver localeResolver() {
        return new MyLocaleResolver();
    }
    

    我们重启项目,来访问一下,发现点击按钮可以实现成功切换!

  • 相关阅读:
    损失函数VS评估指标
    协程到底是什么?看完这个故事明明白白!
    一个故事看懂AI神经网络工作原理
    一个爬虫的故事:这是人干的事儿?
    深夜,我偷听到程序员要对session下手……
    突然挂了!Redis缓存都在内存中,这下完了!
    还不懂Redis?看完这个故事就明白了!
    可怕!公司部署了一个东西,悄悄盯着你!
    小白怎么入门网络安全?看这篇就够啦!
    CPU有个禁区,内核权限也无法进入!
  • 原文地址:https://www.cnblogs.com/rainszj/p/12730699.html
Copyright © 2011-2022 走看看