探究· Web开发
静态资源处理
静态资源映射规则·结论
- 对于初学者来说,纠结于原理,只会让自己陷入痛苦之中。
- 这里只给出结论,先不探究原理
- 所以得出结论,以下四个目录存放的静态资源可以被我们识别:
"classpath:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"
- 可以在resources根目录下新建对应的文件夹,都可以存放我们的静态文件
自定义静态资源路径
- 也可以自己通过配置文件来指定一下,哪些文件夹是需要我们放静态资源文件的,在application.properties中配置;
spring.resources.static-locations=classpath:/coding/,classpath:/xxx/
一旦自己定义了静态文件夹的路径,原来的自动配置就都会失效了!
Thymeleaf
模板引擎
- SpringBoot推荐你可以来使用模板引擎:
- 其实jsp就是一个模板引擎,还有以用的比较多的freemarker,包括SpringBoot给我们推荐的Thymeleaf,模板引擎有非常多,但再多的模板引擎,他们的思想都是一样的,来看一下这张图:
 - 模板引擎的作用
- 写一个页面模板,比如:有些来自在后台封装一些动态的数据值,我们通过书写一些表达式。把这个模板和这个数据交给模板引擎,模板引擎帮我们把这表达式解析、填充到指定的位置,然后把这个数据最终生成一个完整的内容写出去,这就是我们这个模板引擎,不管是jsp还是其他模板引擎,都是这个思想。
引入Thymeleaf
使用分析·thymeleaf
- 首先得按照SpringBoot的自动配置原理看一下这个Thymeleaf的自动配置规则,再按照那个规则,进行使用。
- 打开Thymeleaf的自动配置类***【ThymeleafProperties】***
诀窍:双击shift打开全局搜索,输入ThymeleafProperties,然后点开项目->定位图标,即可快速定位。 
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
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 = "HTML";
private Charset encoding = DEFAULT_ENCODING;
private boolean cache = true;
private Integer templateResolverOrder;
}
- 可以在其中看到默认的前缀和后缀
- 只需要把我们的html页面放在类路径下的templates下,thymeleaf就可以帮我们自动渲染
- 使用thymeleaf什么都不需要配置,只需要将他放在指定的文件夹下
注意事项
- 使用thymeleaf要在html页面引入,命名空间
xmlns:th="http://www.thymeleaf.org"

