四、SpringBoot的Web开发

1、简介

使用Spring Boot:

(1)、创建Spring Boot应用,添加需要的模块;

(2)、Spring Boot对于支持自动配置的模块已经加载完毕,只需要在配置文件中指定少量配置信息即可;

(3)、编写业务逻辑代码。

2、Spring Boot对静态资源的映射规则:

ResourceProperties:Spring Boot静态资源配置类

@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties implements ResourceLoaderAware, InitializingBean {
    //可以设置和静态资源有关的参数,缓存时间等。。。
    
    
    private static final String[] SERVLET_RESOURCE_LOCATIONS = { "/" };

    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
            "classpath:/META-INF/resources/", "classpath:/resources/",
            "classpath:/static/", "classpath:/public/" };
    
    //staticLocations即为上面连个变量的合集
    private String[] staticLocations = RESOURCE_LOCATIONS;
    
    static {
        RESOURCE_LOCATIONS = new String[CLASSPATH_RESOURCE_LOCATIONS.length
                + SERVLET_RESOURCE_LOCATIONS.length];
        System.arraycopy(SERVLET_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS, 0,
                SERVLET_RESOURCE_LOCATIONS.length);
        System.arraycopy(CLASSPATH_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS,
                SERVLET_RESOURCE_LOCATIONS.length, CLASSPATH_RESOURCE_LOCATIONS.length);
    }
    
    //other code...
    
}

WebMvcProperties:WebMvc配置类

@ConfigurationProperties(prefix = "spring.mvc")
public class WebMvcProperties {
    //静态资源的路径pattern为/**
    private String staticPathPattern = "/**";
    
    //other code...
}

MVC的自动配置都在WebMvcAutoConfiguration

@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class,
        WebMvcConfigurerAdapter.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
        ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
    //other code...
}

先来看看静态资源文件的映射规则:

WebMvcAutoConfiguration#addResourceHandlers方法:

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
        logger.debug("Default resource handling disabled");
        return;
    }
    Integer cachePeriod = this.resourceProperties.getCachePeriod();
    //如果是/webjars/**请求,则将其映射到classpath:/META-INF/resources/webjars/目录下
    if (!registry.hasMappingForPattern("/webjars/**")) {
        customizeResourceHandlerRegistration(registry
                                             .addResourceHandler("/webjars/**")
                                             .addResourceLocations("classpath:/META-INF/resources/webjars/")
                                             .setCachePeriod(cachePeriod));
    }
    //如果是请求/**,将其映射到上述staticLocations指定的值
    String staticPathPattern = this.mvcProperties.getStaticPathPattern();
    if (!registry.hasMappingForPattern(staticPathPattern)) {
        customizeResourceHandlerRegistration(
            registry.addResourceHandler(staticPathPattern)
            .addResourceLocations(
                this.resourceProperties.getStaticLocations())
            .setCachePeriod(cachePeriod));
    }
}

1)、所有/webjars/**资源映射请求都去classpath:/META-INF/resources/webjars/找资源

Integer cachePeriod = this.resourceProperties.getCachePeriod();
if (!registry.hasMappingForPattern("/webjars/**")) {
    customizeResourceHandlerRegistration(registry
                                         .addResourceHandler("/webjars/**")
                                         .addResourceLocations("classpath:/META-INF/resources/webjars/")
                                         .setCachePeriod(cachePeriod));
}

​ webjars:以jar包的方式引入的静态资源,http://www.webjars.org/

以jQuery为例:引入jQuery的jar包

<!-- 引入webjars-jQuery -->
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.3.1-1</version>
</dependency>
以jar包引入的静态资源映射图.png

请求jQuery正确的路径为:http://localhost:8080/webjars/jquery/3.3.1-1/jquery.js

__总结:所有以/webjars/**的资源映射请求会去classpath:/META-INF/resources/webjars/找资源。 __

2)、/** 访问当前项目的任何资源(静态资源的文件夹)

String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
    customizeResourceHandlerRegistration(
        registry.addResourceHandler(staticPathPattern)
        .addResourceLocations(
            this.resourceProperties.getStaticLocations())
        .setCachePeriod(cachePeriod));
}

this.mvcProperties.getStaticPathPattern()返回/**

this.resourceProperties.getStaticLocations()返回

"/",
"classpath:/META-INF/resources/", 
"classpath:/resources/",
"classpath:/static/", 
"classpath:/public/"

__总结:任何访问/**的请求,就会去静态资源文件夹下查找对应的资源名。 __

3)、欢迎页(首页)的资源映射

@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(
    ResourceProperties resourceProperties) {
    return new WelcomePageHandlerMapping(resourceProperties.getWelcomePage(),
                                         this.mvcProperties.getStaticPathPattern());
}

ResourceProperties#getWelcomePage方法:

public Resource getWelcomePage() {
    for (String location : getStaticWelcomePageLocations()) {
        Resource resource = this.resourceLoader.getResource(location);
        try {
            if (resource.exists()) {
                resource.getURL();
                return resource;
            }
        }
        catch (Exception ex) {
            // Ignore
        }
    }
    return null;
}

ResourceProperties#getStaticWelcomePageLocations方法:

//获取所有静态资源文件夹下的欢迎页
private String[] getStaticWelcomePageLocations() {
    String[] result = new String[this.staticLocations.length];
    for (int i = 0; i < result.length; i++) {
        String location = this.staticLocations[i];
        if (!location.endsWith("/")) {
            location = location + "/";
        }
        result[i] = location + "index.html";
    }
    return result;
}

__总结:从源码来看,静态资源文件夹下的所有index.html页面会被/**映射。 __

4)、所有的**/favicon.ico 都会在静态资源文件夹下查找:

