Spring Boot 基础(二)

352 阅读18分钟

七、项目实例:员工管理系统

7.1 配置项目环境及首页

  • 项目依赖:
<!--web-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--thymeleaf-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
  • 关闭 thymeleaf 缓存:application.properties
# 关闭模板引擎的缓存
spring.thymeleaf.cache=false

创建实体类:

  • Department(部门表):
// 部门表
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department {
    private Integer id;
    private String departmentName;
}
  • Employee(员工表):
// 员工表
@Data
@NoArgsConstructor
public class Employee {
    private Integer id;
    private String lastName;
    private String email;
    // 性别:0 女  1 男
    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();
    }
}

创建 Dao 层

  • DepartmentDao:
// 部门Dao
// 注册组件:@Component 的衍生注解 dao层
@Repository
public class DepartmentDao {
    // 模拟数据库中的数据
    private static Map<Integer, Department> department = null;

    static {
        // 创建一个部门表
        department = new HashMap<Integer, Department>();
        department.put(101, new Department(101, "部门1"));
        department.put(102, new Department(102, "部门2"));
        department.put(103, new Department(103, "部门3"));
        department.put(104, new Department(104, "部门4"));
        department.put(105, new Department(105, "部门5"));
    }

    // 获得所有部门信息
    public Collection<Department> getDepartments() {
        return department.values();
    }

    // 通过id得到部门
    public Department getDepartmentById(Integer id) {
        return department.get(id);
    }
}
  • EmployeeDao:
// 员工Dao
// 注册组件:@Component 的衍生注解 dao层
@Repository
public class EmployeeDao {
    // 模拟数据库中的数据
    private static Map<Integer, Employee> employees = null;
    // 员工所属的部门
    @Autowired
    private DepartmentDao departmentDao;

    static {
        // 创建员工表
        employees = new HashMap<Integer, Employee>();
        employees.put(1001, new Employee(1001, "AA", "aa@email.com", 1, new Department(101, "部门1")));
        employees.put(1002, new Employee(1002, "BB", "bb@email.com", 0, new Department(102, "部门2")));
        employees.put(1003, new Employee(1003, "CC", "cc@email.com", 0, new Department(103, "部门3")));
        employees.put(1004, new Employee(1004, "DD", "dd@email.com", 1, new Department(104, "部门4")));
        employees.put(1005, new Employee(1005, "EE", "ee@email.com", 0, new Department(105, "部门5")));
    }

    // 主键自增
    public static Integer initId = 1006;

    // 增加员工
    public void save(Employee employee) {
        // 自动添加id
        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();
    }

    // 通过id查询员工
    public Employee getEmployeeById(Integer id) {
        return employees.get(id);
    }

    // 删除员工
    public void delete(Integer id) {
        employees.remove(id);
    }
}

静态资源导入

  • 静态资源链接

  • css、js 等文件,放在 static 文件夹下;

  • html 放在 templates 文件夹下;

  • 项目结构:

首页实现

  • 方式一:通过 controller 实现;
@Controller
public class IndexController {
    // 会解析到templates目录下的index.html页面
    @RequestMapping({"/", "/index.html"})
    public String index() {
        return "index";
    }
}
  • 方式二(推荐):自定义 MVC 的扩展配置;
// 配置类:自定义MVC的扩展配置
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    @Override
    // 视图控制
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index.html").setViewName("index");
    }
}

解决静态资源导入

  • 所有页面资源,都需要使用 thymeleaf 接管;
    • 模板规范:所有资源导入时使用 th: 替换原有的资源路径 @{/...}
<!--thymeleaf使用时需要导入头文件-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<!--@{/}静态资源根目录,static不需要写-->
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
  • 替换 index.html 等页面的资源路径后,启动主程序,运行测试://index.html

7.2 页面国际化

  • 当网站内容,涉及中、英文,甚至多语言的切换时,就需要使用国际化;

准备工作

  • 在 IDEA 中,统一设置 properties 的编码为:UTF-8;

  • 编写国际化配置文件,抽取页面需要显示的国际化页面消息;

  • 查看页面,哪些内容需要编写国际化的配置;

编写配置文件

  • 在 resources 资源文件下,新建 i18n 目录,存放国际化配置文件;

  • 创建 login.propertieslogin_zh_CN.properties 配置文件,IDEA 会自动识别国际化操作,文件夹随之发生变化:

  • 可以在此文件夹上,创建新的配置文件:

  • 弹出如下页面:再添加一个英文的配置文件:

  • IDEA 2021.2 没有国际化界面,需要在插件中下载 Resource Bundle Editor:

  • 安装插件后,打开任意一个配置文件,打开配置页面:

  • 点击 + 号,就可以直接添加属性,新建 login.tip,可以看到边上有三个文件框可以输入:

  • 添加对应的配置内容:

  • 依次添加其他内容:

  • 查看配置文件:

    • login.properties:默认;

      login.btn=登录
      login.password=密码
      login.remember=记住我
      login.tip=请登录
      login.username=用户名
      
    • login_en_US.properties:英文;

      login.btn=Sign in
      login.password=Password
      login.remember=Remember me
      login.tip=Please sign in
      login.username=Username
      
    • login_zh_CN.properties:中文;

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

国际化配置文件生效,原理分析

  • 查看 Spring Boot 对国际化的自动配置,对应类为:MessageSourceAutoConfiguration,类中有一个方法,自动配置了,管理国际化资源文件的组件:ResourceBundleMessageSource
@Bean
// 配置前缀为spring.messages
@ConfigurationProperties(prefix = "spring.messages")
public MessageSourceProperties messageSourceProperties() {
    return new MessageSourceProperties();
}


@Bean
// 获取properties传递过来的值进行判断
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;
}
  • 配置文件,是放在了 i18n 目录下,所以要配置 messages 的路径:
spring.messages.basename=i18n.login

