第六章 单一架构案例

一、创建工程,引入依赖

1、架构

①架构的概念

『架构』其实就是『项目的结构』,只是因为架构是一个更大的词,通常用来形容比较大规模事物的结构。

②单一架构

单一架构也叫『all-in-one』结构,就是所有代码、配置文件、各种资源都在同一个工程。

  • 一个项目包含一个工程
  • 导出一个 war 包
  • 放在一个 Tomcat 上运行

    2、创建工程

    image.png

    3、引入依赖

    ①搜索依赖信息的网站

    [1]到哪儿找?

    https://mvnrepository.com/

    [2]怎么选择?
  • 确定技术选型:确定我们项目中要使用哪些技术

  • 到 mvnrepository 网站搜索具体技术对应的具体依赖信息

Maven案例 - 图2

  • 确定这个技术使用哪个版本的依赖
    • 考虑因素1:看是否有别的技术要求这里必须用某一个版本
    • 考虑因素2:如果没有硬性要求,那么选择较高版本或下载量大的版本

image.png
image.png

  • 在实际使用中检验所有依赖信息是否都正常可用

    确定技术选型、组建依赖列表、项目划分模块……等等这些操作其实都属于架构设计的范畴。

    • 项目本身所属行业的基本特点
    • 项目具体的功能需求
    • 项目预计访问压力程度
    • 项目预计将来需要扩展的功能
    • 设计项目总体的体系结构

②持久化层所需依赖

  • mysql:mysql-connector-java:5.1.37
  • com.alibaba:druid:1.2.8
  • commons-dbutils:commons-dbutils:1.6

    ③表述层所需依赖

  • javax.servlet:javax.servlet-api:3.1.0

  • org.thymeleaf:thymeleaf:3.0.11.RELEASE

    ④辅助功能所需依赖

  • junit:junit:4.12

  • ch.qos.logback:logback-classic:1.2.3

    ⑤最终完整依赖信息

    1. <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
    2. <dependency>
    3. <groupId>mysql</groupId>
    4. <artifactId>mysql-connector-java</artifactId>
    5. <version>5.1.37</version>
    6. </dependency>
    7. <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
    8. <dependency>
    9. <groupId>com.alibaba</groupId>
    10. <artifactId>druid</artifactId>
    11. <version>1.2.8</version>
    12. </dependency>
    13. <!-- https://mvnrepository.com/artifact/commons-dbutils/commons-dbutils -->
    14. <dependency>
    15. <groupId>commons-dbutils</groupId>
    16. <artifactId>commons-dbutils</artifactId>
    17. <version>1.6</version>
    18. </dependency>
    19. <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
    20. <dependency>
    21. <groupId>javax.servlet</groupId>
    22. <artifactId>javax.servlet-api</artifactId>
    23. <version>3.1.0</version>
    24. <scope>provided</scope>
    25. </dependency>
    26. <!-- https://mvnrepository.com/artifact/org.thymeleaf/thymeleaf -->
    27. <dependency>
    28. <groupId>org.thymeleaf</groupId>
    29. <artifactId>thymeleaf</artifactId>
    30. <version>3.0.11.RELEASE</version>
    31. </dependency>
    32. <!-- https://mvnrepository.com/artifact/junit/junit -->
    33. <dependency>
    34. <groupId>junit</groupId>
    35. <artifactId>junit</artifactId>
    36. <version>4.12</version>
    37. <scope>test</scope>
    38. </dependency>
    39. <!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
    40. <dependency>
    41. <groupId>ch.qos.logback</groupId>
    42. <artifactId>logback-classic</artifactId>
    43. <version>1.2.3</version>
    44. <scope>test</scope>
    45. </dependency>

    4、建包

    | package 功能 | package 名称 | | —- | —- | | 主包 | com.atguigu.imperial.court | | 子包[实体类] | com.atguigu.imperial.court.entity | | 子包[Servlet基类包] | com.atguigu.imperial.court.servlet.base | | 子包[Servlet模块包] | com.atguigu.imperial.court.servlet.module | | 子包[Service接口包] | com.atguigu.imperial.court.service.api | | 子包[Service实现类包] | com.atguigu.imperial.court.service.impl | | 子包[Dao接口包] | com.atguigu.imperial.court.dao.api | | 子包[Dao实现类包] | com.atguigu.imperial.court.dao.impl | | 子包[Filter] | com.atguigu.imperial.court.filter | | 子包[异常类包] | com.atguigu.imperial.court.exception | | 子包[工具类] | com.atguigu.imperial.court.util | | 子包[测试类] | com.atguigu.imperial.court.test |

二、搭建环境:持久化层

1、数据建模

①物理建模

  1. create database db_imperial_court;
  2. use db_imperial_court;
  3. create table t_emp
  4. (
  5. emp_id int primary key auto_increment,
  6. emp_name char(100) not null,
  7. emp_position char(100) not null,
  8. login_account char(100) not null unique,
  9. login_password char(100) not null
  10. );
  11. insert into t_emp(emp_name, emp_position, login_account, login_password)
  12. values ('爱新觉罗·玄烨', 'emperor', 'xiaoxuanzi1654', '25325C896624D444B2E241807DCAC98B'), # 16540504
  13. ('纳兰明珠', 'minister', 'brightball1635', 'A580D0EF93C22036C859E194C14CB777'), # 16351119
  14. ('赫舍里·索额图', 'minister', 'tutu1636', 'E40FD7D49B8B7EF46F47407D583C3538'); # 17030921
  15. create table t_memorials
  16. (
  17. memorials_id int primary key auto_increment,
  18. memorials_title char(100) not null,
  19. memorials_content varchar(5000) not null,
  20. memorials_emp int not null,
  21. memorials_create_time char(100),
  22. feedback_time char(100),
  23. feedback_content varchar(1000),
  24. memorials_status int not null
  25. );
  26. insert into t_memorials(memorials_title,
  27. memorials_content,
  28. memorials_emp,
  29. memorials_create_time,
  30. feedback_time,
  31. feedback_content,
  32. memorials_status)
  33. values ('浙江巡抚奏钱塘堤决口疏', '皇上啊,不好啦!钱塘江发大水啦!堤坝冲毁啦!您看这咋弄啊!', 2, '1690-05-07', null, null, 0),
  34. ('左都御史参鳌拜圈地疏', '皇上啊,鳌拜这厮不是东西呀!占老百姓的地哇!还打人呀!您看咋弄啊!', 3, '1690-04-14', null, null, 0),
  35. ('都察院劾吴三桂不臣疏', '皇上啊,不得了啦!吴三桂那孙子想造反呀!', 2, '1693-11-18', null, null, 0),
  36. ('兵部奏准噶尔犯境疏', '皇上啊,不得了啦!葛尔丹要打过来了呀!', 3, '1693-11-18', null, null, 0),
  37. ('朝鲜使臣朝拜事宜呈皇上御览', '皇上啊!朝鲜国的人要来啦!咱们请他们吃猪肉炖粉条子吧!', 2, '1680-06-11', null, null, 0),
  38. ('英吉利炮舰购买事宜疏', '皇上啊!英国的小船船咱们买多少啊?', 3, '1680-06-12', null, null, 0),
  39. ('劾杭州织造贪墨疏', '皇上啊!杭州织造有问题啊!', 2, '1680-06-13', null, null, 0),
  40. ('禀畅春园落成疏', '皇上啊!畅春园修好了哇!您啥时候过来看看呀!', 3, '1680-06-14', null, null, 0),
  41. ('请旨木兰秋狝疏', '皇上啊!秋天到啦,又该打猎啦!', 2, '1680-06-15', null, null, 0),
  42. ('核准西北军饷银两疏', '皇上啊!您看看这钱数算的对不对呀!', 3, '1680-06-16', null, null, 0),
  43. ('请旨裁撤三藩疏', '皇上啊!咱们不裁撤三藩就芭比Q了哇!', 2, '1680-06-17', null, null, 0),
  44. ('蒙古王公进京朝拜疏', '皇上啊!蒙古王公要来啦!咱们请他们吃猪肉炖粉条子吧!', 3, '1680-06-18', null, null, 0),
  45. ('礼部请旨九阿哥赐名疏', '皇上啊!您看九阿哥该叫什么名字呀?', 2, '1680-06-19', null, null, 0),
  46. ('户部尚书请旨告老还乡疏', '皇上啊!臣想回家养老啦!您看看啥时候给臣把俸禄结一下啊!', 3, '1680-06-20', null, null, 0),
  47. ('查江宁织造贪墨疏', '皇上啊!江宁织造有问题啊!', 2, '1680-06-21', null, null, 0)
  48. ;

②逻辑建模