@Configuration
@ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true)
public static class FaviconConfiguration {

    private final ResourceProperties resourceProperties;

    public FaviconConfiguration(ResourceProperties resourceProperties) {
        this.resourceProperties = resourceProperties;
    }

    @Bean
    public SimpleUrlHandlerMapping faviconHandlerMapping() {
        SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
        mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
        mapping.setUrlMap(Collections.singletonMap("**/favicon.ico",
                                                   faviconRequestHandler()));
        return mapping;
    }

    @Bean
    public ResourceHttpRequestHandler faviconRequestHandler() {
        ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();
        requestHandler
            .setLocations(this.resourceProperties.getFaviconLocations());
        return requestHandler;
    }
}

ResourceProperties#getFaviconLocations方法:

//ResourceProperties#getFaviconLocations()
List<Resource> getFaviconLocations() {
    List<Resource> locations = new ArrayList<Resource>(
        this.staticLocations.length + 1);
    if (this.resourceLoader != null) {
        for (String location : this.staticLocations) {
            locations.add(this.resourceLoader.getResource(location));
        }
    }
    locations.add(new ClassPathResource("/"));
    return Collections.unmodifiableList(locations);
}

__总结:任何路径下请求**/favicon.ico,都会去静态资源文件下查找。 __

3、模版引擎

JSP,Velocity,Freemarker,Thymeleaf


模版引擎.png

Spring Boot推荐的模版引擎:Thymeleaf

3.1、引入Thymeleaf依赖

<!-- 修改Spring Boot的默认版本 -->
<thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
<!-- 布局功能的支持程序
      thymeleaf3 对应layout2版本
      thymeleaf2 对应layout1版本
   -->
<thymeleaf-layout-dialect.version>2.2.2</thymeleaf-layout-dialect.version>

<!-- thymeleaf模版引擎 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

3.2、Thymeleaf的使用&语法

ThymeleafProperties配置类:

@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {
    //默认编码
    private static final Charset DEFAULT_ENCODING = Charset.forName("UTF-8");
    //文档类型
    private static final MimeType DEFAULT_CONTENT_TYPE = MimeType.valueOf("text/html");
    //模版位置
    public static final String DEFAULT_PREFIX = "classpath:/templates/";
    //模版后缀
    public static final String DEFAULT_SUFFIX = ".html";

    private boolean checkTemplate = true;

    private boolean checkTemplateLocation = true;

    private String prefix = DEFAULT_PREFIX;

    private String suffix = DEFAULT_SUFFIX;

    private String mode = "HTML5";

    private Charset encoding = DEFAULT_ENCODING;

    private MimeType contentType = DEFAULT_CONTENT_TYPE;
    //缓存
    private boolean cache = true;

    private Integer templateResolverOrder;

    private String[] viewNames;

    private String[] excludedViewNames;

    private boolean enabled = true;
    
    //other code...
}

只要把模版html放置在classpath:/templates/目录下,thymeleaf就会自动渲染。

使用Thymeleaf:

​ (1)、导入thymeleaf的名称空间:

<html lang="en" xmlns:th="http://www.thymeleaf.org"></html>

​ (2)、thymeleaf语法:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div th:text="${hello}">这里是原来的文本!</div>
</body>
</html>

​ (3)、语法规则:

​ i)、th:text:改变当前元素里面的文本内容

th:任意html属性:用来替换原生html属性的值,例如id换成th:id, class换成th:class等等。

thymeleaf语法标签.png

​ ii)、表达式:

Simple expressions:(表达式语法)
    Variable Expressions: ${...}:获取变量值,OGNL表达式
        1)、获取对象的属性、调用方法
        2)、使用内置的基本对象
           #ctx : the context object.
            #vars: the context variables.
            #locale : the context locale.
            #request : (only in Web Contexts) the HttpServletRequest object.
            #response : (only in Web Contexts) the HttpServletResponse object.
            #session : (only in Web Contexts) the HttpSession object.
            #servletContext : (only in Web Contexts) the ServletContext object.
         3)、内置的工具对象
            #execInfo : information about the template being processed.
            #messages : methods for obtaining externalized messages inside variables expressions, in the same way as they
            would be obtained using #{…} syntax.
            #uris : methods for escaping parts of URLs/URIs
            #conversions : methods for executing the configured conversion service (if any).
            #dates : methods for java.util.Date objects: formatting, component extraction, etc.
            #calendars : analogous to #dates , but for java.util.Calendar objects.
            #numbers : methods for formatting numeric objects.
            #strings : methods for String objects: contains, startsWith, prepending/appending, etc.
            #objects : methods for objects in general.
            #bools : methods for boolean evaluation.
            #arrays : methods for arrays.
            #lists : methods for lists.
            #sets : methods for sets.
            #maps : methods for maps.
            #aggregates : methods for creating aggregates on arrays or collections.
            #ids : methods for dealing with id attributes that might be repeated (for example, as a result of an iteration)
            
    Selection Variable Expressions: *{...}:选择表达式,跟${}功能类似
        补充功能:配合th:object进行使用
            <div th:object="${session.user}">
                <p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
                <p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
                <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
             </div>
             
    Message Expressions: #{...}:获取国际化内容
    Link URL Expressions: @{...}:定义URL链接
        例子:@{/order/process(execId=${execId},execType='FAST')}
        
    Fragment Expressions: ~{...}:片段引用表达式
        
Literals:
    Text literals: 'one text' , 'Another one!' ,…
    Number literals: 0 , 34 , 3.0 , 12.3 ,…
    Boolean literals: true , false
    Null literal: null
    Literal tokens: one , sometext , main ,…
    
Text operations:
    String concatenation: +
    Literal substitutions: |The name is ${name}|
    Arithmetic operations:
    Binary operators: + , - , * , / , %
    Minus sign (unary operator): -
    
Boolean operations:
    Binary operators: and , or
    Boolean negation (unary operator): ! , not
    
Comparisons and equality:
    Comparators: > , < , >= , <= ( gt , lt , ge , le )
    Equality operators: == , != ( eq , ne )
    
Conditional operators:
    If-then: (if) ? (then)
    If-then-else: (if) ? (then) : (else)
    Default: (value) ?: (defaultvalue)
    
Special tokens:
    No-Operation: _

4、SpringMVC自动配置

https://docs.spring.io/spring-boot/docs/1.5.12.RELEASE/reference/htmlsingle/#boot-features-developing-web-applications

4.1、Spring MVC auto-configuration

Spring Boot 提供了大多数SpringMVC应用常用的自动配置项。

以下是Spring Boot对SpringMVC的默认配置(来自官网,自行翻译):

  • 自动配置了 ContentNegotiatingViewResolverBeanNameViewResolver 的Beans.
    • 给容器自定义添加一个视图解析器,该ContentNegotiatingViewResolver 的bean会自动组合进来。
    • 自动配置了ViewResolver:ContentNegotiatingViewResolver是组合了所有的视图解析器
public class WebMvcAutoConfiguration {
    //other code...
    
    @Bean
    @ConditionalOnBean(ViewResolver.class)
    @ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)
    public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
        ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
        resolver.setContentNegotiationManager(
            beanFactory.getBean(ContentNegotiationManager.class));
        // ContentNegotiatingViewResolver uses all the other view resolvers to locate
        // a view so it should have a high precedence
        resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return resolver;
    }
}