配置页面国际化值

  • 页面获取国际化的值,查看 Thymeleaf 的文档,国际化 message 的取值操作为:#{...}
  • 修改页面内容:index.html
<form class="form-signin" action="dashboard.html">
    <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>
    <label class="sr-only">Username</label>
    <input type="text" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
    <label class="sr-only">Password</label>
    <input type="password" class="form-control" th:placeholder="#{login.password}" required="">
    <div class="checkbox mb-3">
        <label>
            <!--<input type="checkbox" value="remember-me">[[#{login.remember}]]-->
            <input type="checkbox" value="remember-me" th:text="#{login.remember}">
        </label>
    </div>
    <button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">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>
  • 运行测试:

配置国际化解析

  • 在 Spring 中有一个国际化的 Locale(区域信息对象),里面有一个叫做 LocaleResolver(获取区域信息对象)的解析器;
  • 查看 WebMvcAutoConfiguration 自动配置文件源码,看到 Spring Boot 默认配置:
@Override
@Bean
@ConditionalOnMissingBean(name = DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME)
public LocaleResolver localeResolver() {
    // 容器中没有自定义配置,就使用默认配置,有的话就用自定义的配置
    if (this.webProperties.getLocaleResolver() == WebProperties.LocaleResolver.FIXED) {
        return new FixedLocaleResolver(this.webProperties.getLocale());
    }
    // 接收头国际化分解
    AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
    localeResolver.setDefaultLocale(this.webProperties.getLocale());
    return localeResolver;
}
  • AcceptHeaderLocaleResolver 类中有一个方法:
@Override
public Locale resolveLocale(HttpServletRequest request) {
    Locale defaultLocale = getDefaultLocale();
    // 默认的就是,根据请求头带来的区域信息,获取Locale进行国际化
    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 生效;
  • 修改前端页面的跳转连接:
<!--thymeleaf传参方式:(key=value),不使用 ?方式-->
<a class="btn btn-sm" th:href="@{/index.html(lang='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(lang='en_US')}">English</a>
  • 创建自定义 LocaleResolver,可以在链接上,携带区域信息:
// 可以在链接上携带区域信息
public class MyLocaleResolver implements LocaleResolver {
    // 解析请求
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        // 获取请求中的语言参数
        String lang = request.getParameter("lang");
        // 参照自动配置类:如果没有获取到,就使用系统默认的
        Locale locale = Locale.getDefault();
        // 如果请求链接不为空,携带了区域信息
        if (!StringUtils.isEmpty(lang)) {
            // 分割请求参数:zh_CN 以下划线 _ 进行分割
            String[] split = lang.split("_");
            // 国家、地区
            locale = new Locale(split[0], split[1]);
        }
        return locale;
    }

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

    }
}
  • 为了让自定义的区域化信息,能够生效,需要再配置一下这个组件,在自定义的 MyMvcConfig 下注册这个 bean:
// 注册组件:自定义的国际化组件
@Bean
public LocaleResolver localeResolver() {
    return new MyLocaleResolver();
}
  • 重启项目,进行测试:

总结:

  • 首页配置:
    • 注意点:所有页面的静态资源,都需要使用 thymeleaf 接管;
    • url 取值:@{}
  • 页面国际化:
    • 需要配置 i18n 文件以及设置资源路径;
    • 如果需要在项目中进行按钮自动切换,需要自定义一个组件,实现 LocaleResolver 接口;
    • 将自定义的组件,配置到 Spring 容器中 @Bean
    • 国际化取值:#{}

7.3 登录 + 拦截器

  • 页面存在缓存,需要禁用模板引擎的缓存:
# 禁用模板缓存
spring.thymeleaf.cache=false
  • 使用模板引擎时,页面修改完毕后,想要实时生效,IDEA:Ctrl + F9 重新编译;

实现登录

  • 先不连接数据库,输入任意用户名都可登录成功;
  • 修改登录页面,给表单提交地址写一个 Controller:
<form class="form-signin" th:action="@{/user/login}" method="post">
	<!--所有表单标签,都需要加name属性,才能提交值-->
</form>
  • 创建登录对应的 Controller:
@Controller
public class LoginController {
    @RequestMapping("/user/login")
    // @ResponseBody:返回字符串
    public String login(
            @RequestParam("username") String username,
            @RequestParam("password") String password,
            Model model, HttpSession httpSession) {
        // 具体的业务
        if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
            // 登录成功!将用户信息放入session
            httpSession.setAttribute("loginUser", username);
            // 跳转到后台首页
            return "dashboard";

        } else {
            // 登录失败,存放错误信息
            model.addAttribute("msg", "用户名或密码错误");
            // 跳转到登录页
            return "index";
        }
    }
}
  • 运行测试:

  • 登录失败,需要将后台信息输出到前台:

<!--首页添加登录失败,提示信息-->
<!--th:if="${not #strings.isEmpty(msg)}" 条件判断:msg 不为空,not可替换为 !-->
<p style="color: red" th:text="${msg}"></p>
  • 重启,登录失败测试:

  • 优化:登录成功后,由于是转发,链接不变,可以重定向到后台首页:

  • 在自定义配置类 MyMvcConfig 中,添加视图控制器映射:

registry.addViewController("/main.html").setViewName("dashboard");
  • 将 Controller 的代码,改为重定向:
// 登录成功,防止表单重复提交,使用重定向
return "redirect:/main.html";
  • 重启,运行测试:

登录拦截器

  • 发现问题:不用登录,也可以直接登录到后台主页,可以使用拦截器机制,实现登录检查;
  • 自定义一个拦截器:LoginHandlerInterceptor