[1] Emp 实体类
  1. public class Emp {
  2. private Integer empId;
  3. private String empName;
  4. private String empPosition;
  5. private String loginAccount;
  6. private String loginPassword;

[2] Memorials 实体类
  1. public class Memorials {
  2. private Integer memorialsId;
  3. private String memorialsTitle;
  4. private String memorialsContent;
  5. // 奏折摘要数据库没有,这里是为了配和页面显示
  6. private String memorialsContentDigest;
  7. private Integer memorialsEmp;
  8. // 员工姓名数据库没有,这里是为了配合页面显示
  9. private String memorialsEmpName;
  10. private String memorialsCreateTime;
  11. private String feedbackTime;
  12. private String feedbackContent;
  13. private Integer memorialsStatus;

2、数据库连接信息

说明:这是我们第一次用到 Maven 约定目录结构中的 resources 目录,这个目录存放各种配置文件。
image.png

  1. driverClassName=com.mysql.jdbc.Driver
  2. url=jdbc:mysql://192.168.198.100:3306/db_imperial_court
  3. username=root
  4. password=atguigu
  5. initialSize=10
  6. maxActive=20
  7. maxWait=10000

3、获取数据库连接

①创建 JDBCUtils 工具类

image.png

②创建 javax.sql.DataSource 对象

  1. // 将数据源对象设置为静态属性,保证大对象的单一实例
  2. private static DataSource dataSource;
  3. static {
  4. // 1.创建一个用于存储外部属性文件信息的Properties对象
  5. Properties properties = new Properties();
  6. // 2.使用当前类的类加载器加载外部属性文件:jdbc.properties
  7. InputStream inputStream = JDBCUtils.class.getClassLoader().getResourceAsStream("jdbc.properties");
  8. try {
  9. // 3.将外部属性文件jdbc.properties中的数据加载到properties对象中
  10. properties.load(inputStream);
  11. // 4.创建数据源对象
  12. dataSource = DruidDataSourceFactory.createDataSource(properties);
  13. } catch (Exception e) {
  14. e.printStackTrace();
  15. }
  16. }

③创建 ThreadLocal 对象

[1]提出需求

(1)在一个方法内控制事务

如果在每一个 Service 方法中都写下面代码,那么代码重复性就太高了:

  1. try{
  2. // 1、获取数据库连接
  3. // 重要:要保证参与事务的多个数据库操作(SQL 语句)使用的是同一个数据库连接
  4. Connection conn = JDBCUtils.getConnection();
  5. // 2、核心操作
  6. // ...
  7. // 3、核心操作成功结束,可以提交事务
  8. conn.commit();
  9. }catch(Exception e){
  10. // 4、核心操作抛出异常,必须回滚事务
  11. conn.rollBack();
  12. }finally{
  13. // 5、释放数据库连接
  14. JDBCUtils.releaseConnection(conn);
  15. }

(2)将重复代码抽取到 Filter

所谓『当前请求覆盖的 Servlet 方法、Service 方法、Dao 方法』其实就是 chain.doFilter(request, response) 间接调用的方法。

  1. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain){
  2. try{
  3. // 1、获取数据库连接
  4. // 重要:要保证参与事务的多个数据库操作(SQL 语句)使用的是同一个数据库连接
  5. Connection conn = JDBCUtils.getConnection();
  6. // 重要操作:关闭自动提交功能
  7. connection.setAutoCommit(false);
  8. // 2、核心操作:通过 chain 对象放行当前请求
  9. // 这样就可以保证当前请求覆盖的 Servlet 方法、Service 方法、Dao 方法都在同一个事务中。
  10. // 同时各个请求都经过这个 Filter,所以当前事务控制的代码在这里只写一遍就行了,
  11. // 避免了代码的冗余。
  12. chain.doFilter(request, response);
  13. // 3、核心操作成功结束,可以提交事务
  14. conn.commit();
  15. }catch(Exception e){
  16. // 4、核心操作抛出异常,必须回滚事务
  17. conn.rollBack();
  18. }finally{
  19. // 5、释放数据库连接
  20. JDBCUtils.releaseConnection(conn);
  21. }
  22. }

(3)数据的跨方法传递

通过 JDBCUtils 工具类获取到的 Connection 对象需要传递给 Dao 方法,让事务涉及到的所有 Dao 方法用的都是同一个 Connection 对象。
但是 Connection 对象无法通过 chain.doFilter() 方法以参数的形式传递过去。
所以从获取到 Connection 对象到使用 Connection 对象中间隔着很多不是我们自己声明的方法——我们无法决定它们的参数。
Maven案例 - 图7

[2] ThreadLocal 对象的功能

Maven案例 - 图8

  • 全类名:java.lang.ThreadLocal
  • 泛型 T:要绑定到当前线程的数据的类型
  • 具体三个主要的方法: | 方法名 | 功能 | | —- | —- | | set(T value) | 将数据绑定到当前线程 | | get() | 从当前线程获取已绑定的数据 | | remove() | 将数据从当前线程移除 |

[3] Java 代码
  1. // 由于 ThreadLocal 对象需要作为绑定数据时 k-v 对中的 key,所以要保证唯一性
  2. // 加 static 声明为静态资源即可保证唯一性
  3. private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();

④声明方法:获取数据库连接

  1. /**
  2. * 工具方法:获取数据库连接并返回
  3. * @return
  4. */
  5. public static Connection getConnection() {
  6. Connection connection = null;
  7. try {
  8. // 1、尝试从当前线程检查是否存在已经绑定的 Connection 对象
  9. connection = threadLocal.get();
  10. // 2、检查 Connection 对象是否为 null
  11. if (connection == null) {
  12. // 3、如果为 null,则从数据源获取数据库连接
  13. connection = dataSource.getConnection();
  14. // 4、获取到数据库连接后绑定到当前线程
  15. threadLocal.set(connection);
  16. }
  17. } catch (SQLException e) {
  18. e.printStackTrace();
  19. // 为了调用工具方法方便,编译时异常不往外抛
  20. // 为了不掩盖问题,捕获到的编译时异常封装为运行时异常抛出
  21. throw new RuntimeException(e);
  22. }
  23. return connection;
  24. }

⑤声明方法:释放数据库连接

  1. /**
  2. * 释放数据库连接
  3. */
  4. public static void releaseConnection(Connection connection) {
  5. if (connection != null) {
  6. try {
  7. // 在数据库连接池中将当前连接对象标记为空闲
  8. connection.close();
  9. // 将当前数据库连接从当前线程上移除
  10. threadLocal.remove();
  11. } catch (SQLException e) {
  12. e.printStackTrace();
  13. throw new RuntimeException(e);
  14. }
  15. }
  16. }

⑥初步测试

image.png

  1. public class ImperialCourtTest {
  2. @Test
  3. public void testGetConnection() {
  4. Connection connection = JDBCUtils.getConnection();
  5. System.out.println("connection = " + connection);
  6. JDBCUtils.releaseConnection(connection);
  7. }
  8. }

⑦完整代码

传送门

4、BaseDao

image.png

①泛型的说明

Maven案例 - 图11

②创建 QueryRunner 对象

  1. // DBUtils 工具包提供的数据库操作对象
  2. private QueryRunner runner = new QueryRunner();

③通用增删改方法

特别说明:在 BaseDao 方法中获取数据库连接但是不做释放,因为我们要在控制事务的 Filter 中统一释放。

  1. /**
  2. * 通用的增删改方法,insert、delete、update 操作都可以用这个方法
  3. * @param sql 执行操作的 SQL 语句
  4. * @param parameters SQL 语句的参数
  5. * @return 受影响的行数
  6. */
  7. public int update(String sql, Object ... parameters) {
  8. try {
  9. Connection connection = JDBCUtils.getConnection();
  10. int affectedRowNumbers = runner.update(connection, sql, parameters);
  11. return affectedRowNumbers;
  12. } catch (SQLException e) {
  13. e.printStackTrace();
  14. // 如果真的抛出异常,则将编译时异常封装为运行时异常抛出
  15. new RuntimeException(e);
  16. return 0;
  17. }
  18. }

④查询单个对象

  1. /**
  2. * 查询单个对象
  3. * @param sql 执行查询的 SQL 语句
  4. * @param entityClass 实体类对应的 Class 对象
  5. * @param parameters 传给 SQL 语句的参数
  6. * @return 查询到的实体类对象
  7. */
  8. public T getSingleBean(String sql, Class<T> entityClass, Object ... parameters) {
  9. try {
  10. // 获取数据库连接
  11. Connection connection = JDBCUtils.getConnection();
  12. return runner.query(connection, sql, new BeanHandler<>(entityClass), parameters);
  13. } catch (SQLException e) {
  14. e.printStackTrace();
  15. // 如果真的抛出异常,则将编译时异常封装为运行时异常抛出
  16. new RuntimeException(e);
  17. }
  18. return null;
  19. }

⑤查询多个对象

  1. /**
  2. * 查询返回多个对象的方法
  3. * @param sql 执行查询操作的 SQL 语句
  4. * @param entityClass 实体类的 Class 对象
  5. * @param parameters SQL 语句的参数
  6. * @return 查询结果
  7. */
  8. public List<T> getBeanList(String sql, Class<T> entityClass, Object ... parameters) {
  9. try {
  10. // 获取数据库连接
  11. Connection connection = JDBCUtils.getConnection();
  12. return runner.query(connection, sql, new BeanListHandler<>(entityClass), parameters);
  13. } catch (SQLException e) {
  14. e.printStackTrace();
  15. // 如果真的抛出异常,则将编译时异常封装为运行时异常抛出
  16. new RuntimeException(e);
  17. }
  18. return null;
  19. }

⑥测试

image.png

  1. private BaseDao<Emp> baseDao = new BaseDao<>();
  2. @Test
  3. public void testGetSingleBean() {
  4. String sql = "select emp_id empId,emp_name empName,emp_position empPosition,login_account loginAccount,login_password loginPassword from t_emp where emp_id=?";
  5. Emp emp = baseDao.getSingleBean(sql, Emp.class, 1);
  6. System.out.println("emp = " + emp);
  7. }
  8. @Test
  9. public void testGetBeanList() {
  10. String sql = "select emp_id empId,emp_name empName,emp_position empPosition,login_account loginAccount,login_password loginPassword from t_emp";
  11. List<Emp> empList = baseDao.getBeanList(sql, Emp.class);
  12. for (Emp emp : empList) {
  13. System.out.println("emp = " + emp);
  14. }
  15. }
  16. @Test
  17. public void testUpdate() {
  18. String sql = "update t_emp set emp_position=? where emp_id=?";
  19. String empPosition = "minister";
  20. String empId = "3";
  21. int affectedRowNumber = baseDao.update(sql, empPosition, empId);
  22. System.out.println("affectedRowNumber = " + affectedRowNumber);
  23. }

5、子类 Dao

创建接口和实现类如下:
image.png

三、搭建环境:事务控制

1、总体思路

Maven案例 - 图14

2、TransactionFilter

①创建 Filter 类

image.png

②TransactionFilter 完整代码

  1. public class TransactionFilter implements Filter {
  2. // 声明集合保存静态资源扩展名
  3. private static Set<String> staticResourceExtNameSet;
  4. static {
  5. staticResourceExtNameSet = new HashSet<>();
  6. staticResourceExtNameSet.add(".png");
  7. staticResourceExtNameSet.add(".jpg");
  8. staticResourceExtNameSet.add(".css");
  9. staticResourceExtNameSet.add(".js");
  10. }
  11. @Override
  12. public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
  13. // 前置操作:排除静态资源
  14. HttpServletRequest request = (HttpServletRequest) servletRequest;
  15. String servletPath = request.getServletPath();
  16. if (servletPath.contains(".")) {
  17. String extName = servletPath.substring(servletPath.lastIndexOf("."));
  18. if (staticResourceExtNameSet.contains(extName)) {
  19. // 如果检测到当前请求确实是静态资源,则直接放行,不做事务操作
  20. filterChain.doFilter(servletRequest, servletResponse);
  21. // 当前方法立即返回
  22. return ;
  23. }
  24. }
  25. Connection connection = null;
  26. try{
  27. // 1、获取数据库连接
  28. connection = JDBCUtils.getConnection();
  29. // 重要操作:关闭自动提交功能
  30. connection.setAutoCommit(false);
  31. // 2、核心操作
  32. filterChain.doFilter(servletRequest, servletResponse);
  33. // 3、提交事务
  34. connection.commit();
  35. }catch (Exception e) {
  36. try {
  37. // 4、回滚事务
  38. connection.rollback();
  39. } catch (SQLException ex) {
  40. ex.printStackTrace();
  41. }
  42. // 页面显示:将这里捕获到的异常发送到指定页面显示
  43. // 获取异常信息
  44. String message = e.getMessage();
  45. // 将异常信息存入请求域
  46. request.setAttribute("systemMessage", message);
  47. // 将请求转发到指定页面
  48. request.getRequestDispatcher("/").forward(request, servletResponse);
  49. }finally {
  50. // 5、释放数据库连接
  51. JDBCUtils.releaseConnection(connection);
  52. }
  53. }
  54. @Override
  55. public void init(FilterConfig filterConfig) throws ServletException {}
  56. @Override
  57. public void destroy() {}
  58. }

③配置 web.xml