表达式
Simple expressions:(表达式语法)
Variable Expressions: ${...}:获取变量值;OGNL;
1)、获取对象的属性、调用方法
2)、使用内置的基本对象: #18
#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.
#uris : methods for escaping parts of URLs/URIs
#conversions : methods for executing the configured conversionservice (if any).
#dates : methods for java.util.Date objects: formatting, componentextraction, etc.
#calendars : analogous to #dates , but for java.util.Calendarobjects.
#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 orcollections.
Selection Variable Expressions: *{...}:选择表达式:和${}在功能上是一样;
Message Expressions: #{...}:获取国际化内容
Link URL Expressions: @{...}:定义URL;
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: _
MVC自动配置原理【建议跳过】
- 非常抱歉,由于作者的水平有限,这一部分讲解的比较模糊。
- 在进行项目编写前,我们还需要知道一个东西,就是SpringBoot对我们的SpringMVC还做了哪些配置,包括如何扩展,如何定制。
- 只有把这些都搞清楚了,我们在之后使用才会更加得心应手。 途径一:源码分析,途径二:官方文档!
Spring MVC Auto-configuration
Spring Boot provides auto-configuration for Spring MVC that works well with
most applications.
The auto-configuration adds the following features on top of Spring’s
defaults:
Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
Support for serving static resources, including support for WebJars
int类型
象】
Automatic registration of Converter, GenericConverter, and Formatter beans.
去看官网文档解释;
Support for HttpMessageConverters (covered later in this document).
Automatic registration of MessageCodesResolver (covered later in this
document).
Static index.html support.
Custom Favicon support (covered later in this document).
Automatic use of a ConfigurableWebBindingInitializer bean (covered later in
this document).
If you want to keep Spring Boot MVC features and you want to add additional
MVC configuration
(interceptors, formatters, view controllers, and other features), you
can add your own
@Configuration class of type WebMvcConfigurer but without @EnableWebMvc.
If you wish to provide
custom instances of RequestMappingHandlerMapping,
RequestMappingHandlerAdapter, or
ExceptionHandlerExceptionResolver, you can declare a
WebMvcRegistrationsAdapter instance to provide such components.
注释。
If you want to take complete control of Spring MVC, you can add your own
@Configuration annotated with @EnableWebMvc.
ContentNegotiatingViewResolver·内容协商视图解析器
- 自动配置了ViewResolver,就是我们之前学习的SpringMVC的视图解析器;
- 即根据方法的返回值取得视图对象(View),然后由视图对象决定如何渲染(转发,重定向)。
- 找到 WebMvcAutoConfiguration , 然后搜索ContentNegotiatingViewResolver
@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));
resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
return resolver;
}
- 我们可以点进这类看看!找到对应的解析视图的代码;
 
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
if (!isCache()) {
return createView(viewName, locale);
}
else {
Object cacheKey = getCacheKey(viewName, locale);
View view = this.viewAccessCache.get(cacheKey);
if (view == null) {
synchronized (this.viewCreationCache) {
view = this.viewCreationCache.get(cacheKey);
if (view == null) {
view = createView(viewName, locale);
if (view == null && this.cacheUnresolved) {
view = UNRESOLVED_VIEW;
}
if (view != null && this.cacheFilter.filter(view, viewName, locale)) {
this.viewAccessCache.put(cacheKey, view);
this.viewCreationCache.put(cacheKey, view);
}
}
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace(formatKey(cacheKey) + "served from cache");
}
}
return (view != UNRESOLVED_VIEW ? view : null);
}
}
- 很抱歉到这里之后,新版的内容和旧版的内容,有很大的不同,大家可以跳过这一部分的内容。不要拘泥于剖析源码。
- 但是把最终的结论放在这里:ContentNegotiatingViewResolver 这个视图解析器就是用来组合所有的视图解析器的
实现
@Bean
public ViewResolver myViewResolver() {
return new MyViewResolver();
}
public static class MyViewResolver implements ViewResolver {
@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
return null;
}
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/yang").setViewName("test");
}
配置项目环境及首页
依赖
mybatis整合·代码·依赖
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
其他依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
导入·实体类
package com.yang.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department {
private Integer id;
private String DepartmentName;
}
package com.yang.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import sun.util.calendar.LocalGregorianCalendar;
import java.util.Date;
@Data
@NoArgsConstructor
public class Employee {
private Integer id;
private String lastName;
private String email;
private Integer gender;
private Department department;
private Date birth;
public Employee(Integer id, String lastName, String email, Integer gender, Department department) {
this.id = id;
this.lastName = lastName;
this.email = email;
this.gender = gender;
this.department = department;
this.birth = new Date();
}
}
伪造·数据
package com.yang.Dao;
import com.yang.pojo.Department;
import com.yang.pojo.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@Repository
public class EmployeeDao {
private static Map<Integer, Employee> employees;
@Autowired
private DepartmentDao departmentDao;
static {
employees=new HashMap<Integer,Employee>();
employees.put(1001,new Employee(1001,"小明", "1558975826@qq.com",1,new Department(101,"教学部")));
employees.put(1002,new Employee(1002,"小李", "1568975826@qq.com",1,new Department(102,"包装部")));
employees.put(1003,new Employee(1003,"小红", "1578975826@qq.com",0,new Department(103,"销售部")));
employees.put(1004,new Employee(1004,"小华", "1588975826@qq.com",1,new Department(104,"人事部")));
employees.put(1005,new Employee(1005,"小王", "1598975826@qq.com",1,new Department(105,"主管部")));
employees.put(1006,new Employee(1006,"小张", "1518975826@qq.com",0,new Department(106,"生产部")));
}
private static Integer initId=1007;
public void add(Employee employee) {
if(employee.getId()==null) {
employee.setId(initId++);
}
employee.setDepartment(departmentDao.getDepartmentById(employee.getDepartment().getId()));
employees.put(employee.getId(),employee);
}
public Collection<Employee> getAll() {
return employees.values();
}
public Employee getEmployeeById(Integer id) {
return employees.get(id);
}
public void delete(Integer id) {
employees.remove(id);
}
}
package com.yang.Dao;
import com.yang.pojo.Department;
import org.springframework.stereotype.Repository;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@Repository
public class DepartmentDao {
private static Map<Integer, Department> departments=null;
static {
departments=new HashMap<Integer,Department>();
departments.put(101,new Department(101,"教学部"));
departments.put(102,new Department(102,"包装部"));
departments.put(103,new Department(103,"销售部"));
departments.put(104,new Department(104,"人事部"));
departments.put(105,new Department(105,"主管部"));
departments.put(106,new Department(106,"生产部"));
}
public static Collection<Department> getDepartments() {
return departments.values();
}
public Department getDepartmentById(Integer id) {
return departments.get(id);
}
}
controller层
package com.yang.config;
import com.yang.Dao.DepartmentDao;
import com.yang.Dao.EmployeeDao;
import com.yang.pojo.Department;
import com.yang.pojo.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import javax.jws.WebParam;
import java.util.Collection;
@Controller
public class EmployeeController {
@Autowired
EmployeeDao employeeDao;
@RequestMapping("/emps")
public String list(Model model) {
Collection<Employee> employees = employeeDao.getAll();
model.addAttribute("emps",employees);
return "emp/list";
}
@GetMapping("/emp")
public String toAddPage(Model model) {
Collection<Department> departments = DepartmentDao.getDepartments();
model.addAttribute("departments",departments);
return "emp/add";
}
@PostMapping("/emp")
public String addEmp(Employee employee) {
employeeDao.add(employee);
return "redirect:/emps";
}
@GetMapping("/emp/{id}")
public String toUpdate(@PathVariable("id")Integer id, Model model) {
Employee employee = employeeDao.getEmployeeById(id);
model.addAttribute("emp",employee);
Collection<Department> departments = DepartmentDao.getDepartments();
model.addAttribute("departments",departments);
return "emp/update";
}
@PostMapping("/updateEmp")
public String updateEmp(Employee employee) {
employeeDao.add(employee);
return "redirect:/emps";
}
@GetMapping("/delemp/{id}")
public String deleteEmp(@PathVariable("id") int id) {
employeeDao.delete(id);
return "redirect:/emps";
}
}
注意Maven资源导出问题
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
导入静态资源
- css,js等放在static文件夹下
- html 放在 templates文件夹下
- 结构图