// 自定义拦截器,需要实现HandlerInterceptor接口
public class LoginHandlerInterceptor implements HandlerInterceptor {
    /*
        如果返回true执行下一个拦截器
        如果返回false就不执行下一个拦截器
     */
    // preHandle:在请求处理的方法之前执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 登录成功后,应该有用户session,获取session进行判断
        Object loginUser = request.getSession().getAttribute("loginUser");
        if (loginUser == null) {
            // 未登录,返回登录页面
            request.setAttribute("msg", "没有权限,请先登录");
            request.getRequestDispatcher("/index.html").forward(request, response);
            return false;
        } else {
            return true;
        }
    }
}
  • 在自定义配置类 MyMvcConfig 中,自定义拦截器:
// 自定义拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
    // 注册拦截器,及拦截请求和要剔除哪些请求
    // 需要过滤静态资源文件,否则样式无法显示
    registry.addInterceptor(new LoginHandlerInterceptor())
            // 拦截所有请求
            .addPathPatterns("/**")
            // 过滤静态资源文件
            .excludePathPatterns("/index.html", "/", "/user/login", "/css/**","/js/**","/img/**");
}
  • 重启运行,登录测试拦截器;

  • 在后台主页,获取用户登录的信息:

<a class="navbar-brand col-sm-3 col-md-2 mr-0" href="" th:text="${session.loginUser}"></a>
  • 测试:

7.4 员工列表

RestFul 风格

  • 使用 Restful 风格,实现 CRUD 操作:
功能普通CRUD(url 来区分操作)Restful CRUD
查询getEmpemp --> GET
添加addEmp?xxxemp --> POST
修改updateEmp?id=xxx&xxx=xxemp/{id} --> PUT
删除deleteEmp?id=1emp/{id} --> DELETE
  • 具体功能对应的请求方式:
项目功能请求URL请求方式
查询所有员工empsGET
查询某个员工(跳转到修改页面)emp/1GET
跳转到添加页面empGET
添加员工empPOST
跳转到修改页面(查出员工,进行信息回显)emp/1GET
修改员工empPUT
删除员工emp/1DELETE
  • Tomcat 8.5 以上版本,不支持 put 和 delete 请求,如需开启需要配置:
    • 修改配置文件:application.properties
# 开启支持 put 和 delete 请求
spring.mvc.hiddenmethod.filter.enabled=true
  • 并在表单中添加隐藏域:
<input type="hidden" name="_method" value="put"/>
  • 注解方式:@PutMapping@DeleteMapping
  • 建议使用 POST @PostMapping、GET @GetMapping,两种方式;

员工列表页面跳转

  • 将后台首页的侧边栏 Customers 改为员工管理;
  • a 链接添加请求:
<a class="nav-link" th:href="@{/emps}">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
         fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
         stroke-linejoin="round" class="feather feather-users">
        <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
        <circle cx="9" cy="7" r="4"></circle>
        <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
        <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
    </svg>
    员工管理
</a>
  • 创建 emp 文件夹,将 list.html 放入文件夹内:

  • 编写处理请求的 Controller:

@Controller
public class EmployeeController {
    // 未创建server层,所以直接调用Dao层
    @Autowired
    EmployeeDao employeeDao;

    // 查询所有员工,返回列表页面
    @GetMapping("/emps")
    public String list(Model model) {
        Collection<Employee> employees = employeeDao.getAll();
        // 将结果放在请求中,传递给前端
        model.addAttribute("emps", employees);
        return "emp/list";
    }
}
  • 启动项目测试:

  • 跳转成功,然后,只需要将数据渲染进去即可;

Thymeleaf 公共页面元素抽取

  • 发现页面侧边栏和顶部都相同,可以抽取出公共页面元素;

  • 步骤:

    • 抽取公共片段 th:fragment 定义模板;
      • 如:th:fragment="topbar"
    • 引入公共片段 :
      • th:insert 插入模板;
      • th:replace 替换模板(推荐);
      • th:include 插入内容(不推荐);
    • 模板名:使用 thymeleaf 的前后缀配置规则,进行解析;
    • 表达式:~{/目录/模板名::标签名},如:th:replace="~{/commons/commons::topbar}"
    • 如果要传递参数,可以直接使用 (),接收判断即可,如:th:replace="~{/commons/commons::sidebar(active='list')}"
  • 在 templates 下创建公共页面文件夹 commons,在里面创建存放公共内容的页面:commons.html

<!--抽取出的公共页面内容-->
<!DOCTYPE html>
<!--解决idea thymeleaf 表达式模板报红波浪线-->
<!--suppress ALL-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!--头部导航栏-->
<!--th:fragment="topbar" 定义模板-->
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar">
    <!--th:text="${session.loginUser}" 显示登录用户的信息-->
    <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="" th:text="${session.loginUser}"></a>
    <input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
    <ul class="navbar-nav px-3">
        <li class="nav-item text-nowrap">
            <a class="nav-link" href="">注销</a>
        </li>
    </ul>
</nav>
<!--侧边栏-->
<nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar">
    <div class="sidebar-sticky">
        <ul class="nav flex-column">
            <li class="nav-item">
                <a class="nav-link active" th:href="@{/main.html}">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
                         fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
                         stroke-linejoin="round" class="feather feather-home">
                        <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
                        <polyline points="9 22 9 12 15 12 15 22"></polyline>
                    </svg>
                    首页 <span class="sr-only">(current)</span>
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" th:href="@{/emps}">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
                         fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
                         stroke-linejoin="round" class="feather feather-users">
                        <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
                        <circle cx="9" cy="7" r="4"></circle>
                        <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
                        <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
                    </svg>
                    员工管理
                </a>
            </li>
            <!--其它内容...-->
        </ul>
    </div>
</nav>
</html>
  • dashboard.htmllist.html 页面中,删除公共内容,引入抽取的模板:
    • th:replace="~{/commons/commons::topbar}"