注意:需要首先将当前工程改成 Web 工程。
image.png

  1. <filter>
  2. <filter-name>txFilter</filter-name>
  3. <filter-class>com.atguigu.imperial.court.filter.TransactionFilter</filter-class>
  4. </filter>
  5. <filter-mapping>
  6. <filter-name>txFilter</filter-name>
  7. <url-pattern>/*</url-pattern>
  8. </filter-mapping>

④注意点

[1]确保异常回滚

在程序执行的过程中,必须让所有 catch 块都把编译时异常转换为运行时异常抛出;如果不这么做,在 TransactionFilter 中 catch 就无法捕获到底层抛出的异常,那么该回滚的时候就无法回滚。

[2]谨防数据库连接提前释放

由于诸多操作都是在使用同一个数据库连接,那么中间任何一个环节释放数据库连接都会导致后续操作无法正常完成。

四、搭建环境:表述层

1、视图模板技术 Thymeleaf

①服务器端渲染

参考资料(opens new window)
Maven案例 - 图17

②Thymeleaf 简要工作机制

[1]初始化阶段
  • 目标:创建 TemplateEngine 对象
  • 封装:因为对每一个请求来说,TemplateEngine 对象使用的都是同一个,所以在初始化阶段准备好

image.png

[2]请求处理阶段

Maven案例 - 图19

③逻辑视图与物理视图

假设有下列页面地址:
/WEB-INF/pages/apple.html
/WEB-INF/pages/banana.html
/WEB-INF/pages/orange.html
/WEB-INF/pages/grape.html
/WEB-INF/pages/egg.html
这样的地址可以直接访问到页面本身,我们称之为:物理视图。而将物理视图中前面、后面的固定内容抽取出来,让每次请求指定中间变化部分即可,那么中间变化部分就叫:逻辑视图
Maven案例 - 图20

④ViewBaseServlet 完整代码

为了简化视图页面处理过程,我们将 Thymeleaf 模板引擎的初始化和请求处理过程封装到一个 Servlet 基类中:ViewBaseServlet。以后负责具体模块业务功能的 Servlet 继承该基类即可直接使用。
传送门
特别提醒:这个类不需要掌握,因为以后都被框架封装了,我们现在只是暂时用一下。
image.png

⑤声明初始化参数

image.png

  1. <!-- 配置 Web 应用初始化参数指定视图前缀、后缀 -->
  2. <!--
  3. 物理视图举例:/WEB-INF/pages/index.html
  4. 对应逻辑视图:index
  5. -->
  6. <context-param>
  7. <param-name>view-prefix</param-name>
  8. <param-value>/WEB-INF/pages/</param-value>
  9. </context-param>
  10. <context-param>
  11. <param-name>view-suffix</param-name>
  12. <param-value>.html</param-value>
  13. </context-param>

⑥Thymeleaf 的页面语法

传送门(opens new window)

2、ModelBaseServlet

①提出问题

[1]我们的需求

image.png

[2]HttpServlet 的局限
  • doGet() 方法:处理 GET 请求
  • doPost() 方法:处理 POST 请求

    ②解决方案

  • 每个请求附带一个请求参数,表明自己要调用的目标方法

  • Servlet 根据目标方法名通过反射调用目标方法

    ③ModelBaseServlet 完整代码

    image.png
    特别提醒:为了配合 TransactionFilter 实现事务控制,捕获的异常必须抛出。
    传送门

    ④继承关系

    Maven案例 - 图25

    五、搭建环境:辅助功能

    1、常量类

    image.png ```java public class ImperialCourtConst {

    public static final String LOGIN_FAILED_MESSAGE = “账号、密码错误,不可进宫!”; public static final String ACCESS_DENIED_MESSAGE = “宫闱禁地,不得擅入!”;

}

  1. <a name="vcmnI"></a>
  2. ### 2、MD5 加密工具方法
  3. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/26803611/1652236169179-e0a0e96a-212c-4626-9083-2c4718f015d8.png#clientId=u10210f57-b892-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u76bad718&margin=%5Bobject%20Object%5D&name=image.png&originHeight=119&originWidth=313&originalType=url&ratio=1&rotation=0&showTitle=false&size=3679&status=done&style=none&taskId=u077c76f9-a8c4-4921-a856-3521149d9be&title=)
  4. ```java
  5. public class MD5Util {
  6. /**
  7. * 针对明文字符串执行MD5加密
  8. * @param source
  9. * @return
  10. */
  11. public static String encode(String source) {
  12. // 1.判断明文字符串是否有效
  13. if (source == null || "".equals(source)) {
  14. throw new RuntimeException("用于加密的明文不可为空");
  15. }
  16. // 2.声明算法名称
  17. String algorithm = "md5";
  18. // 3.获取MessageDigest对象
  19. MessageDigest messageDigest = null;
  20. try {
  21. messageDigest = MessageDigest.getInstance(algorithm);
  22. } catch (NoSuchAlgorithmException e) {
  23. e.printStackTrace();
  24. }
  25. // 4.获取明文字符串对应的字节数组
  26. byte[] input = source.getBytes();
  27. // 5.执行加密
  28. byte[] output = messageDigest.digest(input);
  29. // 6.创建BigInteger对象
  30. int signum = 1;
  31. BigInteger bigInteger = new BigInteger(signum, output);
  32. // 7.按照16进制将bigInteger的值转换为字符串
  33. int radix = 16;
  34. String encoded = bigInteger.toString(radix).toUpperCase();
  35. return encoded;
  36. }
  37. }

3、日志配置文件

image.png

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <configuration debug="true">
  3. <!-- 指定日志输出的位置 -->
  4. <appender name="STDOUT"
  5. class="ch.qos.logback.core.ConsoleAppender">
  6. <encoder>
  7. <!-- 日志输出的格式 -->
  8. <!-- 按照顺序分别是:时间、日志级别、线程名称、打印日志的类、日志主体内容、换行 -->
  9. <pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%n</pattern>
  10. <charset>UTF-8</charset>
  11. </encoder>
  12. </appender>
  13. <!-- 设置全局日志级别。日志级别按顺序分别是:DEBUG、INFO、WARN、ERROR -->
  14. <!-- 指定任何一个日志级别都只打印当前级别和后面级别的日志。 -->
  15. <root level="INFO">
  16. <!-- 指定打印日志的appender,这里通过“STDOUT”引用了前面配置的appender -->
  17. <appender-ref ref="STDOUT" />
  18. </root>
  19. <!-- 专门给某一个包指定日志级别 -->
  20. <logger name="com.atguigu" level="DEBUG" additivity="false">
  21. <appender-ref ref="STDOUT" />
  22. </logger>
  23. </configuration>

六、业务功能:登录

1、显示首页

①流程图

image.png

②创建 PortalServlet

[1]创建 Java 类

image.png

  1. public class PortalServlet extends ViewBaseServlet {
  2. @Override
  3. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  4. doPost(req, resp);
  5. }
  6. @Override
  7. protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  8. // 声明要访问的首页的逻辑视图
  9. String templateName = "index";
  10. // 调用父类的方法根据逻辑视图名称渲染视图
  11. processTemplate(templateName, req, resp);
  12. }
  13. }

[2]注册

image.png

  1. <servlet>
  2. <servlet-name>portalServlet</servlet-name>
  3. <servlet-class>com.atguigu.imperial.court.servlet.module.PortalServlet</servlet-class>
  4. </servlet>
  5. <servlet-mapping>
  6. <servlet-name>portalServlet</servlet-name>
  7. <url-pattern>/</url-pattern>
  8. </servlet-mapping>

③在 index.html 中编写登录表单

image.png

  1. <!DOCTYPE html>
  2. <html lang="en" xml:th="http://www.thymeleaf.org">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. </head>
  7. <body>
  8. <!-- @{/auth} 解析后:/demo/auth -->
  9. <form th:action="@{/auth}" method="post">
  10. <!-- 传递 method 请求参数,目的是为了让当前请求调用 AuthServlet 中的 login() 方法 -->
  11. <input type="hidden" name="method" value="login" />
  12. <!-- th:text 解析表达式后会替换标签体 -->
  13. <!-- ${attrName} 从请求域获取属性名为 attrName 的属性值 -->
  14. <p th:text="${message}"></p>
  15. <p th:text="${systemMessage}"></p>
  16. 账号:<input type="text" name="loginAccount"/><br/>
  17. 密码:<input type="password" name="loginPassword"><br/>
  18. <button type="submit">进宫</button>
  19. </form>
  20. </body>
  21. </html>

2、登录操作

①流程图

image.png

②创建 EmpService

image.png

③创建登录失败异常

image.png

  1. public class LoginFailedException extends RuntimeException {
  2. public LoginFailedException() {
  3. }
  4. public LoginFailedException(String message) {
  5. super(message);
  6. }
  7. public LoginFailedException(String message, Throwable cause) {
  8. super(message, cause);
  9. }
  10. public LoginFailedException(Throwable cause) {
  11. super(cause);
  12. }
  13. public LoginFailedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
  14. super(message, cause, enableSuppression, writableStackTrace);
  15. }
  16. }

④增加常量声明

image.png

  1. public class ImperialCourtConst {
  2. public static final String LOGIN_FAILED_MESSAGE = "账号、密码错误,不可进宫!";
  3. public static final String ACCESS_DENIED_MESSAGE = "宫闱禁地,不得擅入!";
  4. public static final String LOGIN_EMP_ATTR_NAME = "loginInfo";
  5. }

⑤创建 AuthServlet

[1]创建 Java 类

image.png

  1. public class AuthServlet extends ModelBaseServlet {
  2. private EmpService empService = new EmpServiceImpl();
  3. protected void login(
  4. HttpServletRequest request,
  5. HttpServletResponse response)
  6. throws ServletException, IOException {
  7. try {
  8. // 1、获取请求参数
  9. String loginAccount = request.getParameter("loginAccount");
  10. String loginPassword = request.getParameter("loginPassword");
  11. // 2、调用 EmpService 方法执行登录逻辑
  12. Emp emp = empService.getEmpByLoginAccount(loginAccount, loginPassword);
  13. // 3、通过 request 获取 HttpSession 对象
  14. HttpSession session = request.getSession();
  15. // 4、将查询到的 Emp 对象存入 Session 域
  16. session.setAttribute(ImperialCourtConst.LOGIN_EMP_ATTR_NAME, emp);
  17. // 5、前往指定页面视图
  18. String templateName = "temp";
  19. processTemplate(templateName, request, response);
  20. } catch (Exception e) {
  21. e.printStackTrace();
  22. // 6、判断此处捕获到的异常是否是登录失败异常
  23. if (e instanceof LoginFailedException) {
  24. // 7、如果是登录失败异常则跳转回登录页面
  25. // ①将异常信息存入请求域
  26. request.setAttribute("message", e.getMessage());
  27. // ②处理视图:index
  28. processTemplate("index", request, response);
  29. }else {
  30. // 8、如果不是登录异常则封装为运行时异常继续抛出
  31. throw new RuntimeException(e);
  32. }
  33. }
  34. }
  35. }

[2]注册

image.png

  1. <servlet>
  2. <servlet-name>authServlet</servlet-name>
  3. <servlet-class>com.atguigu.imperial.court.servlet.module.AuthServlet</servlet-class>
  4. </servlet>
  5. <servlet-mapping>
  6. <servlet-name>authServlet</servlet-name>
  7. <url-pattern>/auth</url-pattern>
  8. </servlet-mapping>

⑥EmpService 方法

image.png

  1. public class EmpServiceImpl implements EmpService {
  2. private EmpDao empDao = new EmpDaoImpl();
  3. @Override
  4. public Emp getEmpByLoginAccount(String loginAccount, String loginPassword) {
  5. // 1、对密码执行加密
  6. String encodedLoginPassword = MD5Util.encode(loginPassword);
  7. // 2、根据账户和加密密码查询数据库
  8. Emp emp = empDao.selectEmpByLoginAccount(loginAccount, encodedLoginPassword);
  9. // 3、检查 Emp 对象是否为 null
  10. if (emp != null) {
  11. // ①不为 null:返回 Emp
  12. return emp;
  13. } else {
  14. // ②为 null:抛登录失败异常
  15. throw new LoginFailedException(ImperialCourtConst.LOGIN_FAILED_MESSAGE);
  16. }
  17. }
  18. }

⑦EmpDao 方法

image.png

  1. public class EmpDaoImpl extends BaseDao<Emp> implements EmpDao {
  2. @Override
  3. public Emp selectEmpByLoginAccount(String loginAccount, String encodedLoginPassword) {
  4. // 1、编写 SQL 语句
  5. String sql = "select emp_id empId," +
  6. "emp_name empName," +
  7. "emp_position empPosition," +
  8. "login_account loginAccount," +
  9. "login_password loginPassword " +
  10. "from t_emp where login_account=? and login_password=?";
  11. // 2、调用父类方法查询单个对象
  12. return super.getSingleBean(sql, Emp.class, loginAccount, encodedLoginPassword);
  13. }
  14. }

⑧临时页面

image.png

  1. <!DOCTYPE html>
  2. <html lang="en" xml:th="http://www.thymeleaf.org">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>临时</title>
  6. </head>
  7. <body>
  8. <p th:text="${session.loginInfo}"></p>
  9. </body>
  10. </html>

3、退出登录

①在临时页面编写超链接

image.png

  1. <a th:href="@{/auth?method=logout}">退朝</a>

②在 AuthServlet 编写退出逻辑

image.png

  1. protected void logout(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  2. // 1、通过 request 对象获取 HttpSession 对象
  3. HttpSession session = request.getSession();
  4. // 2、将 HttpSession 对象强制失效
  5. session.invalidate();
  6. // 3、回到首页
  7. String templateName = "index";
  8. processTemplate(templateName, request, response);
  9. }

七、业务功能:显示奏折列表

1、流程图

image.png

2、创建组件

①创建 WorkServlet

[1]创建 Java 类

刚开始是空的,还没有写方法:
image.png

  1. public class WorkServlet extends ModelBaseServlet {
  2. private MemorialsService memorialsService = new MemorialsServiceImpl();
  3. }

[2]注册
  1. <servlet>
  2. <servlet-name>workServlet</servlet-name>
  3. <servlet-class>com.atguigu.imperial.court.servlet.module.WorkServlet</servlet-class>
  4. </servlet>
  5. <servlet-mapping>
  6. <servlet-name>workServlet</servlet-name>
  7. <url-pattern>/work</url-pattern>
  8. </servlet-mapping>