首页实现
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/index.html").setViewName("index");
registry.addViewController("/main.html").setViewName("dashboard");
}
- 解决了首页问题,我们还需要解决一个资源导入的问题;
- 为了保证资源导入稳定,我们建议在所有资源导入时候使用 th:去替换原有的资源路径!这也是模板规范
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet">
页面国际化
准备工作
- 先在IDEA中统一设置properties的编码问题
 - 配置文件编写
- 在resources资源文件下新建一个i18n目录,存放国际化配置文件
- 建立一个login.properties文件,还有一个login_zh_CN.properties;发现IDEA自动识别了我们要做
国际化操作;文件夹变了!   - 接下来,我们就来编写配置
 - 这个视图我们点击 + 号就可以直接添加属性了;我们新建一个login.tip,可以看到边上有三个文件框可以输入
  - 然后去查看我们的配置文件
login.username=用户名
login.btn=登录
login.password=密码
login.remember=记住我
login.tip=请登录
- 英文
login.username=username
login.btn=Sign in
login.password=Password
login.remember=Remember me
login.tip=Please sign in
- 中文
login.username=用户名
login.btn=登录
login.password=密码
login.remember=记住我
login.tip=请登录
配置页面国际化值
- 去页面获取国际化的值,查看Thymeleaf的文档,找到message取值操作为: #{…}
<!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>Sign in 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">
<style>
#tip {
color: red;
}
</style>
</head>
<body class="text-center">
<form class="form-signin" th:action="@{/user/login}">
<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>
<!--如果msg的值为空,则不提示信息-->
<p id="tip" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
<label class="sr-only" >Username</label>
<input type="text" class="form-control" name="username" th:placeholder="#{login.username}" required="" autofocus="">
<label class="sr-only" name="password" >Password</label>
<input type="password" class="form-control" name="password" th:placeholder="#{login.password}" required="">
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me">[[#{login.remember}]]
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" >[[#{login.btn}]]</button>
<p class="mt-5 mb-3 text-muted">? 2017-2018</p>
<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>
</form>
</body>
</html>

