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();
}
}
为何这么做会生效(原理)
WebMvcAutoConfiguration
是SpringMVC的自动配置类,里面有一个类WebMvcAutoConfigurationAdapter
- 这个类上有一个注解,在做其他自动配置时会导入:
@Import(EnableWebMvcConfiguration.class)
- 我们点击
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一下,看看是否会自动装配。
- 我们可以在
DelegatingWebMvcConfiguration
类中去寻找一个我们刚才设置的 addViewControllers() 当做参考,发现它调用了WebMvcConfigurerComposite
的addViewControllers()
方法
@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
然后再添加@EnableWebMvc
// 标记这个注解的类是一个配置类,本质也是一个 Component:组件
@Configuration
// 标记这个注解的类会全面接管SpringMVC,不会再自动装配 WebMvc配置
@EnableWebMvc
public class MyMvcConfig implements WebMvcConfigurer {
}
再次访问首页
可以看到自动配置失效了,回归到了最初的样子!
说说为什么:
我们先点击去这个:@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的编码问题,防止乱码
在resources目录下新建一个i18n(Internationalization)目录,新建一个login.properties 文件,还有一个 login_zh_CN.properties,到这一步IDEA会自动识别我们要做国际化的操作;文件夹变了!
第一步:编写页面对应的国际化配置文件
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();
}
我们重启项目,来访问一下,发现点击按钮可以实现成功切换!