②创建 MemorialsService

[1]接口

image.png

[2]实现类

image.png

  1. public class MemorialsServiceImpl implements MemorialsService {
  2. private MemorialsDao memorialsDao = new MemorialsDaoImpl();
  3. }

3、WorkServlet 方法

image.png

  1. protected void showMemorialsDigestList(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  2. // 1、调用 Service 方法查询数据
  3. List<Memorials> memorialsList = memorialsService.getAllMemorialsDigest();
  4. // 2、将查询得到的数据存入请求域
  5. request.setAttribute("memorialsList", memorialsList);
  6. // 3、渲染视图
  7. String templateName = "memorials-list";
  8. processTemplate(templateName, request, response);
  9. }

4、MemorialsService 方法

image.png

  1. @Override
  2. public List<Memorials> getAllMemorialsDigest() {
  3. return memorialsDao.selectAllMemorialsDigest();
  4. }

5、MemorialsDao 方法

image.png

  1. @Override
  2. public List<Memorials> selectAllMemorialsDigest() {
  3. String sql = "select memorials_id memorialsId,\n" +
  4. " memorials_title memorialsTitle,\n" +
  5. " concat(left(memorials_content, 10), \"...\") memorialsContentDigest,\n" +
  6. " emp_name memorialsEmpName,\n" +
  7. " memorials_create_time memorialsCreateTime,\n" +
  8. " memorials_status memorialsStatus\n" +
  9. "from t_memorials m left join t_emp e on m.memorials_emp=e.emp_id;";
  10. return getBeanList(sql, Memorials.class);
  11. }

6、页面显示

image.png

①页面上的样式声明

  1. <style type="text/css">
  2. table {
  3. border-collapse: collapse;
  4. margin: 0px auto 0px auto;
  5. }
  6. table th, td {
  7. border: 1px solid black;
  8. text-align: center;
  9. }
  10. div {
  11. text-align: right;
  12. }
  13. </style>

②用户登录信息部分

  1. <!-- 登录信息部分 -->
  2. <div>
  3. <span th:if="${session.loginInfo.empPosition == 'emperor'}">恭请皇上圣安</span>
  4. <span th:if="${session.loginInfo.empPosition == 'minister'}"><span th:text="${session.loginInfo.empName}">XXX</span>大人请安</span>
  5. <a th:href="@{/auth?method=logout}">退朝</a>
  6. </div>

③数据展示信息部分

  1. <!-- 数据显示部分 -->
  2. <table>
  3. <thead>
  4. <tr>
  5. <th>奏折标题</th>
  6. <th>内容摘要</th>
  7. <th>上疏大臣</th>
  8. <th>上疏时间</th>
  9. <th>奏折状态</th>
  10. <th>奏折详情</th>
  11. </tr>
  12. </thead>
  13. <tbody th:if="${#lists.isEmpty(memorialsList)}">
  14. <tr>
  15. <td colspan="6">没有人上过折子</td>
  16. </tr>
  17. </tbody>
  18. <tbody th:if="${not #lists.isEmpty(memorialsList)}">
  19. <tr th:each="memorials : ${memorialsList}">
  20. <td th:switch="${memorials.memorialsStatus}">
  21. <span th:text="${memorials.memorialsTitle}" th:case="0" style="color: red;">奏折标题</span>
  22. <span th:text="${memorials.memorialsTitle}" th:case="1" style="color: blue;">奏折标题</span>
  23. <span th:text="${memorials.memorialsTitle}" th:case="2">奏折标题</span>
  24. </td>
  25. <td th:switch="${memorials.memorialsStatus}">
  26. <span th:text="${memorials.memorialsContentDigest}" th:case="0" style="color: red;">内容摘要</span>
  27. <span th:text="${memorials.memorialsContentDigest}" th:case="1" style="color: blue;">内容摘要</span>
  28. <span th:text="${memorials.memorialsContentDigest}" th:case="2">内容摘要</span>
  29. </td>
  30. <td th:switch="${memorials.memorialsStatus}">
  31. <span th:text="${memorials.memorialsEmpName}" th:case="0" style="color: red;">上疏大臣</span>
  32. <span th:text="${memorials.memorialsEmpName}" th:case="1" style="color: blue;">上疏大臣</span>
  33. <span th:text="${memorials.memorialsEmpName}" th:case="2">上疏大臣</span>
  34. </td>
  35. <td th:switch="${memorials.memorialsStatus}">
  36. <span th:text="${memorials.memorialsCreateTime}" th:case="0" style="color: red;">上疏时间</span>
  37. <span th:text="${memorials.memorialsCreateTime}" th:case="1" style="color: blue;">上疏时间</span>
  38. <span th:text="${memorials.memorialsCreateTime}" th:case="2">上疏时间</span>
  39. </td>
  40. <td th:switch="${memorials.memorialsStatus}">
  41. <span th:case="0" style="color: red;">未读</span>
  42. <span th:case="1" style="color: blue;">已读</span>
  43. <span th:case="2">已批示</span>
  44. </td>
  45. <td>
  46. <a th:href="@{/work?method=detail}">奏折详情</a>
  47. </td>
  48. </tr>
  49. </tbody>
  50. </table>

7、和登录成功对接

image.png

  1. protected void login(
  2. HttpServletRequest request,
  3. HttpServletResponse response)
  4. throws ServletException, IOException {
  5. try {
  6. // 1、获取请求参数
  7. String loginAccount = request.getParameter("loginAccount");
  8. String loginPassword = request.getParameter("loginPassword");
  9. // 2、调用 EmpService 方法执行登录逻辑
  10. Emp emp = empService.getEmpByLoginAccount(loginAccount, loginPassword);
  11. // 3、通过 request 获取 HttpSession 对象
  12. HttpSession session = request.getSession();
  13. // 4、将查询到的 Emp 对象存入 Session 域
  14. session.setAttribute(ImperialCourtConst.LOGIN_EMP_ATTR_NAME, emp);
  15. // 5、前往指定页面视图
  16. // 前往临时页面
  17. // String templateName = "temp";
  18. // processTemplate(templateName, request, response);
  19. // 前往正式的目标地址
  20. response.sendRedirect(request.getContextPath() + "/work?method=showMemorialsDigestList");
  21. } catch (Exception e) {
  22. e.printStackTrace();
  23. // 6、判断此处捕获到的异常是否是登录失败异常
  24. if (e instanceof LoginFailedException) {
  25. // 7、如果是登录失败异常则跳转回登录页面
  26. // ①将异常信息存入请求域
  27. request.setAttribute("message", e.getMessage());
  28. // ②处理视图:index
  29. processTemplate("index", request, response);
  30. }else {
  31. // 8、如果不是登录异常则封装为运行时异常继续抛出
  32. throw new RuntimeException(e);
  33. }
  34. }
  35. }

八、业务功能:显示奏折详情

1、流程图

image.png

2、调整奏折列表页面的超链接

  1. <a th:href="@{/work(method='showMemorialsDetail',memorialsId=${memorials.memorialsId})}">奏折详情</a>

3、WorkServlet 方法

image.png

  1. protected void showMemorialsDetail(
  2. HttpServletRequest request,
  3. HttpServletResponse response)
  4. throws ServletException, IOException {
  5. // 1、从请求参数读取 memorialsId
  6. String memorialsId = request.getParameter("memorialsId");
  7. // 2、根据 memorialsId 从 Service 中查询 Memorials 对象
  8. Memorials memorials = memorialsService.getMemorialsDetailById(memorialsId);
  9. // 3、将 Memorials 对象存入请求域
  10. request.setAttribute("memorials", memorials);
  11. // 4、解析渲染视图
  12. String templateName = "memorials_detail";
  13. processTemplate(templateName, request, response);
  14. }

4、MemorialsService 方法

image.png

  1. @Override
  2. public Memorials getMemorialsDetailById(String memorialsId) {
  3. return memorialsDao.selectMemorialsById(memorialsId);
  4. }

5、MemorialsDao 方法

image.png

  1. @Override
  2. public Memorials selectMemorialsById(String memorialsId) {
  3. String sql = "select memorials_id memorialsId,\n" +
  4. " memorials_title memorialsTitle,\n" +
  5. " memorials_content memorialsContent,\n" +
  6. " emp_name memorialsEmpName,\n" +
  7. " memorials_create_time memorialsCreateTime,\n" +
  8. " memorials_status memorialsStatus,\n" +
  9. " feedback_time feedbackTime,\n" +
  10. " feedback_content feedbackContent\n" +
  11. "from t_memorials m left join t_emp e on m.memorials_emp=e.emp_id " +
  12. "where memorials_id=?;";
  13. return getSingleBean(sql, Memorials.class, memorialsId);
  14. }

6、详情页

image.png

  1. <!-- 登录信息部分 -->
  2. <div>
  3. <span th:if="${session.loginInfo.empPosition == 'emperor'}">恭请皇上圣安</span>
  4. <span th:if="${session.loginInfo.empPosition == 'minister'}"><span th:text="${session.loginInfo.empName}">XXX</span>大人请安</span>
  5. <a th:href="@{/auth?method=logout}">退朝</a>
  6. </div>
  7. <table>
  8. <tr>
  9. <td>奏折标题</td>
  10. <td th:text="${memorials.memorialsTitle}"></td>
  11. </tr>
  12. <tr>
  13. <td>上疏大臣</td>
  14. <td th:text="${memorials.memorialsEmpName}"></td>
  15. </tr>
  16. <tr>
  17. <td>上疏时间</td>
  18. <td th:text="${memorials.memorialsCreateTime}"></td>
  19. </tr>
  20. <tr>
  21. <td>奏折内容</td>
  22. <td th:text="${memorials.memorialsContent}"></td>
  23. </tr>
  24. <tr th:if="${memorials.memorialsStatus == 2}">
  25. <td>批复时间</td>
  26. <td th:text="${memorials.feedbackTime}"></td>
  27. </tr>
  28. <tr th:if="${memorials.memorialsStatus == 2}">
  29. <td>批复时间</td>
  30. <td th:text="${memorials.feedbackContent}"></td>
  31. </tr>
  32. </table>
  33. <div th:if="${memorials.memorialsStatus != 2}">
  34. <form th:action="@{/work}" method="post">
  35. <input type="hidden" name="method" value="feedBack" />
  36. <input type="hidden" name="memorialsId" th:value="${memorials.memorialsId}"/>
  37. <textarea name="feedbackContent"></textarea>
  38. <button type="submit">御批</button>
  39. </form>
  40. </div>
  41. <a th:href="@{/work?method=showMemorialsDigestList}">返回列表</a>

7、更新状态

①业务逻辑规则

一份未读奏折,点击查看后,需要从未读变成已读。

②WorkServlet 方法

image.png
增加判断:

  1. protected void showMemorialsDetail(
  2. HttpServletRequest request,
  3. HttpServletResponse response)
  4. throws ServletException, IOException {
  5. // 1、从请求参数读取 memorialsId
  6. String memorialsId = request.getParameter("memorialsId");
  7. // 2、根据 memorialsId 从 Service 中查询 Memorials 对象
  8. Memorials memorials = memorialsService.getMemorialsDetailById(memorialsId);
  9. // **********************补充功能**********************
  10. // 获取当前奏折对象的状态
  11. Integer memorialsStatus = memorials.getMemorialsStatus();
  12. // 判断奏折状态
  13. if (memorialsStatus == 0) {
  14. // 更新奏折状态:数据库修改
  15. memorialsService.updateMemorialsStatusToRead(memorialsId);
  16. // 更新奏折状态:当前对象修改
  17. memorials.setMemorialsStatus(1);
  18. }
  19. // **********************补充功能**********************
  20. // 3、将 Memorials 对象存入请求域
  21. request.setAttribute("memorials", memorials);
  22. // 4、解析渲染视图
  23. String templateName = "memorials_detail";
  24. processTemplate(templateName, request, response);
  25. }

③MemorialsService 方法

image.png

  1. @Override
  2. public void updateMemorialsStatusToRead(String memorialsId) {
  3. memorialsDao.updateMemorialsStatusToRead(memorialsId);
  4. }