设置按钮切换中英文
- 配置国际化解析
- 在Spring中有一个国际化的Locale (区域信息对象);里面有一个叫做LocaleResolver (获取区域信息对象)的解析器!
- 我们去我们webmvc自动配置文件,寻找一下!看到SpringBoot默认配置:
@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;
}
- AcceptHeaderLocaleResolver 这个类中有一个方法
public Locale resolveLocale(HttpServletRequest request) {
Locale defaultLocale = this.getDefaultLocale();
if (defaultLocale != null && request.getHeader("Accept-Language") ==
null) {
return defaultLocale;
} else {
Locale requestLocale = request.getLocale();
List<Locale> supportedLocales = this.getSupportedLocales();
if (!supportedLocales.isEmpty() && !supportedLocales.contains(requestLocale)) {
Locale supportedLocale = this.findSupportedLocale(request,supportedLocales);
if (supportedLocale != null) {
return supportedLocale;
} else {
return defaultLocale != null ? defaultLocale :requestLocale;
}
} else {
return 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>
package com.yang.config;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;
public class MyLocalResolver 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 request, HttpServletResponse response, Locale locale) {
}
}
- 为了让我们的区域化信息能够生效,我们需要再配置一下这个组件!在我们自己的MvcConofig下添加
bean;
@Bean
public LocaleResolver localeResolver(){
return new MyLocalResolver();
}
登录+拦截器
禁用模板缓存
说明:页面存在缓存,所以我们需要禁用模板引擎的缓存
spring:
thymeleaf:
cache: false
模板引擎修改后,想要实时生效!页面修改完毕后,IDEA小技巧 : Ctrl + F9 重新编译!即可生效!
登录
- 把登录页面的表单提交地址写一个controller
<form class="form-signin" th:action="@{/user/login}" method="post">
//这里面的所有表单标签都需要加上一个name属性
</form>
- 编写对应的controller
package com.yang.config;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpSession;
@Controller
public class LoginController {
@RequestMapping("/user/login")
public String login(
@RequestParam("username") String username,
@RequestParam("password") String password,
Model model, HttpSession httpSession) {
if(!ObjectUtils.isEmpty(username) && "5201314".equals(password)) {
httpSession.setAttribute("loginUser",username);
return "redirect:/main.html";
}else {
model.addAttribute("msg","用户名或者密码错误!");
return "index";
}
}
@RequestMapping("/user/logout")
public String logout(HttpSession session) {
session.invalidate();
return "direct:/index.html";
}
}
- 登录拦截器
- 我们可以直接登录到后台主页,不用登录也可以实现!怎么处理这个问题呢?我们可以使用拦截器机制,实现登录检查!
public class LoginHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Object loginUser = request.getSession().getAttribute("loginUser");
if(loginUser==null) {
request.setAttribute("msg","没有权限,请先登录");
request.getRequestDispatcher("/index.html").forward(request,response);
return false;
}else {
return true;
}
}
}
- 然后将拦截器注册到我们的SpringMVC配置类当中
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginHandlerInterceptor()).
addPathPatterns("/**").excludePathPatterns("/index.html","/","/user/login","/js/**","/css/**","/img/**");
}
<a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">[[${session.loginUser}]]</a>
- 结束了

|