其中ContentNegotiatintViewResolver类实现ViewResoler接口:

public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
        implements ViewResolver, Ordered, InitializingBean {
    //other code...
    
    @Override
    public View resolveViewName(String viewName, Locale locale) throws Exception {
        RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
        Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
        //获取所有的MediaType,例如application/json,text/html等等。。。
        List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
        if (requestedMediaTypes != null) {
            //获取所有的视图
            List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
            //根据请求获取最适合的视图
            View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
            if (bestView != null) {
                return bestView;
            }
        }
        //other code...
    }
    
    private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
            throws Exception {

        List<View> candidateViews = new ArrayList<View>();
        for (ViewResolver viewResolver : this.viewResolvers) {
            View view = viewResolver.resolveViewName(viewName, locale);
            if (view != null) {
                candidateViews.add(view);
            }
            for (MediaType requestedMediaType : requestedMediaTypes) {
                List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
                for (String extension : extensions) {
                    String viewNameWithExtension = viewName + '.' + extension;
                    view = viewResolver.resolveViewName(viewNameWithExtension, locale);
                    if (view != null) {
                        candidateViews.add(view);
                    }
                }
            }
        }
        //other code...
        
        return candidateViews;
    }
}
  • 支持静态资源文件夹和webjars
  • 静态首页的访问。
  • 自定义favicon图标
  • 自动注册了 Converter, GenericConverter, Formatter Beans.
    • Converter:转换器,用于类型转换
    • Formatter:格式化器
@Bean
//如果配置了spring.mvc.date-format,则自动注册Formatter<Date>的Bean
@ConditionalOnProperty(prefix = "spring.mvc", name = "date-format")
public Formatter<Date> dateFormatter() {
    return new DateFormatter(this.mvcProperties.getDateFormat());
}

自动添加的格式化转换器,只需添加到容器中即可。

  • 支持HttpMessageConverters 消息转换器。
    • HttpMessageConverter:SpringMVC用来转换Http请求和响应的
    • HttpMessageConverters 是从容器中获取的所有的HttpMessageConverter,如果需要给容器中添加HttpMessageConverter,只需要将自定义的组件注册在容器中即可。
  • 自动配置 MessageCodesResolver 用于错误代码的生成规则。
    • PREFIX_ERROR_CODE: error_code + "." + object name + "." + field
    • POSTFIX_ERROR_CODE:object name + "." + field + "." + error_code
  • 自动使用 ConfigurableWebBindingInitializerBean。

    • 可以自定义配置一个ConfigurableWebBindingInitializer来替换默认的,需要添加到容器中。
    • 初始化WebDataBinder:用于将请求数据绑定到数据模型等。

    org.springframework.boot.autoconfigure.web:web的所有自动配置场景。

If you want to keep Spring Boot MVC features, and you just want to add additional MVC configuration (interceptors, formatters, view controllers etc.) you can add your own @Configuration class of type WebMvcConfigurerAdapter, but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter or ExceptionHandlerExceptionResolver you can declare a WebMvcRegistrationsAdapter instance providing such components.

If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.

4.2、扩展SpringMVC

编写一个配置类,是WebMvcConfigurerAdapter类的子类,但是不能标注@EnableWebMvc注解。

​ 既保留了所有的自动配置,也能使用自定义的扩展配置。

//使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter{

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
//      super.addViewControllers(registry);
        //浏览器发送/cay请求,会直接跳到success页面。
        registry.addViewController("/cay").setViewName("success");
    }
}

原理:

1)、WebMvcAutoConfiguration是SpringMVC的自动配置类,在内部维护了一个内部类WebMvcAutoConfigurationAdapter,该类又继承自WebMvcConfigurerAdapter,看定义:

@Configuration
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter {}

WebMvcConfigurerAdapter又实现了WebMvcConfigurer接口:

public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer {}

所以自定义的配置类(此例为MyMvcConfig)是个WebMvcConfigurer接口的实现类。

2)、在做WebMvcAutoConfigurationAdapter自动配置时会导入@Import(EnableWebMvcConfiguration.class)

@Configuration
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {}

父类:

@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {

    private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();

    //从容器中获取所有的WebMvcConfigurer
    @Autowired(required = false)
    public void setConfigurers(List<WebMvcConfigurer> configurers) {
        if (!CollectionUtils.isEmpty(configurers)) {
            this.configurers.addWebMvcConfigurers(configurers);
        }
    }
}