④MemorialsDao 方法

image.png

  1. @Override
  2. public void updateMemorialsStatusToRead(String memorialsId) {
  3. String sql = "update t_memorials set memorials_status=1 where memorials_id=?";
  4. update(sql, memorialsId);
  5. }

九、业务功能:批复奏折

1、本质

提交表单,更新数据。

2、WorkServlet 方法

  1. protected void feedBack(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  2. // 获取表单提交的请求参数
  3. String memorialsId = request.getParameter("memorialsId");
  4. String feedbackContent = request.getParameter("feedbackContent");
  5. // 执行更新
  6. memorialsService.updateMemorialsFeedBack(memorialsId, feedbackContent);
  7. // 重定向回显示奏折列表页面
  8. response.sendRedirect(request.getContextPath() + "/work?method=showMemorialsDigestList");
  9. }

3、MemorialsService 方法

  1. @Override
  2. public void updateMemorialsFeedBack(String memorialsId, String feedbackContent) {
  3. memorialsDao.updateMemorialsFeedBack(memorialsId, feedbackContent);
  4. }

4、MemorialsDao 方法

  1. @Override
  2. public void updateMemorialsFeedBack(String memorialsId, String feedbackContent) {
  3. String feedbackTime = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
  4. String sql = "update t_memorials set memorials_status=2,feedback_content=?,feedback_time=? where memorials_id=?";
  5. update(sql, feedbackContent, feedbackTime, memorialsId);
  6. }

十、业务功能:登录检查

1、流程图

image.png

2、创建 LoginFilter

①创建 Java 类

image.png

  1. public class LoginFilter implements Filter {
  2. @Override
  3. public void init(FilterConfig filterConfig) throws ServletException {}
  4. @Override
  5. public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
  6. // 1、获取 HttpSession 对象
  7. HttpServletRequest request = (HttpServletRequest) servletRequest;
  8. HttpSession session = request.getSession();
  9. // 2、尝试从 Session 域获取已登录的对象
  10. Object loginEmp = session.getAttribute(ImperialCourtConst.LOGIN_EMP_ATTR_NAME);
  11. // 3、判断 loginEmp 是否为空
  12. if (loginEmp != null) {
  13. // 4、若不为空则说明当前请求已登录,直接放行
  14. filterChain.doFilter(request, servletResponse);
  15. return ;
  16. }
  17. // 5、若为空说明尚未登录,则回到登录页面
  18. request.setAttribute("systemMessage", ImperialCourtConst.ACCESS_DENIED_MESSAGE);
  19. request.getRequestDispatcher("/").forward(request, servletResponse);
  20. }
  21. @Override
  22. public void destroy() {}
  23. }

②注册

image.png
把 LoginFilter 放在 TransactionFilter 前面声明,原因是:如果登录检查失败不放行,直接跳转到页面,此时将不必执行 TransactionFilter 中的事务操作,可以节约性能。

  1. <filter>
  2. <filter-name>loginFilter</filter-name>
  3. <filter-class>com.atguigu.imperial.court.filter.LoginFilter</filter-class>
  4. </filter>
  5. <filter-mapping>
  6. <filter-name>loginFilter</filter-name>
  7. <url-pattern>/work</url-pattern>
  8. </filter-mapping>

十一、打包部署

1、适配部署环境

MySQL 连接信息中,IP 地址部分需要改成 localhost。
image.png

  1. url=jdbc:mysql://localhost:3306/db_imperial_court

2、跳过测试打包

mvn clean package -Dmaven.test.skip=true

可以人为指定最终 war 包名称:

  1. <!-- 对构建过程进行自己的定制 -->
  2. <build>
  3. <!-- 当前工程在构建过程中使用的最终名称 -->
  4. <finalName>demo-me</finalName>
  5. </build>

3、部署执行

①上传 war 包

[略]

②启动 Tomcat

  1. /opt/apache-tomcat-8.5.75/bin/startup.sh

③访问测试

image.png

第七章 SSM 整合伪分布式案例

一、创建工程,引入依赖

1、创建工程

①工程清单

image.png

工程名 地位 说明
demo-imperial-court-ssm-show 父工程 总体管理各个子工程
demo-module01-web 子工程 唯一的 war 包工程
demo-module02-component 子工程 管理项目中的各种组件
demo-module03-entity 子工程 管理项目中的实体类
demo-module04-util 子工程 管理项目中的工具类
demo-module05-environment 子工程 框架环境所需依赖
demo-module06-generate 子工程 Mybatis 逆向工程

②工程间关系

image.png

2、各工程 POM 配置

①父工程

POM 位置如下:
image.png
各子工程创建好之后就会有下面配置,不需要手动编辑:

  1. <groupId>com.atguigu</groupId>
  2. <artifactId>demo-imperial-court-ssm-show</artifactId>
  3. <packaging>pom</packaging>
  4. <version>1.0-SNAPSHOT</version>
  5. <modules>
  6. <module>demo-module01-web</module>
  7. <module>demo-module02-component</module>
  8. <module>demo-module03-entity</module>
  9. <module>demo-module04-util</module>
  10. <module>demo-module05-environment</module>
  11. <module>demo-module06-generate</module>
  12. </modules>

②Mybatis 逆向工程

POM 位置如下:
image.png

  1. <!-- 依赖MyBatis核心包 -->
  2. <dependencies>
  3. <dependency>
  4. <groupId>org.mybatis</groupId>
  5. <artifactId>mybatis</artifactId>
  6. <version>3.5.7</version>
  7. </dependency>
  8. </dependencies>
  9. <!-- 控制Maven在构建过程中相关配置 -->
  10. <build>
  11. <!-- 构建过程中用到的插件 -->
  12. <plugins>
  13. <!-- 具体插件,逆向工程的操作是以构建过程中插件形式出现的 -->
  14. <plugin>
  15. <groupId>org.mybatis.generator</groupId>
  16. <artifactId>mybatis-generator-maven-plugin</artifactId>
  17. <version>1.3.0</version>
  18. <!-- 插件的依赖 -->
  19. <dependencies>
  20. <!-- 逆向工程的核心依赖 -->
  21. <dependency>
  22. <groupId>org.mybatis.generator</groupId>
  23. <artifactId>mybatis-generator-core</artifactId>
  24. <version>1.3.2</version>
  25. </dependency>
  26. <!-- 数据库连接池 -->
  27. <dependency>
  28. <groupId>com.mchange</groupId>
  29. <artifactId>c3p0</artifactId>
  30. <version>0.9.2</version>
  31. </dependency>
  32. <!-- MySQL驱动 -->
  33. <dependency>
  34. <groupId>mysql</groupId>
  35. <artifactId>mysql-connector-java</artifactId>
  36. <version>5.1.8</version>
  37. </dependency>
  38. </dependencies>
  39. </plugin>
  40. </plugins>
  41. </build>

③环境依赖工程

POM 位置如下:
image.png

  1. <!-- SpringMVC -->
  2. <dependency>
  3. <groupId>org.springframework</groupId>
  4. <artifactId>spring-webmvc</artifactId>
  5. <version>5.3.1</version>
  6. </dependency>
  7. <!-- Spring 持久化层所需依赖 -->
  8. <dependency>
  9. <groupId>org.springframework</groupId>
  10. <artifactId>spring-orm</artifactId>
  11. <version>5.3.1</version>
  12. </dependency>
  13. <!-- 日志 -->
  14. <dependency>
  15. <groupId>ch.qos.logback</groupId>
  16. <artifactId>logback-classic</artifactId>
  17. <version>1.2.3</version>
  18. </dependency>
  19. <!-- Spring5和Thymeleaf整合包 -->
  20. <dependency>
  21. <groupId>org.thymeleaf</groupId>
  22. <artifactId>thymeleaf-spring5</artifactId>
  23. <version>3.0.12.RELEASE</version>
  24. </dependency>
  25. <!-- Mybatis 和 Spring 的整合包 -->
  26. <dependency>
  27. <groupId>org.mybatis</groupId>
  28. <artifactId>mybatis-spring</artifactId>
  29. <version>2.0.6</version>
  30. </dependency>
  31. <!-- Mybatis核心 -->
  32. <dependency>
  33. <groupId>org.mybatis</groupId>
  34. <artifactId>mybatis</artifactId>
  35. <version>3.5.7</version>
  36. </dependency>
  37. <!-- MySQL驱动 -->
  38. <dependency>
  39. <groupId>mysql</groupId>
  40. <artifactId>mysql-connector-java</artifactId>
  41. <version>5.1.3</version>
  42. </dependency>
  43. <!-- 数据源 -->
  44. <dependency>
  45. <groupId>com.alibaba</groupId>
  46. <artifactId>druid</artifactId>
  47. <version>1.0.31</version>
  48. </dependency>

④工具类工程

无配置。

⑤实体类工程

无配置。

⑥组件工程

POM 位置如下:
image.png

  1. <dependency>
  2. <groupId>com.atguigu</groupId>
  3. <artifactId>demo-module03-entity</artifactId>
  4. <version>1.0-SNAPSHOT</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>com.atguigu</groupId>
  8. <artifactId>demo-module04-util</artifactId>
  9. <version>1.0-SNAPSHOT</version>
  10. </dependency>
  11. <dependency>
  12. <groupId>com.atguigu</groupId>
  13. <artifactId>demo-module05-environment</artifactId>
  14. <version>1.0-SNAPSHOT</version>
  15. </dependency>
  16. <!-- ServletAPI -->
  17. <dependency>
  18. <groupId>javax.servlet</groupId>
  19. <artifactId>javax.servlet-api</artifactId>
  20. <version>3.1.0</version>
  21. <scope>provided</scope>
  22. </dependency>

⑦Web 工程

POM 位置如下:
image.png

  1. <dependency>
  2. <groupId>com.atguigu</groupId>
  3. <artifactId>demo-module02-component</artifactId>
  4. <version>1.0-SNAPSHOT</version>
  5. </dependency>
  6. <!-- junit5 -->
  7. <dependency>
  8. <groupId>org.junit.jupiter</groupId>
  9. <artifactId>junit-jupiter-api</artifactId>
  10. <version>5.7.0</version>
  11. <scope>test</scope>
  12. </dependency>
  13. <!-- Spring 的测试功能 -->
  14. <dependency>
  15. <groupId>org.springframework</groupId>
  16. <artifactId>spring-test</artifactId>
  17. <version>5.3.1</version>
  18. <scope>test</scope>
  19. </dependency>

二、搭建环境:持久化层

1、物理建模

我们仍然继续使用《第六章 单一架构案例》中创建的数据库和表。

2、Mybatis 逆向工程

①generatorConfig.xml

image.png
查看详细配置信息

②执行逆向生成

image.png

③资源归位

下面罗列各种资源应该存放的位置,排名不分先后:

[1]Mapper 配置文件

image.png

[2]Mapper 接口

image.png

[3]实体类

Mybatis 逆向工程生成的实体类只有字段和 get、set 方法,我们可以自己添加无参构造器、有参构造器、toString() 方法。
image.png

3、建立数据库连接

①数据库连接信息

image.png

  1. dev.driverClassName=com.mysql.jdbc.Driver
  2. dev.url=jdbc:mysql://192.168.198.100:3306/db_imperial_court
  3. dev.username=root
  4. dev.password=atguigu
  5. dev.initialSize=10
  6. dev.maxActive=20
  7. dev.maxWait=10000

②配置数据源

image.png

  1. <context:property-placeholder location="classpath:jdbc.properties"/>
  2. <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
  3. <property name="username" value="${dev.username}"/>
  4. <property name="password" value="${dev.password}"/>
  5. <property name="url" value="${dev.url}"/>
  6. <property name="driverClassName" value="${dev.driverClassName}"/>
  7. <property name="initialSize" value="${dev.initialSize}"/>
  8. <property name="maxActive" value="${dev.maxActive}"/>
  9. <property name="maxWait" value="${dev.maxWait}"/>
  10. </bean>

③测试