<body>
<!--引入头部导航栏 th:insert 方式会显示div标签  th:replace 方式,不显示div标签-->
<div th:insert="~{/commons/commons::topbar}"></div>
<!--<div th:replace="~{/commons/commons::topbar}"></div>-->
<div class="container-fluid">
    <div class="row">
        <!--引入侧边栏-->
        <div th:replace="~{/commons/commons::sidebar}"></div>
        <!--内容部分-->
        <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
        <!--...-->
        </main>
    </div>
</div>
</body>
  • 运行测试:

  • 说明:

    • 引入公共片段的三种方式:
      • insert 插入;
      • replace 替换
      • include 包含;
    • 建议使用 replace 替换,可以解决 div 多余的问题

侧边栏激活

  • 在 a 标签中增加判断,通过接收的参数改变 class 标签的值;
  • 修改公共页面:commons.html
<!--首页:侧边栏激活-->
<a th:class="${active=='main'}?'nav-link active':'nav-link'" th:href="@{/main.html}">...首页</a>
...
<!--员工管理:侧边栏激活-->
<a th:class="${active=='emps'}?'nav-link active':'nav-link'" th:href="@{/emps}">...员工管理</a>
  • 修改后台主页面:dashboard.html
<!--引入侧边栏:传递参数active='main',前端接收判断-->
<div th:replace="~{/commons/commons::sidebar(active='main')}"></div>
  • 修改员工列表页面:list.html
<!--引入侧边栏:传递参数active='emps',前端接收判断-->
<div th:replace="~{/commons/commons::sidebar(active='emps')}"></div>
  • 运行测试:

员工信息页面展示

  • 修改页面:遍历员工信息,增加功能按钮:list.html
<!--内容部分-->
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <h2>员工列表 <button class="btn btn-sm btn-success">添加员工</button></h2>
    <div class="table-responsive">
        <table class="table table-striped table-sm">
            <thead>
            <tr>
                <th>id</th>
                <th>lastName</th>
                <th>email</th>
                <th>gender</th>
                <th>department</th>
                <th>birth</th>
                <th>操作</th>
            </tr>
            </thead>
            <tbody>
            <tr th:each="emp:${emps}">
                <!--<td th:text="${emp.getId()}"></td>-->
                <td th:text="${emp.id}"></td>
                <td th:text="${emp.lastName}"></td>
                <td th:text="${emp.email}"></td>
                <td th:text="${emp.gender==0?'女':'男'}"></td>
                <td th:text="${emp.department.departmentName}"></td>
                <!--#dates.format:日期格式化-->
                <td th:text="${#dates.format(emp.birth, 'yyyy-MM-dd HH:mm')}"></td>
                <td>
                    <button class="btn btn-sm btn-primary">编辑</button>
                    <button class="btn btn-sm btn-danger">删除</button>
                </td>
            </tr>
            </tbody>
        </table>
    </div>
</main>
  • 运行测试:

7.5 添加员工

  • 实现步骤:
    • 按钮提交;
    • 跳转到添加页面;
    • 添加员工成功;
    • 返回首页;

表单及细节优化

  • 将添加员工按钮,改为超链接,并添加链接请求:list.html
<h2>员工列表 <a class="btn btn-sm btn-success" th:href="@{/emp}">添加员工</a></h2>
  • 编写对应的 Controller:EmployeeController
// 跳转到员工添加页面
@GetMapping("/emp")
public String toAddPage() {
    return "emp/add";
}
  • 添加前端页面:add.html(复制 list.html,修改)
<!--内容部分-->
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <h2>添加员工</h2>
    <div class="table-responsive">
        <form class="col-4">
            <div class="form-group">
                <label>LastName</label>
                <input type="text" name="lastName" class="form-control" placeholder="userName">
            </div>
            <div class="form-group">
                <label>Email</label>
                <input type="email" name="email" class="form-control" placeholder="aa@bb.com">
            </div>
            <div class="form-group">
                <label>Gender</label><br/>
                <div class="form-check form-check-inline">
                    <input class="form-check-input" type="radio" name="gender" value="1">
                    <label class="form-check-label"></label>
                </div>
                <div class="form-check form-check-inline">
                    <input class="form-check-input" type="radio" name="gender" value="0">
                    <label class="form-check-label"></label>
                </div>
            </div>
            <div class="form-group">
                <label>department</label>
                <select name="department" class="form-control">
                    <option>1</option>
                    <option>2</option>
                    <option>3</option>
                    <option>4</option>
                    <option>5</option>
                </select>
            </div>
            <div class="form-group">
                <label>Birth</label>
                <input type="text" name="birth" class="form-control" placeholder="2022-01-01">
            </div>
            <button type="submit" class="btn btn-primary">添加</button>
        </form>
    </div>
</main>
  • 部门信息下拉框,应该关联部门表的数据,所以需要在跳转到添加页面的同时,获取部门信息,修改后端的 Controller:
// 自动装配置
@Autowired
DepartmentDao departmentDao;

// 跳转到员工添加页面
@GetMapping("/emp")
public String toAddPage(Model model) {
    // 获取所有的部门
    Collection<Department> departments = departmentDao.getDepartments();
    // 数据返回到前端
    model.addAttribute("departments", departments);
    return "emp/add";
}
  • 修改前端页面:add.html
<!--部门下拉框-->
<div class="form-group">
    <label>department</label>
    <!--department.id:提交的是属性,部门的id,不是对象-->
    <select name="department.id" class="form-control">
        <option th:each="dept:${departments}" th:text="${dept.departmentName}"
                th:value="${dept.id}"></option>
    </select>
</div>
  • 修改了 Controller,重启项目测试

具体实现添加功能

  • 修改 form 表单提交地址和方式:add.html
<!--Restful风格,请求地址相同,功能不同-->
<form class="col-4" th:action="@{/emp}" method="post">
  • 创建 Controller:
    • 接收前端传过来的属性,将它封装成为对象;
    • 前端页面需要通过 name 属性传递到后端;