3)、容器中所有的WebMvcConfigurer都会一起起作用。

class WebMvcConfigurerComposite implements WebMvcConfigurer {

    private final List<WebMvcConfigurer> delegates = new ArrayList<WebMvcConfigurer>();

    public void addWebMvcConfigurers(List<WebMvcConfigurer> configurers) {
        if (!CollectionUtils.isEmpty(configurers)) {
            this.delegates.addAll(configurers);
        }
    }
    
    @Override
    public void addFormatters(FormatterRegistry registry) {
        for (WebMvcConfigurer delegate : this.delegates) {
            delegate.addFormatters(registry);
        }
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        for (WebMvcConfigurer delegate : this.delegates) {
            delegate.addInterceptors(registry);
        }
    }
    
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        for (WebMvcConfigurer delegate : this.delegates) {
            delegate.addViewControllers(registry);
        }
    }
    
    //other code...
}

从源码中可以看到,从容器中获取的所有WebMvcConfigurer对象都会被调用对应的配置方法。

4)、最后可以结合第2点和第3点看出,自定义的配置类也会被调用。

总结:SpringMVC的自动配置和自定义的扩展配置都会起作用。

4.3、全面接管SpringMVC

在配置类上使用@EnableWebMvc注解,这样Spring Boot对SpringMVC的自动配置就失效了,所有都需要自定义配置。

原理:为什么使用了@EnableWebMvc后自动配置就失效了?

​ 1)、EnableWebMvc注解的定义

@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {}

​ 2)、导入了DelegatingWebMvcConfiguration组件配置

@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {}

​ 3)、查看WebMvcAutoConfiguration自动配置类的签名

@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class,
        WebMvcConfigurerAdapter.class })
//如果容器中没有该组件的时候,这个自动配置类就自动生效。
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
        ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {}

​ 4)、@EnableWebMvc将WebMvcConfigurationSupport组件导入进来了,导致Spring Boot对SpringMVC的自动配置失效,即WebMvcAutoonfiguration配置类未注册成功。

5、如何修改SpringBoot的默认配置?

模式:

​ 1)、Spring Boot在自动配置很多组件的时候,会先检查容器中是否有用户自定义配置的Bean或者组件。如果有,就使用用户自定义的;如果没有,Spring Boot才自动配置;如果有些组件可以有多个,用户可以自定义添加组件,并加入到容器中,这样Spring Boot会自动将用户配置的和默认的组合起来。

​ 2)、在Spring Boot中会有很多的Configurer帮助用户进行扩展配置。

​ 3)、在Spring Boot中会有很多的Customizer帮助用户进行定制配置。

6、Restful

6.1、设置默认访问首页的url

@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter{

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
//      super.addViewControllers(registry);
        registry.addViewController("/cay").setViewName("success");
        registry.addViewController("/login.html").setViewName("login");
        registry.addViewController("/").setViewName("login");
    }
}

6.2、国际化

Spring应用程序处理国际化资源的步骤:

​ 1)、编写国际化配置文件

​ 2)、使用ResourceBundleMessageSource管理国际化资源文件

​ 3)、在页面使用fmt:message取出国际化内容

步骤:

​ 1)、编写国际化配置文件,抽取页面中需要进行显示的国际化信息。


国际化配置信息.png

​ 2)、Spring Boot自动配置了管理国际化资源文件的组件:

@Configuration
@ConditionalOnMissingBean(value = MessageSource.class, search = SearchStrategy.CURRENT)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Conditional(ResourceBundleCondition.class)
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "spring.messages")
public class MessageSourceAutoConfiguration {
    
    /**
     * Comma-separated list of basenames (essentially a fully-qualified classpath
     * location), each following the ResourceBundle convention with relaxed support for
     * slash based locations. If it doesn't contain a package qualifier (such as
     * "org.mypackage"), it will be resolved from the classpath root.
     */
    private String basename = "messages";
    
    //other code...
    
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        if (StringUtils.hasText(this.basename)) {
            //设置国际化资源文件的基础名
            messageSource.setBasenames(StringUtils.commaDelimitedListToStringArray(
                    StringUtils.trimAllWhitespace(this.basename)));
        }
        if (this.encoding != null) {
            messageSource.setDefaultEncoding(this.encoding.name());
        }
        messageSource.setFallbackToSystemLocale(this.fallbackToSystemLocale);
        messageSource.setCacheSeconds(this.cacheSeconds);
        messageSource.setAlwaysUseMessageFormat(this.alwaysUseMessageFormat);
        return messageSource;
    }
}