image.png

  1. @ExtendWith(SpringExtension.class)
  2. @ContextConfiguration(value = {"classpath:spring-persist.xml"})
  3. public class ImperialCourtTest {
  4. @Autowired
  5. private DataSource dataSource;
  6. private Logger logger = LoggerFactory.getLogger(ImperialCourtTest.class);
  7. @Test
  8. public void testConnection() throws SQLException {
  9. Connection connection = dataSource.getConnection();
  10. logger.debug(connection.toString());
  11. }
  12. }

配置文件为什么要放到 Web 工程里面?

  • Web 工程将来生成 war 包。
  • war 包直接部署到 Tomcat 运行。
  • Tomcat 从 war 包(解压目录)查找配置文件最直接。
  • 如果不是把配置文件放在 Web 工程,而是放在 Java 工程,那就等于将配置文件放在了 war 包内的 jar 包中。
  • 配置文件在 jar 包中读取相对困难。

4、Spring 整合 Mybatis

image.png

①配置 SqlSessionFactoryBean

目的1:装配数据源
目的2:指定 Mapper 配置文件的位置

  1. <!-- 配置 SqlSessionFactoryBean -->
  2. <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  3. <!-- 装配数据源 -->
  4. <property name="dataSource" ref="druidDataSource"/>
  5. <!-- 指定 Mapper 配置文件的位置 -->
  6. <property name="mapperLocations" value="classpath:mapper/*Mapper.xml"/>
  7. </bean>

②扫描 Mapper 接口

  1. <mybatis:scan base-package="com.atguigu.imperial.court.mapper"/>

③测试

image.png

  1. @Autowired
  2. private EmpMapper empMapper;
  3. @Test
  4. public void testEmpMapper() {
  5. List<Emp> empList = empMapper.selectByExample(new EmpExample());
  6. for (Emp emp : empList) {
  7. System.out.println("emp = " + emp);
  8. }
  9. }

三、搭建环境:事务控制

1、声明式事务配置

image.png

  1. <!-- 配置事务管理器 -->
  2. <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  3. <!-- 装配数据源 -->
  4. <property name="dataSource" ref="dataSource"/>
  5. </bean>
  6. <!-- 配置事务的注解驱动,开启基于注解的声明式事务功能 -->
  7. <tx:annotation-driven transaction-manager="transactionManager"/>
  8. <!-- 配置对 Service 所在包的自动扫描 -->
  9. <context:component-scan base-package="com.atguigu.imperial.court.service"/>

2、注解写法

①查询操作

  1. @Transactional(readOnly = true)

②增删改操作

  1. @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = false)

在具体代码开发中可能会将相同设置的 @Transactional 注解提取到 Service 类上。

四、搭建环境:表述层

1、设定 Web 工程

传送门(opens new window)

2、web.xml 配置

①配置 ContextLoaderListener

  1. <!-- 第一部分:加载 spring-persist.xml 配置文件 -->
  2. <context-param>
  3. <param-name>contextConfigLocation</param-name>
  4. <param-value>classpath:spring-persist.xml</param-value>
  5. </context-param>
  6. <listener>
  7. <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  8. </listener>

②配置 DispatcherServlet

  1. <!-- 第一部分:加载 spring-persist.xml 配置文件 -->
  2. <context-param>
  3. <param-name>contextConfigLocation</param-name>
  4. <param-value>classpath:spring-persist.xml</param-value>
  5. </context-param>
  6. <listener>
  7. <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  8. </listener>

③配置 CharacterEncodingFilter

  1. <!-- 第三部分:设置字符集的 Filter -->
  2. <filter>
  3. <filter-name>characterEncodingFilter</filter-name>
  4. <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
  5. <init-param>
  6. <param-name>encoding</param-name>
  7. <param-value>UTF-8</param-value>
  8. </init-param>
  9. <init-param>
  10. <param-name>forceRequestEncoding</param-name>
  11. <param-value>true</param-value>
  12. </init-param>
  13. <init-param>
  14. <param-name>forceResponseEncoding</param-name>
  15. <param-value>true</param-value>
  16. </init-param>
  17. </filter>
  18. <filter-mapping>
  19. <filter-name>characterEncodingFilter</filter-name>
  20. <url-pattern>/*</url-pattern>
  21. </filter-mapping>

④配置 HiddenHttpMethodFilter

  1. <!-- 第三部分:设置字符集的 Filter -->
  2. <filter>
  3. <filter-name>characterEncodingFilter</filter-name>
  4. <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
  5. <init-param>
  6. <param-name>encoding</param-name>
  7. <param-value>UTF-8</param-value>
  8. </init-param>
  9. <init-param>
  10. <param-name>forceRequestEncoding</param-name>
  11. <param-value>true</param-value>
  12. </init-param>
  13. <init-param>
  14. <param-name>forceResponseEncoding</param-name>
  15. <param-value>true</param-value>
  16. </init-param>
  17. </filter>
  18. <filter-mapping>
  19. <filter-name>characterEncodingFilter</filter-name>
  20. <url-pattern>/*</url-pattern>
  21. </filter-mapping>

3、显示首页

①配置 SpringMVC

[1]标配
  1. <!-- 开启 SpringMVC 的注解驱动功能 -->
  2. <mvn:annotation-driven />
  3. <!-- 让 SpringMVC 对没有 @RequestMapping 的请求直接放行 -->
  4. <mvc:default-servlet-handler />

[2]配置视图解析相关
  1. <!-- 配置视图解析器 -->
  2. <bean id="thymeleafViewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
  3. <property name="order" value="1"/>
  4. <property name="characterEncoding" value="UTF-8"/>
  5. <property name="templateEngine">
  6. <bean class="org.thymeleaf.spring5.SpringTemplateEngine">
  7. <property name="templateResolver">
  8. <bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
  9. <property name="prefix" value="/WEB-INF/templates/"/>
  10. <property name="suffix" value=".html"/>
  11. <property name="characterEncoding" value="UTF-8"/>
  12. <property name="templateMode" value="HTML5"/>
  13. </bean>
  14. </property>
  15. </bean>
  16. </property>
  17. </bean>

注意:需要我们自己手动创建 templates 目录。
image.png

[3]配置自动扫描的包
  1. <!-- 配置自动扫描的包 -->
  2. <context:component-scan base-package="com.atguigu.imperial.court.controller"/>

②配置 view-controller 访问首页

  1. <!-- 配置 view-controller -->
  2. <mvc:view-controller path="/" view-name="index" />

③创建首页模板文件

image.png

  1. <!DOCTYPE html>
  2. <html lang="en" xml:th="http://www.thymeleaf.org">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>首页</title>
  6. </head>
  7. <body>
  8. <!-- @{/auth} 解析后:/demo/auth -->
  9. <form th:action="@{/auth/login}" method="post">
  10. <!-- th:text 解析表达式后会替换标签体 -->
  11. <!-- ${attrName} 从请求域获取属性名为 attrName 的属性值 -->
  12. <p style="color: red;font-weight: bold;" th:text="${message}"></p>
  13. <p style="color: red;font-weight: bold;" th:text="${systemMessage}"></p>
  14. 账号:<input type="text" name="loginAccount"/><br/>
  15. 密码:<input type="password" name="loginPassword"><br/>
  16. <button type="submit">进宫</button>
  17. </form>
  18. </body>
  19. </html>

五、搭建环境:辅助功能

1、登录失败异常

image.png

  1. public class LoginFailedException extends RuntimeException {
  2. public LoginFailedException() {
  3. }
  4. public LoginFailedException(String message) {
  5. super(message);
  6. }
  7. public LoginFailedException(String message, Throwable cause) {
  8. super(message, cause);
  9. }
  10. public LoginFailedException(Throwable cause) {
  11. super(cause);
  12. }
  13. public LoginFailedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
  14. super(message, cause, enableSuppression, writableStackTrace);
  15. }
  16. }

2、常量类

image.png

  1. public class ImperialCourtConst {
  2. public static final String LOGIN_FAILED_MESSAGE = "账号、密码错误,不可进宫!";
  3. public static final String ACCESS_DENIED_MESSAGE = "宫闱禁地,不得擅入!";
  4. public static final String LOGIN_EMP_ATTR_NAME = "loginInfo";
  5. }

3、MD5 工具

image.png

  1. public class MD5Util {
  2. /**
  3. * 针对明文字符串执行MD5加密
  4. * @param source
  5. * @return
  6. */
  7. public static String encode(String source) {
  8. // 1.判断明文字符串是否有效
  9. if (source == null || "".equals(source)) {
  10. throw new RuntimeException("用于加密的明文不可为空");
  11. }
  12. // 2.声明算法名称
  13. String algorithm = "md5";
  14. // 3.获取MessageDigest对象
  15. MessageDigest messageDigest = null;
  16. try {
  17. messageDigest = MessageDigest.getInstance(algorithm);
  18. } catch (NoSuchAlgorithmException e) {
  19. e.printStackTrace();
  20. }
  21. // 4.获取明文字符串对应的字节数组
  22. byte[] input = source.getBytes();
  23. // 5.执行加密
  24. byte[] output = messageDigest.digest(input);
  25. // 6.创建BigInteger对象
  26. int signum = 1;
  27. BigInteger bigInteger = new BigInteger(signum, output);
  28. // 7.按照16进制将bigInteger的值转换为字符串
  29. int radix = 16;
  30. String encoded = bigInteger.toString(radix).toUpperCase();
  31. return encoded;
  32. }
  33. }

4、日志配置文件

image.png

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <configuration debug="true">
  3. <!-- 指定日志输出的位置 -->
  4. <appender name="STDOUT"
  5. class="ch.qos.logback.core.ConsoleAppender">
  6. <encoder>
  7. <!-- 日志输出的格式 -->
  8. <!-- 按照顺序分别是:时间、日志级别、线程名称、打印日志的类、日志主体内容、换行 -->
  9. <pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%n</pattern>
  10. <charset>UTF-8</charset>
  11. </encoder>
  12. </appender>
  13. <!-- 设置全局日志级别。日志级别按顺序分别是:DEBUG、INFO、WARN、ERROR -->
  14. <!-- 指定任何一个日志级别都只打印当前级别和后面级别的日志。 -->
  15. <root level="INFO">
  16. <!-- 指定打印日志的appender,这里通过“STDOUT”引用了前面配置的appender -->
  17. <appender-ref ref="STDOUT" />
  18. </root>
  19. <!-- 专门给某一个包指定日志级别 -->
  20. <logger name="com.atguigu" level="DEBUG" additivity="false">
  21. <appender-ref ref="STDOUT" />
  22. </logger>
  23. </configuration>

六、业务功能:登录

1、AuthController

image.png

  1. @Controller
  2. public class AuthController {
  3. @Autowired
  4. private EmpService empService;
  5. @RequestMapping("/auth/login")
  6. public String doLogin(
  7. @RequestParam("loginAccount") String loginAccount,
  8. @RequestParam("loginPassword") String loginPassword,
  9. HttpSession session,
  10. Model model
  11. ) {
  12. // 1、尝试查询登录信息
  13. Emp emp = empService.getEmpByLogin(loginAccount, loginPassword);
  14. // 2、判断登录是否成功
  15. if (emp == null) {
  16. // 3、如果登录失败则回到登录页面显示提示消息
  17. model.addAttribute("message", ImperialCourtConst.LOGIN_FAILED_MESSAGE);
  18. return "index";
  19. } else {
  20. // 4、如果登录成功则将登录信息存入 Session 域
  21. session.setAttribute("loginInfo", emp);
  22. return "target";
  23. }
  24. }
  25. }

2、EmpService

image.png

  1. @Service
  2. @Transactional(readOnly = true)
  3. public class EmpServiceImpl implements EmpService {
  4. @Autowired
  5. private EmpMapper empMapper;
  6. @Override
  7. public Emp getEmpByLogin(String loginAccount, String loginPassword) {
  8. // 1、密码加密
  9. String encodedLoginPassword = MD5Util.encode(loginPassword);
  10. // 2、通过 QBC 查询方式封装查询条件
  11. EmpExample example = new EmpExample();
  12. EmpExample.Criteria criteria = example.createCriteria();
  13. criteria.andLoginAccountEqualTo(loginAccount).andLoginPasswordEqualTo(encodedLoginPassword);
  14. List<Emp> empList = empMapper.selectByExample(example);
  15. if (empList != null && empList.size() > 0) {
  16. // 3、返回查询结果
  17. return empList.get(0);
  18. }
  19. return null;
  20. }
  21. }