// 添加员工,使用post接收,相同请求地址,功能不同
// 接收前端传递的参数,自动封装成为对象[要求前端传递的参数名,和属性名一致]
@PostMapping("/emp")
public String addEmp(Employee employee) {
    System.out.println(employee);
    // 保存员工信息
    employeeDao.save(employee);
    // 回到员工列表页面,可以使用redirect或者forward,就不会被视图解析器解析
    return "redirect:/emps";
}

回顾:重定向、转发以及路径 / 的问题:

  • 查看视图解析器:ThymeleafReactiveViewResolver
// 包含了重定向和转发对就的规则
public static final String REDIRECT_URL_PREFIX = "redirect:";

public static final String FORWARD_URL_PREFIX = "forward:";

@Override
public Mono<View> resolveViewName(final String viewName, final Locale locale) {

    // First possible call to check "viewNames": before processing redirects and forwards
    if (!this.alwaysProcessRedirectAndForward && !canHandle(viewName, locale)) {
        vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafReactiveViewResolver. Passing on to the next resolver in the chain.", viewName);
        return Mono.empty();
    }
    // Process redirects (HTTP redirects)
    if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
        vrlogger.trace("[THYMELEAF] View \"{}\" is a redirect, and will not be handled directly by ThymeleafReactiveViewResolver.", viewName);
        final String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
        final RedirectView view = this.redirectViewProvider.apply(redirectUrl);
        final RedirectView initializedView =
                (RedirectView) getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, REDIRECT_URL_PREFIX);
        return Mono.just(initializedView);
    }
    // Process forwards (to JSP resources)
    if (viewName.startsWith(FORWARD_URL_PREFIX)) {
        vrlogger.trace("[THYMELEAF] View \"{}\" is a forward, and will not be handled directly by ThymeleafReactiveViewResolver.", viewName);
        // TODO * No view forwarding in Spring WebFlux yet. See https://jira.spring.io/browse/SPR-14537
        return Mono.error(new UnsupportedOperationException("Forwards are not currently supported by ThymeleafReactiveViewResolver"));
    }
    // Second possible call to check "viewNames": after processing redirects and forwards
    if (this.alwaysProcessRedirectAndForward && !canHandle(viewName, locale)) {
        vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafReactiveViewResolver. Passing on to the next resolver in the chain.", viewName);
        return Mono.empty();
    }
    vrlogger.trace("[THYMELEAF] View {} will be handled by ThymeleafReactiveViewResolver and a " +
            "{} instance will be created for it", viewName, getViewClass().getSimpleName());
    return loadView(viewName, locale);

}

启动测试

  • 前端填写数据,注意时间问题:

  • 点击添加,后台输出正常,页面跳转及数据显示正常:

  • 更换时间格式提交:

  • 提交,发现页面出现了400错误:

  • SpringMVC 会将页面提交的值,转换为指定的类型,默认日期,是按照 / 的方式提交,如:2019/01/01 转换为一个 date 对象;

  • 查看自动配置文件,找到日期格式化的方法:

# 修改日期格式
spring.mvc.format.date=yyyy-MM-dd
  • 修改后支持 - 的格式,不在支持 / 格式;

7.6 修改员工信息

  • 逻辑分析:
    • 点击修改按钮,跳转到编辑页面,可以直接使用添加员工的页面实现;
    • 显示原数据,修改完毕后,跳回列表页面;

实现跳转到修改页面

  • 修改跳转链接的位置:list.html
<!--Thymeleaf的Restful表示方式-->
<!--<a class="btn btn-sm btn-primary" th:href="@{'/emp/'+${emp.id}}">编辑</a>-->
<a class="btn btn-sm btn-primary" th:href="@{/emp/{id}(id=${emp.id})}">编辑</a>
  • 创建对应的 Controller:跳转到修改页面
 // 跳转到员工修改页面
@GetMapping("/emp/{id}")
// @PathVariable注解:Restful 绑定传过来的值,到方法的参数
public String toUpdateEmp(@PathVariable("id") Integer id, Model model) {
    // 根据id查询员工
    Employee employee = employeeDao.getEmployeeById(id);
    // 将员工信息返回页面
    model.addAttribute("emp", employee);
    // 查出所有的部门,提供修改选择
    Collection<Department> departments = departmentDao.getDepartments();
    // 将部门信息返回前端
    model.addAttribute("departments", departments);
    return "emp/update";
}
  • 创建对应的前端页面:update.html
    • 复制 add.html 修改,将后台查询数据,进行页面回显;
<!--内容部分-->
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <h2>修改员工信息</h2>
    <div class="table-responsive">
        <!--Restful风格,请求地址相同,功能不同-->
        <form class="col-4" th:action="@{/updateEmp}" method="post">
            <!--隐藏输入框:存放当前id的值-->
            <input type="hidden" name="id" th:value="${emp.id}">
            <div class="form-group">
                <label>LastName</label>
                <input type="text" name="lastName" class="form-control" th:value="${emp.lastName}">
            </div>
            <div class="form-group">
                <label>Email</label>
                <input type="email" name="email" class="form-control" th:value="${emp.email}">
            </div>
            <div class="form-group">
                <label>Gender</label><br/>
                <div class="form-check form-check-inline">
                    <input class="form-check-input" type="radio" name="gender" value="1"
                           th:checked="${emp.gender==1}">
                    <label class="form-check-label"></label>
                </div>
                <div class="form-check form-check-inline">
                    <input class="form-check-input" type="radio" name="gender" value="0"
                           th:checked="${emp.gender==0}">
                    <label class="form-check-label"></label>
                </div>
            </div>
            <!--部门下拉框-->
            <div class="form-group">
                <label>department</label>
                <!--department.id:提交的是属性,部门的id,不是对象-->
                <select name="department.id" class="form-control">
                    <option th:selected="${dept.id==emp.department.id}" th:each="dept:${departments}"
                            th:text="${dept.departmentName}"
                            th:value="${dept.id}"></option>
                </select>
            </div>
            <div class="form-group">
                <label>Birth</label>
                <!--#dates.format:日期格式化-->
                <input th:value="${#dates.format(emp.birth,'yyyy-MM-dd HH:mm')}" type="text" name="birth"
                       class="form-control">
            </div>
            <button type="submit" class="btn btn-primary">修改</button>
        </form>
    </div>