默认情况下,国际化资源文件的基础名为messages,且存放在classpath根路径下,即messages.properties、messages_zh_CN.properties、messages_en_US.properties等等,这样就无需在配置文件中设置spring.messages.basename=...了,但是如果基础名不为messages或者不在classpath根路径下,则需要手动添加spring.messages.basename=文件名.自定义的基础名,如果有多个就用逗号分隔。

spring.messages.basename=i18n.login

​ 3)、去页面获取国际化的值:

<!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 href="asserts/css/bootstrap.min.css" th:href="@{/webjars/bootstrap/4.1.0/css/bootstrap.css}" rel="stylesheet">
        <!-- Custom styles for this template -->
        <link href="asserts/css/signin.css" th:href="@{/asserts/css/signin.css}" rel="stylesheet">
    </head>

    <body class="text-center">
        <form class="form-signin" action="dashboard.html">
            <img class="mb-4" th:src="@{/asserts/img/bootstrap-solid.svg}" src="asserts/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>
            <label class="sr-only" th:text="#{login.username}">Username</label>
            <input type="text" class="form-control" placeholder="Username" th:placeholder="#{login.username}" required="" autofocus="">
            <label class="sr-only" th:text="#{login.password}">Password</label>
            <input type="password" class="form-control" placeholder="Password" th:placeholder="#{login.password}" required="">
            <div class="checkbox mb-3">
                <label>
          <input type="checkbox" value="remember-me"> [[#{login.rememberme}]]
        </label>
            </div>
            <button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.signin}">Sign in</button>
            <p class="mt-5 mb-3 text-muted">© 2017-2018</p>
            <a class="btn btn-sm">中文</a>
            <a class="btn btn-sm">English</a>
        </form>
    </body>
</html>

效果:根据浏览器语言设置的语言信息进行切换国际化语言。

​ 4)、如何通过链接切换国际化语言?

原理:

​ 国际化区域信息对象Locale,有个区域信息对象处理器LocaleResolver,在WebMvcAutoConfiguration自动配置类中配置了一个LocaleResolver的Bean。

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
public LocaleResolver localeResolver() {
    if (this.mvcProperties
        .getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
        return new FixedLocaleResolver(this.mvcProperties.getLocale());
    }
    AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
    localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
    return localeResolver;
}
  • 如果在配置文件中配置了spring.mvc.locale-resolver=fixed,且指定了spring.mvc.locale的值,则页面使用的是指定且固定的国际语言(FixedLocaleResolver);
  • spring.mvc.locale-resolver默认为accept-header,即如果不配置则通过浏览器请求头中的Accept-Language 作为判断依据(AcceptHeaderLocaleResolver):
public class AcceptHeaderLocaleResolver implements LocaleResolver {
    
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        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);
    }
    
    //other code..
}

步骤:

​ i)、编写一个自定义的LocaleResolver类:

//使用链接的方法发送国际语言代码
public class MyLocaleResolver  implements LocaleResolver{

    private final Logger logger = LoggerFactory.getLogger(MyLocaleResolver.class);

    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        String lan = request.getParameter("lan");
        Locale locale = Locale.getDefault();
        if(StringUtils.hasText(lan)){
            try{
                String[] split = lan.split("_");
                locale = new Locale(split[0], split[1]);
            }catch (Exception e){
                e.printStackTrace();
                logger.error("错误信息:", e);
            }
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

    }
}

先从请求参数中获取lan数据,然后通过下划线分割成语言和国家,然后通过语言和国家来封装成一个Locale对象并返回,如果未指定lan参数,则默认使用default的Locale对象。

​ ii)、修改页面切换语言的链接,添加国际化语言代码:

<a class="btn btn-sm" th:href="@{/(lan='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/(lan='en_US')}">English</a>

​ iii)、将自定义的LocaleResolver覆盖默认的LocaleResolver:

@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter{

    //使用自定义的LocaleResolver来替换掉默认的LocaleResolver
    @Bean
    public LocaleResolver localeResolver(){
        return new MyLocaleResolver();
    }
    //other code...
}

​ 这样配置了自定义的LocaleResolver对象就会把WebMvcAutoConfiguration自动配置类中预定义的LocaleResolver的Bean屏蔽掉,Spring Boot就会使用自定义的LocaleResolver对象。