3、target.html

image.png

  1. <!DOCTYPE html>
  2. <html lang="en" xml:th="http://www.thymeleaf.org">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. </head>
  7. <body>
  8. <p th:text="${session.loginInfo}"></p>
  9. </body>
  10. </html>

第八章 微服务架构案例

一、创建工程

1、创建工程

image.png

工程名 地位 说明
demo-imperial-court-ms-show 父工程 总体管理各个子工程
demo01-imperial-court-gateway 子工程 网关
demo02-user-auth-center 子工程 用户中心
demo03-emp-manager-center 子工程 员工数据维护中心
demo04-memorials-manager-center 子工程 奏折数据维护中心
demo05-working-manager-center 子工程 批阅奏折工作中心
demo06-mysql-data-provider 子工程 MySQL 数据提供者
demo07-redis-data-provider 子工程 Redis 数据提供者
demo08-base-api 子工程 声明 Feign 接口
demo09-base-entity 子工程 实体类
demo10-base-util 子工程 工具类

2、建立工程间依赖关系

image.png

二、父工程管理依赖

image.png

  1. <dependencyManagement>
  2. <dependencies>
  3. <!-- SpringCloud 依赖导入 -->
  4. <dependency>
  5. <groupId>org.springframework.cloud</groupId>
  6. <artifactId>spring-cloud-dependencies</artifactId>
  7. <version>Hoxton.SR9</version>
  8. <type>pom</type>
  9. <scope>import</scope>
  10. </dependency>
  11. <!-- SpringCloud Alibaba 依赖导入 -->
  12. <dependency>
  13. <groupId>com.alibaba.cloud</groupId>
  14. <artifactId>spring-cloud-alibaba-dependencies</artifactId>
  15. <version>2.2.6.RELEASE</version>
  16. <type>pom</type>
  17. <scope>import</scope>
  18. </dependency>
  19. <!-- SpringBoot 依赖导入 -->
  20. <dependency>
  21. <groupId>org.springframework.boot</groupId>
  22. <artifactId>spring-boot-dependencies</artifactId>
  23. <version>2.3.6.RELEASE</version>
  24. <type>pom</type>
  25. <scope>import</scope>
  26. </dependency>
  27. <!-- 通用 Mapper 依赖 -->
  28. <dependency>
  29. <groupId>tk.mybatis</groupId>
  30. <artifactId>mapper-spring-boot-starter</artifactId>
  31. <version>2.1.5</version>
  32. </dependency>
  33. <!-- Druid 数据源依赖 -->
  34. <dependency>
  35. <groupId>com.alibaba</groupId>
  36. <artifactId>druid-spring-boot-starter</artifactId>
  37. <version>1.1.10</version>
  38. </dependency>
  39. <!-- JPA 依赖 -->
  40. <dependency>
  41. <groupId>javax.persistence</groupId>
  42. <artifactId>persistence-api</artifactId>
  43. <version>1.0</version>
  44. </dependency>
  45. </dependencies>
  46. </dependencyManagement>

三、打基础

1、demo10-base-util

①ImperialCourtConst 常量类

  1. public class ImperialCourtConst {
  2. public static final String LOGIN_FAILED_MESSAGE = "账号、密码错误,不可进宫!";
  3. public static final String ACCESS_DENIED_MESSAGE = "宫闱禁地,不得擅入!";
  4. }

②字符串加密工具类

  1. public class MD5Util {
  2. /**
  3. * 针对明文字符串执行MD5加密
  4. * @param source
  5. * @return
  6. */
  7. public static String encode(String source) {
  8. // 1.判断明文字符串是否有效
  9. if (source == null || "".equals(source)) {
  10. throw new RuntimeException("用于加密的明文不可为空");
  11. }
  12. // 2.声明算法名称
  13. String algorithm = "md5";
  14. // 3.获取MessageDigest对象
  15. MessageDigest messageDigest = null;
  16. try {
  17. messageDigest = MessageDigest.getInstance(algorithm);
  18. } catch (NoSuchAlgorithmException e) {
  19. e.printStackTrace();
  20. }
  21. // 4.获取明文字符串对应的字节数组
  22. byte[] input = source.getBytes();
  23. // 5.执行加密
  24. byte[] output = messageDigest.digest(input);
  25. // 6.创建BigInteger对象
  26. int signum = 1;
  27. BigInteger bigInteger = new BigInteger(signum, output);
  28. // 7.按照16进制将bigInteger的值转换为字符串
  29. int radix = 16;
  30. String encoded = bigInteger.toString(radix).toUpperCase();
  31. return encoded;
  32. }
  33. }

③登录失败异常

  1. public class LoginFailedException extends RuntimeException {
  2. public LoginFailedException() {
  3. }
  4. public LoginFailedException(String message) {
  5. super(message);
  6. }
  7. public LoginFailedException(String message, Throwable cause) {
  8. super(message, cause);
  9. }
  10. public LoginFailedException(Throwable cause) {
  11. super(cause);
  12. }
  13. public LoginFailedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
  14. super(message, cause, enableSuppression, writableStackTrace);
  15. }
  16. }

④远程方法调用统一返回结果

  1. /**
  2. * 统一整个项目中远程方法调用返回的结果
  3. * @author Lenovo
  4. *
  5. * @param <T>
  6. */
  7. public class ResultEntity<T> {
  8. public static final String SUCCESS = "SUCCESS";
  9. public static final String FAILED = "FAILED";
  10. // 用来封装当前请求处理的结果是成功还是失败
  11. private String result;
  12. // 请求处理失败时返回的错误消息
  13. private String message;
  14. // 要返回的数据
  15. private T data;
  16. /**
  17. * 请求处理成功且不需要返回数据时使用的工具方法
  18. * @return
  19. */
  20. public static <Type> ResultEntity<Type> successWithoutData() {
  21. return new ResultEntity<Type>(SUCCESS, null, null);
  22. }
  23. /**
  24. * 请求处理成功且需要返回数据时使用的工具方法
  25. * @param data 要返回的数据
  26. * @return
  27. */
  28. public static <Type> ResultEntity<Type> successWithData(Type data) {
  29. return new ResultEntity<Type>(SUCCESS, null, data);
  30. }
  31. /**
  32. * 请求处理失败后使用的工具方法
  33. * @param message 失败的错误消息
  34. * @return
  35. */
  36. public static <Type> ResultEntity<Type> failed(String message) {
  37. return new ResultEntity<Type>(FAILED, message, null);
  38. }
  39. public ResultEntity() {
  40. }
  41. public ResultEntity(String result, String message, T data) {
  42. super();
  43. this.result = result;
  44. this.message = message;
  45. this.data = data;
  46. }
  47. @Override
  48. public String toString() {
  49. return "ResultEntity [result=" + result + ", message=" + message + ", data=" + data + "]";
  50. }
  51. public String getResult() {
  52. return result;
  53. }
  54. public void setResult(String result) {
  55. this.result = result;
  56. }
  57. public String getMessage() {
  58. return message;
  59. }
  60. public void setMessage(String message) {
  61. this.message = message;
  62. }
  63. public T getData() {
  64. return data;
  65. }
  66. public void setData(T data) {
  67. this.data = data;
  68. }
  69. }

2、demo09-base-entity

①引入依赖

  1. <dependency>
  2. <groupId>javax.persistence</groupId>
  3. <artifactId>persistence-api</artifactId>
  4. </dependency>

②创建实体类

在 MySQL 数据提供服务中用到的通用 Mapper 技术需要借助 @Table 注解将实体类和数据库表关联起来。

  1. @Table(name = "t_emp")
  2. public class Emp implements Serializable {
  3. private Integer empId;
  4. private String empName;
  5. private String empPosition;
  6. private String loginAccount;
  7. private String loginPassword;
  8. public Integer getEmpId() {
  9. return empId;
  10. }
  11. public void setEmpId(Integer empId) {
  12. this.empId = empId;
  13. }
  14. public String getEmpName() {
  15. return empName;
  16. }
  17. public void setEmpName(String empName) {
  18. this.empName = empName == null ? null : empName.trim();
  19. }
  20. public String getEmpPosition() {
  21. return empPosition;
  22. }
  23. public void setEmpPosition(String empPosition) {
  24. this.empPosition = empPosition == null ? null : empPosition.trim();
  25. }
  26. public String getLoginAccount() {
  27. return loginAccount;
  28. }
  29. public void setLoginAccount(String loginAccount) {
  30. this.loginAccount = loginAccount == null ? null : loginAccount.trim();
  31. }
  32. public String getLoginPassword() {
  33. return loginPassword;
  34. }
  35. public void setLoginPassword(String loginPassword) {
  36. this.loginPassword = loginPassword == null ? null : loginPassword.trim();
  37. }
  38. }

四、用户登录认证服务:提供端

1、总体分析

image.png

2、注册中心

在本地启动 Nacos 注册中心:

d:\software\nacos\bin>startup.cmd -m standalone

3、声明接口,暴露服务

①接口文档

点此查看接口文档

② Feign 接口代码

[1]接口位置

image.png

[2]引入依赖
  1. <!-- OpenFeign 专用依赖 -->
  2. <dependency>
  3. <groupId>org.springframework.cloud</groupId>
  4. <artifactId>spring-cloud-starter-openfeign</artifactId>
  5. </dependency>
  6. <!-- 提供 Emp 实体类使用 -->
  7. <dependency>
  8. <groupId>com.atguigu.demo</groupId>
  9. <artifactId>demo09-base-entity</artifactId>
  10. <version>1.0-SNAPSHOT</version>
  11. </dependency>
  12. <!-- 提供 ResultEntity 工具类使用 -->
  13. <dependency>
  14. <groupId>com.atguigu.demo</groupId>
  15. <artifactId>demo10-base-util</artifactId>
  16. <version>1.0-SNAPSHOT</version>
  17. </dependency>

[3]接口代码

注意:@FeignClient 注解中指定的是提供服务的微服务名称,要和注册中心注册的一致

  1. // @FeignClient 注解将当前接口标记为服务暴露接口
  2. // name 属性:指定被暴露服务的微服务名称
  3. @FeignClient(name = "demo06-mysql-data-provider")
  4. public interface MySQLProvider {
  5. @RequestMapping("/remote/get/emp/by/login/info")
  6. ResultEntity<Emp> getEmpByLoginInfo(
  7. // @RequestParam 无论如何不能省略
  8. @RequestParam("loginAccount") String loginAccount,
  9. @RequestParam("loginPassword") String loginPassword);
  10. }

4、实现接口

①所在工程

image.png

②引入依赖

  1. <!-- Nacos 服务注册发现启动器 -->
  2. <dependency>
  3. <groupId>com.alibaba.cloud</groupId>
  4. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  5. </dependency>
  6. <!--通用mapper启动器依赖-->
  7. <dependency>
  8. <groupId>tk.mybatis</groupId>
  9. <artifactId>mapper-spring-boot-starter</artifactId>
  10. </dependency>
  11. <!--mysql驱动-->
  12. <dependency>
  13. <groupId>mysql</groupId>
  14. <artifactId>mysql-connector-java</artifactId>
  15. </dependency>
  16. <!--druid启动器依赖-->
  17. <dependency>
  18. <groupId>com.alibaba</groupId>
  19. <artifactId>druid-spring-boot-starter</artifactId>
  20. </dependency>
  21. <!--web启动器依赖-->
  22. <dependency>
  23. <groupId>org.springframework.boot</groupId>
  24. <artifactId>spring-boot-starter-web</artifactId>
  25. </dependency>
  26. <!--编码工具包-->
  27. <dependency>
  28. <groupId>org.apache.commons</groupId>
  29. <artifactId>commons-lang3</artifactId>
  30. </dependency>
  31. <!--单元测试启动器-->
  32. <dependency>
  33. <groupId>org.springframework.boot</groupId>
  34. <artifactId>spring-boot-starter-test</artifactId>
  35. <scope>test</scope>
  36. </dependency>
  37. <!--热部署 -->
  38. <dependency>
  39. <groupId>org.springframework.boot</groupId>
  40. <artifactId>spring-boot-devtools</artifactId>
  41. <scope>runtime</scope>
  42. <optional>true</optional>
  43. </dependency>
  44. <dependency>
  45. <groupId>com.atguigu.demo</groupId>
  46. <artifactId>demo09-base-entity</artifactId>
  47. <version>1.0-SNAPSHOT</version>
  48. </dependency>
  49. <dependency>
  50. <groupId>com.atguigu.demo</groupId>
  51. <artifactId>demo10-base-util</artifactId>
  52. <version>1.0-SNAPSHOT</version>
  53. </dependency>