</main>

注意:

  • 需要对日期,进行格式化:
<!--#dates.format:日期格式化-->
<input th:value="${#dates.format(emp.birth,'yyyy-MM-dd HH:mm')}" type="text" name="birth" class="form-control">
  • 创建隐藏域,存储当前 id 值:
<!--隐藏输入框:存放当前id的值-->
<input type="hidden" name="id" th:value="${emp.id}">
  • 修改表单提交地址:
<form class="col-4" th:action="@{/updateEmp}" method="post">

修改保存

  • 创建对应的 Controller:保存信息,并跳转到列表页面
// 修改员工信息
@PostMapping("/updateEmp")
public String updateEmp(Employee employee) {
    // 保存员工信息
    employeeDao.save(employee);
    // 回到员工列表页面
    return "redirect:/emps";
}
  • 运行测试:

7.7 删除员工

  • 修改列表页面,添加删除的提交地址:list.html
<!--不能使用button,需要改为a标签-->
<a class="btn btn-sm btn-danger" th:href="@{/delEmp/{id}(id=${emp.id})}">删除</a>
  • 创建对应的 Controller:
// 删除员工
@GetMapping("/delEmp/{id}")
public String delete(@PathVariable("id") Integer id) {
    employeeDao.delete(id);
    // 回到员工列表页面
    return "redirect:/emps";
}
  • 运行测试:

7.8 404 及注销

404

  • 只需要在模板目录下,添加一个 error 文件夹,文件夹中存放相应的错误页面,如 404.html 等,Spring Boot 就会帮自动使用了:

  • 运行测试:

注销

  • 修改页面,添加注销请求:提取的公共导航栏 commons.html
<a class="nav-link" th:href="@{/user/loginOut}">注销</a>
  • 创建对应的 Controller:LoginController
// 注销
@RequestMapping("/user/loginOut")
public String loginOut(HttpSession session){
    // 注销session
    session.invalidate();
    return "redirect:/index.html";
}

7.9 定制错误数据

Spring Boot 默认的错误处理机制

  • 浏览器访问,默认的错误处理效果:

  • 如果是其他客户端,默认响应一个 json 数据:

错误处理原理分析

  • 错误处理的自动配置类:ErrorMvcAutoConfiguration;
  • 里面注入了几个很重要的 bean:
    • DefaultErrorAttributes;
    • BasicErrorController;
    • ErrorPageCustomizer;
    • DefaultErrorViewResolver;

错误处理步骤:

  • 一旦系统出现了 4xx 或者 5xx 之类的错误,ErrorPageCustomizer 就会生效(定制错误的响应规则):
@Bean
public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
    return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}
  • 进入 ErrorPageCustomizer 类中,发现一个方法 registerErrorPages 注册错误页面:
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
    ErrorPage errorPage = new ErrorPage(
            this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
    errorPageRegistry.addErrorPages(errorPage);
}
  • 进入 getPath() 方法:
public String getPath() {
    return this.path;
}
  • 点击 path 查看:
// 错误控制器的路径:会在这个目录下,查找对应的错误文件
@Value("${error.path:/error}")
private String path = "/error";
  • 系统一旦出现错误,就会来到 /error 请求进行处理,这个请求会被 BasicErrorController 处理,查看 BasicErrorController 类:
@Controller
// 处理默认的 /error 请求
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
// ...
}
  • 类有两个方法分别对应 html 和 json 两种类型:
// 产生html类型的数据,浏览器发送的请求会被这个方法处理
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
    HttpStatus status = getStatus(request);
    Map<String, Object> model = Collections
            .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
    response.setStatus(status.value());
    // resolveErrorView方法:获取错误页面
    ModelAndView modelAndView = resolveErrorView(request, response, status, model);
    return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

// 返回json类型的数据,其他的客户端请求会被这个方法处理
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
    HttpStatus status = getStatus(request);
    if (status == HttpStatus.NO_CONTENT) {
        return new ResponseEntity<>(status);
    }
    Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
    return new ResponseEntity<>(body, status);
}
  • 点击进入 resolveErrorView 方法:html 获取错误页面
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
    // 获取所有的errorViewResolvers错误视图解析器
    for (ErrorViewResolver resolver : this.errorViewResolvers) {
        ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
        if (modelAndView != null) {
            return modelAndView;
        }
    }
    return null;
}
  • 查看默认的错误视图解析器:DefaultErrorViewResolver
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {

	private static final Map<Series, String> SERIES_VIEWS;

	static {
		Map<Series, String> views = new EnumMap<>(Series.class);
        // 客户端错误
		views.put(Series.CLIENT_ERROR, "4xx");
        // 服务端错误
		views.put(Series.SERVER_ERROR, "5xx");
		SERIES_VIEWS = Collections.unmodifiableMap(views);
	}
	// ...
	// HttpStatus 状态码
    @Override
	public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
		ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
		if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
            // 通过状态码解析视图
			modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
		}
		return modelAndView;
	}

    // 去error路径下解析视图
	private ModelAndView resolve(String viewName, Map<String, Object> model) {
        // 比如 error/404 error/500
		String errorViewName = "error/" + viewName;
		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
				this.applicationContext);
		if (provider != null) {
			return new ModelAndView(errorViewName, model);
		}
		return resolveResource(errorViewName, model);
	}
}