注意:一旦使用链接来切换国际化语言,则会导致通过浏览器语言切换国际化语言的功能失效了。

6.3、登录

模版引擎页面修改以后,要实时生效:

  • 禁用模版引擎的缓存:
    • spring.thymeleaf.cache=false
  • 页面修改完成后,按下Ctrl+F9,重新编译。

登录错误消息的显示:

<p style="color:red;" th:text="${message}" th:if="${not #strings.isEmpty(message)}"></p>

6.4、拦截器进行登录检查

步骤:

​ i)、编写登录拦截器:

//登录检查
public class LoginHandlerInterceptor implements HandlerInterceptor {

    //目标方法执行前
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object user = request.getSession().getAttribute("loginUser");
        if(user == null){
            //未登录,返回登录页面
            request.setAttribute("message", "请先登录!");
            request.getRequestDispatcher("/").forward(request, response);
            return false;
        }else{
            //已登录,放行
            return true;
        }
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

​ ii)、配置拦截器:

@Override
public void addInterceptors(InterceptorRegistry registry) {
    //      super.addInterceptors(registry);
    registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**")
        .excludePathPatterns("/", "/login.html", "/user/login");
}

6.5、CRUD-员工实验

thymeleaf公共页面元素抽取:

​ 1)、抽取公共片段

<div th:fragment="copy">
    &copy; 版权所有
</div>

​ 2)、引入公共片段

<div th:insert="~{footer :: copy}"></div>

~{templatename :: #selectorId} 表示:模版名::#选择器id

代码片段
<div id="copy-section">
    &copy; 2011 The Good Thymes Virtual Grocery
</div>

插入片段
<div th:insert="~{footer :: #copy-section}"></div>

~{templatename :: fragmentname} 表示:模版名::片段名

代码片段
<div th:fragment="copy">
    &copy; 2011 The Good Thymes Virtual Grocery
</div>

插入片段
<div th:insert="~{footer :: copy}"></div>

​ 3)、默认效果

​ insert的功能片段会被插入到div标签中。

​ 如果使用th:insert等属性进行引入,可以不用写~{},可以直接写templatename::#selectorId/fragmentname。

但是如果是行内写法,必须加上~{}, 如[[~{}]]。

三种引入公共片段的th属性:

th:insert:将公共片段整个插入到声明引入的元素中

th:replace:将声明引入的元素替换为公共片段

th:include:将被引入的片段的内容包含到引入的元素中

代码片段
<footer th:fragment="copy">
    &copy; 版权所有
</footer>

引入方式
<div th:insert="~{footer::copy}"></div>
<div th:replace="~{footer::copy}"></div>
<div th:include="~{footer::copy}"></div>

实际效果:
th:insert:
<div>
    <footer>
        &copy; 版权所有
    </footer>
</div>

th:replace:
<footer>
    &copy; 版权所有
</footer>

th:include:
<div>
    &copy; 版权所有
</div>

在引入代码片段的时候,可以使用传递参数的方式,这样在代码片段中就可以使用传递过来的参数。

<div th:fragment="frag">
...
</div>

<div th:replace="::frag (onevar=${value1},twovar=${value2})">

<div th:fragment="frag (onevar,twovar)">
<p th:text="${onevar} + ' - ' + ${twovar}">...</p>
</div>

<div th:replace="::frag (${value1},${value2})">...</div>
使用命名参数时顺序不重要
<div th:replace="::frag (onevar=${value1},twovar=${value2})">...</div>
<div th:replace="::frag (twovar=${value2},onevar=${value1})">...</div>

比如,在templates/commons/bar.html中定义了如下代码片段

<nav id="sidebar">
    <a class="nav-link" th:class="${activeUrl == 'main' ? 'nav-link active' : 'nav-link'}" ...> 
        ...
    </a>
</nav>

在引入该代码片段的时候,可以使用传递参数的方式

<div th:replace="commons/bar::#sidebar(activeUrl='main')"></div>
或
<div th:replace="commons/bar::#sidebar(activeUrl='emps')"></div>

如果出现status=400,大多数是数据格式不正确,特别是日期格式。

默认日期格式化是按照将/形式的字符串转换成日期类型,可以通过spring.mvc.date-format来设置日期格式:

spring.mvc.date-format=yyyy-MM-dd
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348