③Java 代码

[1]总体结构

image.png

[2]EmpMapper

继承 tk.mybatis.mapper.common.Mapper 后就可以使用通用 Mapper 提供的常规代码实现。除非有非常规需求,否则我们自己什么都不用写。
image.png

  1. public interface EmpMapper extends Mapper<Emp> {
  2. }

[3]Service 接口

image.png

  1. public interface EmpService {
  2. Emp getEmpByLoginInfo(String loginAccount, String loginPassword);
  3. }

[4]Service 实现
  1. @Service
  2. @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
  3. public class EmpServiceImpl implements EmpService {
  4. @Autowired
  5. private EmpMapper empMapper;
  6. @Override
  7. public Emp getEmpByLoginInfo(String loginAccount, String loginPassword) {
  8. String encodedLoginPassword = MD5Util.encode(loginPassword);
  9. Example example = new Example(Emp.class);
  10. example
  11. .createCriteria()
  12. .andEqualTo("loginAccount", loginAccount)
  13. .andEqualTo("loginPassword", encodedLoginPassword);
  14. List<Emp> empList = empMapper.selectByExample(example);
  15. if (empList == null || empList.size() == 0) {
  16. throw new LoginFailedException(ImperialCourtConst.LOGIN_FAILED_MESSAGE);
  17. }
  18. return empList.get(0);
  19. }
  20. }

[5]EmpController

image.png

  1. @RestController
  2. public class EmpController {
  3. @Autowired
  4. private EmpService empService;
  5. @RequestMapping("remote/get/emp/by/login/info")
  6. ResultEntity<Emp> getEmpByLoginInfo(
  7. @RequestParam("loginAccount") String loginAccount,
  8. @RequestParam("loginPassword") String loginPassword) {
  9. try {
  10. Emp emp = empService.getEmpByLoginInfo(loginAccount, loginPassword);
  11. return ResultEntity.successWithData(emp);
  12. } catch (Exception e) {
  13. e.printStackTrace();
  14. String message = e.getMessage();
  15. return ResultEntity.failed(message);
  16. }
  17. }
  18. }

[6]主启动类

image.png

  1. // 为了让当前微服务对接(注册或发现服务)注册中心
  2. @EnableDiscoveryClient
  3. // SpringBoot 标配注解
  4. @SpringBootApplication
  5. // 扫描通用 Mapper 的 Mapper 接口所在包
  6. // 这个注解全类名:tk.mybatis.spring.annotation.MapperScan
  7. // basePackage 属性:指定要扫描的 Mapper 接口所在的包
  8. @MapperScan(basePackages = "com.atguigu.imperial.court.mapper")
  9. public class MainType {
  10. public static void main(String[] args) {
  11. SpringApplication.run(MainType.class, args);
  12. }
  13. }

④YAML 配置文件

image.png

  1. server:
  2. port: 10001
  3. spring:
  4. datasource:
  5. driver-class-name: com.mysql.jdbc.Driver
  6. url: jdbc:mysql://192.168.198.100:3306/db_imperial_court
  7. username: root
  8. password: atguigu
  9. type: com.alibaba.druid.pool.DruidDataSource
  10. application:
  11. name: demo06-mysql-data-provider
  12. cloud:
  13. nacos:
  14. discovery:
  15. server-addr: localhost:8848

五、用户登录认证服务:消费端

1、所在工程

image.png

2、引入依赖

  1. <!-- Nacos 服务注册发现启动器 -->
  2. <dependency>
  3. <groupId>com.alibaba.cloud</groupId>
  4. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  5. </dependency>
  6. <!-- web启动器依赖 -->
  7. <dependency>
  8. <groupId>org.springframework.boot</groupId>
  9. <artifactId>spring-boot-starter-web</artifactId>
  10. </dependency>
  11. <!-- 视图模板技术 thymeleaf -->
  12. <dependency>
  13. <groupId>org.springframework.boot</groupId>
  14. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  15. </dependency>
  16. <dependency>
  17. <groupId>com.atguigu.demo</groupId>
  18. <artifactId>demo08-base-api</artifactId>
  19. <version>1.0-SNAPSHOT</version>
  20. </dependency>

3、YAML 配置文件

image.png

  1. server:
  2. port: 10002
  3. spring:
  4. application:
  5. name: demo02-user-auth-center
  6. cloud:
  7. nacos:
  8. discovery:
  9. server-addr: localhost:8848

就 Thymeleaf 而言,有两个常用属性,但我们全部都使用的是默认值,所以可以省略。

4、显示首页

①配置 view-controller

image.png

  1. @SpringBootConfiguration
  2. public class DemoConfig implements WebMvcConfigurer {
  3. @Override
  4. public void addViewControllers(ViewControllerRegistry registry) {
  5. registry.addViewController("/").setViewName("portal");
  6. }
  7. }

②Thymeleaf 视图模板页面

image.png

  1. <!DOCTYPE html>
  2. <html lang="en" xml:th="http://www.thymeleaf.org">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>首页</title>
  6. </head>
  7. <body>
  8. <form th:action="@{/consumer/do/login}" method="post">
  9. <p style="color: red;font-weight: bold;" th:if="${not #strings.isEmpty(authMessage)}" th:text="${authMessage}">
  10. 这里根据条件显示登录失败消息</p>
  11. <p style="color: red;font-weight: bold;" th:if="${not #strings.isEmpty(systemMessage)}" th:text="${systemMessage}">
  12. 这里根据条件显示系统消息</p>
  13. 账号:<input type="text" name="loginAccount"/><br/>
  14. 密码:<input type="password" name="loginPassword"><br/>
  15. <button type="submit">进宫</button>
  16. </form>
  17. </body>
  18. </html>

5、登录验证

①流程图

image.png

②主启动类

注意:一定要标记 @EnableFeignClients 注解。
image.png

  1. @EnableFeignClients
  2. @EnableDiscoveryClient
  3. @SpringBootApplication
  4. public class MainType {
  5. public static void main(String[] args) {
  6. SpringApplication.run(MainType.class, args);
  7. }
  8. }

③AuthController

[1]装配远程接口分析

image.png

  1. @Controller
  2. public class AuthController {
  3. // 1、本地使用 @Autowired 注解装配远程接口类型即可实现方法的远程调用,
  4. // 看起来就像是调用本地方法一样,我们管这种特性叫方法的声明式远程调用。
  5. // 2、凭啥通过 @Autowired 注解就能够导入远程接口对应的 bean
  6. // ①当前环境包含 Feign 相关 jar 包。
  7. // ②当前微服务的主启动类上标记 @EnableFeignClients
  8. // ③符合 SpringBoot 自动扫描包的约定规则:默认情况下主启动类所在的包、以及主启动类所在包的子包都会被自动扫描
  9. // 主启动类所在包: com.atguigu.imperial.court
  10. // 被扫描的接口所在的包:com.atguigu.imperial.court.api
  11. @Autowired
  12. private MySQLProvider mySQLProvider;
  13. }

#[2]执行登录验证的方法
  1. @RequestMapping("/consumer/do/login")
  2. public String doLogin(@RequestParam("loginAccount") String loginAccount,
  3. @RequestParam("loginPassword") String loginPassword, HttpSession session, Model model) {
  4. // 1、调用远程接口根据登录账号、密码查询 Emp 对象
  5. ResultEntity<Emp> resultEntity = mySQLProvider.getEmpByLoginInfo(loginAccount, loginPassword);
  6. // 2、验证远程接口调用是否成功
  7. String result = resultEntity.getResult();
  8. if ("SUCCESS".equals(result)) {
  9. // 3、从 ResultEntity 中获取查询得到的 Emp 对象
  10. Emp emp = resultEntity.getData();
  11. // 4、将 Emp 对象存入 Session 域
  12. session.setAttribute("loginInfo", emp);
  13. // 5、前往 target 页面
  14. return "target";
  15. } else {
  16. // 6、获取失败消息
  17. String message = resultEntity.getMessage();
  18. // 7、将失败消息存入模型
  19. model.addAttribute("message", message);
  20. // 8、回到登录页面
  21. return "index";
  22. }
  23. }

六、部署运行

1、最终目标

image.png

2、微服务打包

①修改 MySQL 连接信息

image.png

  1. server:
  2. port: 10001
  3. spring:
  4. datasource:
  5. driver-class-name: com.mysql.jdbc.Driver
  6. # 当前微服务和 MySQL 位于同一个服务器上,需要把访问地址改成 localhost
  7. url: jdbc:mysql://localhost:3306/db_imperial_court
  8. username: root
  9. password: atguigu
  10. type: com.alibaba.druid.pool.DruidDataSource
  11. application:
  12. name: demo06-mysql-data-provider
  13. cloud:
  14. nacos:
  15. discovery:
  16. server-addr: localhost:8848

②在父工程执行 install 命令

[1]Why parent?工程间关系梳理

正确的安装顺序:

  • ①父工程:pro07-demo-imperial-court-micro-service
  • ②被依赖的 module:demo10-base-util 或 demo09-base-entity
  • ③当前 module:demo06-mysql-data-provider

image.png

[2]执行命令

image.png

③生成微服务可运行 jar 包

[1]应用微服务打包插件

可以以 SpringBoot 微服务形式直接运行的 jar 包包括:

  • 当前微服务本身代码
  • 当前微服务所依赖的 jar 包
  • 内置 Tomcat(Servlet 容器)
  • 与 jar 包可以通过 java -jar 方式直接启动相关的配置

要加入额外的资源、相关配置等等,仅靠 Maven 自身的构建能力是不够的,所以要通过 build 标签引入下面的插件。

  1. <!-- build 标签:用来配置对构建过程的定制 -->
  2. <build>
  3. <!-- plugins 标签:定制化构建过程中所使用到的插件 -->
  4. <plugins>
  5. <!-- plugin 标签:一个具体插件 -->
  6. <plugin>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-maven-plugin</artifactId>
  9. </plugin>
  10. </plugins>
  11. </build>

加入这个插件后的效果:
image.png
提示:IDEA 对于我们这里 build 标签里加入的 plugin 的配置没有能够很好的识别到插件的版本。如果我们能够保证其它操作都正常执行完成,准备工作都准备好了,那么这里我们判定是 IDEA 识别能力不足导致。一切以实际执行的结果为准:运行结果是最高权威
请对 demo02-user-auth-center 和 demo06-mysql-data-provider 都添加上面的 build 配置。

[2]执行插件目标

请对 demo02-user-auth-center 和 demo06-mysql-data-provider 都执行下面的命令:

  • clean 子命令:清理之前构建的结果
  • package 子命令:我们真正要调用的 spring-boot:repackage 要求必须将当前微服务本身的 jar 包提前准备好,所以必须在它之前执行 package 子命令。
  • spring-boot:repackage 子命令:调用 spring-boot 插件的 repackage 目标
  • -Dmaven.test.skip=true 参数:跳过测试

    mvn clean package spring-boot:repackage -Dmaven.test.skip=true

    3、执行部署

    ①启动 Nacos

    1. sh /opt/nacos/bin/startup.sh -m standalone

    ②上传微服务 jar 包

    image.png

    ③启动微服务

    1. nohup java -jar demo06-mysql-data-provider-1.0-SNAPSHOT.jar>demo06.log 2>&1 &
    2. nohup java -jar demo02-user-auth-center-1.0-SNAPSHOT.jar>demo02.log 2>&1 &