总结:

  • 通过上面分析可知,定制错误页面,可以建立一个 error 目录,然后放入对应的错误码 html 文件,如:404.html、500.html、4xx.html、5xx.html 等;

  • 在 DefaultErrorAttributes 中,可以获取页面的信息数据,里面有很多的 addxxx 方法,就是添加不同的信息:

    • addStatus;
    • addErrorDetails;
    • addErrorMessage;
    • addStackTrace;
    • addPath;
  • 里面存了相关的错误信息,可以在错误页面直接取出来;

八、Spring Boot 数据库操作

九、Spring Boot 安全框架

十、Swagger

十一、任务

11.1 异步任务

  • 异步处理:比如在网站上发送邮件,后台会去发送邮件,此时前台会造成响应不动,直到邮件发送完毕,响应才会成功,所以一般会采用多线程的方式,去处理这些任务;

搭建测试环境

  • 创建空的 Maven 项目,并删除 src 目录;
  • 在新建的空 Maven 项目中,创建 Spring Boot 模块,选择 web,删除多余文件;
  • 新建 service 包,在包下创建 AsyncService 类:
@Service
public class AsyncService {
    // 模拟正在处理数据的方法
    public void hello() {
        try {
            // 使用线程设置延时3秒,模拟同步等待的情况
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("数据处理中...");
    }
}
  • 新建 controller 包,在包下创建 AsyncController 类:
@RestController
public class AsyncController {
    @Autowired
    AsyncService asyncService;

    @RequestMapping("/hello")
    public String hello() {
        // 调用asyncService方法,停止3秒(浏览器会转圈)
        asyncService.hello();
        return "OK";
    }
}
  • 运行测试:访问 http://localhost:8080/hello 3 秒后,出现 OK,这是同步等待的情况;

  • 问题:用户不需等待,直接得到消息的实现方式:

    • 可以在后台,使用多线程的方式进行处理,但是每次都需要手动去编写多线程,这样太麻烦;
    • Spring Boot 中,只需要在方法上,加一个简单的注解即可实现;

Spring Boot 实现异步处理

  • @Async 用于异步处理,作用于方法上,Spring Boot 会自己开一个线程池,进行调用,但是要让 @Async 注解生效,还需要在主程序上,添加一个注解 @EnableAsync,开启异步注解功能;
  • @EnableAsync 作用于主类上;
  • 给 hello 方法,添加 @Async 注解:
@Service
public class AsyncService {
    // 模拟正在处理数据的方法
    // @Async:告诉Spring这是一个异步方法
    @Async
    public void hello() {
        try {
            // 使用线程设置延时3秒,模拟同步等待的情况
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("数据处理中...");
    }
}
  • 在程序主类上,开启异步注解功能:
@SpringBootApplication
// 开启异步注解功能
@EnableAsync
public class SpringbootAsyncApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootAsyncApplication.class, args);
    }
}
  • 重启测试,网页瞬间响应,后台代码依旧执行;

11.2 邮件任务

Spring Boot 邮件任务

  • 邮件发送需要引入 spring-boot-start-mail;
  • SpringBoot 自动配置 MailSenderAutoConfiguration;
  • 定义 MailProperties 内容,配置在 application.yml 中;
  • 自动装配 JavaMailSender;

配置环境

  • 创建 Spring Boot 模块,删除多余文件;
  • 引入依赖:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
    <version>2.6.7</version>
</dependency>
  • 查看 spring-boot-starter-mail,可以看到引入了 jakarta.mail 依赖:
<dependency>
    <groupId>com.sun.mail</groupId>
    <artifactId>jakarta.mail</artifactId>
    <version>1.6.7</version>
    <scope>compile</scope>
</dependency>
  • 查看自动配置类(双击 shift):MailSenderAutoConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ MimeMessage.class, MimeType.class, MailSender.class })
@ConditionalOnMissingBean(MailSender.class)
@Conditional(MailSenderCondition.class)
// MailProperties:配置文件
@EnableConfigurationProperties(MailProperties.class)
@Import({ MailSenderJndiConfiguration.class, MailSenderPropertiesConfiguration.class })
public class MailSenderAutoConfiguration {
    //...
}
  • 这个类中没有注册 bean,看这个类导入的其它类 MailSenderJndiConfiguration.class
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Session.class)
@ConditionalOnProperty(prefix = "spring.mail", name = "jndi-name")
@ConditionalOnJndi
class MailSenderJndiConfiguration {

	private final MailProperties properties;

	MailSenderJndiConfiguration(MailProperties properties) {
		this.properties = properties;
	}

	@Bean
    // 邮件发送的实现类
	JavaMailSenderImpl mailSender(Session session) {
		JavaMailSenderImpl sender = new JavaMailSenderImpl();
		sender.setDefaultEncoding(this.properties.getDefaultEncoding().name());
		sender.setSession(session);
		return sender;
	}
	
	// ...
}
  • 查看配置文件:MailProperties
@ConfigurationProperties(prefix = "spring.mail")
public class MailProperties {

	private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

	/**
	 * SMTP server host. For instance, 'smtp.example.com'.
	 */
	private String host;

	/**
	 * SMTP server port.
	 */
	private Integer port;

	/**
	 * Login user of the SMTP server.
	 */
	private String username;

	/**
	 * Login password of the SMTP server.
	 */
	private String password;

	/**
	 * Protocol used by the SMTP server.
	 */
	private String protocol = "smtp";

	/**
	 * Default MimeMessage encoding.
	 */
	private Charset defaultEncoding = DEFAULT_CHARSET;

	/**
	 * Additional JavaMail Session properties.
	 */
	private Map<String, String> properties = new HashMap<>();

	/**
	 * Session JNDI name. When set, takes precedence over other Session settings.
	 */
	private String jndiName;
	// ...
}	
  • 编写配置文件:application.propertiesyaml
spring.mail.username=邮箱用户名
spring.mail.password=邮箱密码
spring.mail.host=smtp服务器地址
# qq需要配置ssl
# spring.mail.properties.mail.smtp.ssl.enable=true
  • Spring Boot 单元测试:
@SpringBootTest
class SpringbootMailApplicationTests {
    @Autowired
    // JavaMailSenderImpl:邮件发送的实现类
    JavaMailSenderImpl mailSender;

    @Test
    void contextLoads() {
        // 邮件设置1:一个简单的邮件
        SimpleMailMessage mailMessage = new SimpleMailMessage();
        // 邮件主题
        mailMessage.setSubject("springboot 测试邮件主题");
        // 邮件正文
        mailMessage.setText("测试邮件正文");
        // 收件邮箱
        mailMessage.setTo("收件邮箱");
        // 发件邮箱
        mailMessage.setFrom("发件邮箱");
        mailSender.send(mailMessage);
    }
    
    @Test
    void contextLoads2() throws MessagingException {
        // 邮件设置2:一个复杂的邮件
        MimeMessage mimeMessage = mailSender.createMimeMessage();
        // 组装
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
        helper.setSubject("复杂邮件主题测试");
        // true:输出html
        helper.setText("<p style='color:red'>复杂邮件,正文</p>", true);

        // 发送附件
        helper.addAttachment("1.png", new File("附件路径"));
        helper.addAttachment("1.txt", new File("附件路径"));

        helper.setTo("收件邮箱");
        helper.setFrom("发件邮箱");
        mailSender.send(mimeMessage);
    }
}
  • 封装一个邮件方法:
/**
 * 封装一个邮件发送方法
 * @param html 是否显示 html
 * @param subject 邮件主题
 * @param text 邮件正文
 * @param fileName 附件名称
 * @param fileUrl 附件地址
 * @param mailTo 收件邮箱
 * @param mailFrom 发件邮箱
 * @throws MessagingException
 */
public void sendMail(Boolean html, String subject, String text,
                     String fileName, String fileUrl, String mailTo, String mailFrom) throws MessagingException {
    // 邮件设置2:一个复杂的邮件
    MimeMessage mimeMessage = mailSender.createMimeMessage();
    // 组装
    MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
    helper.setSubject(subject);
    // true:输出html
    helper.setText(text, html);

    // 发送附件
    helper.addAttachment(fileName, new File(fileUrl));

    helper.setTo(mailTo);
    helper.setFrom(mailFrom);
    mailSender.send(mimeMessage);
}

11.3 定时任务

  • 项目开发中,经常需要执行一些定时任务,比如:需要在每天凌晨时,分析一次前一天的日志信息,Spring Boot 提供了异步执行任务调度的方式,提供了两个接口:

    • TaskExecutor 接口:任务调度者;
    • TaskScheduler 接口:任务执行者;
  • 两个注解:

    • @EnableScheduling:开启定时功能的注解(主程序类上);
    • @Scheduled:设置执行的时间(方法上);
  • cron 表达式:在线生成

  • 特殊字符含义:

定时任务测试

  • 创建测试类:ScheduledService
@Service
public class ScheduledService {
    // 秒 分 时 日 月 周几
    // 0 05/1 18 * * ?:18:05开始,每隔一分钟,执行一次
    @Scheduled(cron = "0 05/1 18 * * ?")
    public void hello() {
        System.out.println("定时任务执行...");
    }
}
  • 在主程序上,增加 @EnableScheduling,开启定时任务功能:
@SpringBootApplication
// @EnableScheduling:主程序类上开启定时功能的注解
@EnableScheduling
public class SpringbootMailApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootMailApplication.class, args);
    }
}
  • 重启程序,运行测试;

常用表达式例子

1. 0/2 * * * * ?   表示每2秒 执行任务
2. 0 0/2 * * * ?    表示每2分钟 执行任务
3. 0 0 2 1 * ?   表示在每月的1日的凌晨2点调整任务
4. 0 15 10 ? * MON-FRI   表示周一到周五每天上午10:15执行作业
5. 0 15 10 ? 6L 2002-2006   表示2002-2006年的每个月的最后一个星期五上午10:15执行作
6. 0 0 10,14,16 * * ?   每天上午10点,下午2点,4点 
7. 0 0/30 9-17 * * ?   朝九晚五工作时间内每半小时 
8. 0 0 12 ? * WED    表示每个星期三中午12点 
9. 0 0 12 * * ?   每天中午12点触发 
10. 0 15 10 ? * *    每天上午10:15触发 
11. 0 15 10 * * ?     每天上午10:15触发 
12. 0 15 10 * * ?    每天上午10:15触发 
13. 0 15 10 * * ? 2005    2005年的每天上午10:15触发 
14. 0 * 14 * * ?     在每天下午2点到下午2:59期间的每1分钟触发 
15. 0 0/5 14 * * ?    在每天下午2点到下午2:55期间的每5分钟触发 
16. 0 0/5 14,18 * * ?     在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 
17. 0 0-5 14 * * ?    在每天下午2点到下午2:05期间的每1分钟触发 
18. 0 10,44 14 ? 3 WED    每年三月的星期三的下午2:10和2:44触发 
19. 0 15 10 ? * MON-FRI    周一至周五的上午10:15触发 
20. 0 15 10 15 * ?    每月15日上午10:15触发 
21. 0 15 10 L * ?    每月最后一日的上午10:15触发 
22. 0 15 10 ? * 6L    每月的最后一个星期五上午10:15触发 
23. 0 15 10 ? * 6L 2002-2005   2002年至2005年的每月的最后一个星期五上午10:15触发 
24. 0 15 10 ? * 6#3   每月的第三个星期五上午10:15触发 

十二、Spring Boot 整合 Dubbo 3+Zookeeper

十三、Spring Boot 集成 Redis