项目实战:系统公告设计及实现

本期涉及内容:JDBC点击即可跳转复习JDBC

1.需求及业务设计

项目(公告系统)

1.需求(为行政人员发布公告,通知提供遍历)

2.原型设计(系统做完以后是什么样子的,先做一个设计稿)

3.表的设计(公告内容中包含哪些字段)

4.技术分层架构(分而治之-将复杂问题简单化)

5.技术栈的设计(数据库端技术,服务端技术,客服端技术)

目的:以项目为驱动,讲解技术的应用。


SpringBoot技术

1.是什么?(基于spring技术实现的一个脚手架)

2.基于此脚手架要做什么?(快速构建项目开发环境)

3.为什么使用springboot搭建环境?(提供了开箱即用的特性-依赖,自动配置…)

4.springboot工程的创建,目录结构,启动分析。
   4.1 创建(基于http://start.spring.io官方提供的构建结构进行实现)

   4.2 目录结构
      4.2.1)src/main/ java (java业务代码)
      4.2.2)src/main/resources(项目配置文件,静态资源)
      4.2.3)src/test/java(单元测试代码)
      4.2.4)pom. xm1(服务端依赖, maven插件配置)

   4.3 项目的启动
      4.3.1 启动类(@SpringBootTest)
      4.3.2 启动过程(了解启动过程)


FAQ?

  1. 我们写的类所在包与启动类所在包要建立什么关系?

  2. 启动类在启动时我如何知道加载了哪些类?    (-XX:+TraceClassLoading)


HikariCP连接池

1.池化思想(空间换时间,实现对象复用,进行减少对象创建销毁带来的系统开销)
   1.1 整数池(Integer[)
   1.2 字符串(char[])
   1.3 线程池(ThreadPoolExecutor)
   1.4 对象池(Spring中的bean池)
   1.5 …

2.Java中连接池规范(DataSource)
   2.1 为什么要创建连接池?(连接的创建和销毁非常耗时-TCP/IP-3次握手,4次挥手)
   2.2 java中连接池的规范?(javax. sql. DataSource)
   2.3 java中连接池的江湖?(c3p0, dbcp, druid, hikariCP,. . . )
   2.4 连接池设计思考?(数据存储结构,算法,线程安全,参数设计,…)

3.Java中的连接池规范实现-HikariCP
   3.1 特点:日本人->设计小而精,性能高,稳定
   3.2 SpringBoot 默认优先加载?(添加了spring-jdbc依赖会自动配置hikaricp)
   3.3 连接池的测试应用?
      3.3.1 添加依赖?(mysql驱动,spring-jdbc依赖)
      3.3.2 连接数据库的配置(url,username,password)
      3.3.3 构建单元测试类及方法对连接池进行单元测试?(获取连接)
      3.3.4 连接获取原理分析?(@Test->DataSource->HikariDataSource->HikariPool->.)

业务描述

      系统公告是方便公司行政管理人员向公司全员或部分指定人员发送通知公告的模块。如开会通知、放假通知及其它信息。使用户可以很方便的了解公司动向。

1.2系统原型设计

公告列表页面,如图所示:
在这里插入图片描述
公告编辑页面,如图所示:
在这里插入图片描述

1.3数据库及表设计

基于系统公告业务,现对公告表进行设计,sql 脚本如下:

drop database if exists db_notice;
create database if not exists db_notice default character set utf8; use db_notice
drop table if exists sys_ notice;
create table sys_ notice (
id             int(4) auto_ increment     comment'ID',
title          varchar(50) not null       comment'标题 ',
type           char(1) not null           comment'类型(1通知 2公告),
content        varchar(500) default null  comment'公告内容',
status         char(1) default '0'        comment'状态(0 正常 1关闭)',
createdUser    varchar(64) default''      comment'创建者' ,
createTime      datetime                  comment'创建时间',
modifiedUser    varchar(64) default''     comment'更新者',
modifiedTime    datetime                  comment'更新时间',
remark          varchar( 255)             comment'备注',
primary key (id)
) engine=innodb auto_increment=1 comment = '通知公告表';

 

1.4技术架构分层设计

       系统分层设计是一种设计思想(分而治之),是让每层对象都有一个独立职责,再让多层对象协同(耦合)完成一个完整的功能。这样做可以更好提高系统可扩展性,但同时也会增加系统整体运维的难度。
在这里插入图片描述
       其中,在上图中的箭头表示一种直接依赖关系,开放接口层可以依赖于 Web 层,也可以直接依赖于 Service 层,其它依此类推(具体每层要实现的逻辑可自行查阅阿里巴巴开发手册)。

1.5技术栈中技术选型设计

2.SpringBoot 技术

2.1 简介

      JAVAEE 应用体系中繁重的配置、低下的开发效率、高难度的三方集成,复杂的部署流程等等一直被开发人员所诟病。
      Spring 这样的轻量级的资源整合框架,在实现其相对比较多的资源整合时,依旧需要大量的手动依赖管理,复杂的 XML 配置(还经常没有提示)。
      现在的软件生态应用也已经形成一定的规模,系统架构正在从单体架构,分布式架构, 跨越到微服务架构。随着整个架构体系的变化,企业对技术的要求也在变化,现在的企业更注重技术的开箱即用,更注重技术在生态圈中的深度融合,更注重轻量级的运维。由此由此spring boot 诞生。
      Spring Boot 是 Java 软件开发框架(很多人现在把它理解为一个脚手架),其设计目的是用来简化 Spring 项目的初始搭建以及开发过程。该框架使用了特定的注解方式来进行配置,从而使开发人员不再需要大量的 xml 配置。不再需要大量的手动依赖管理。Spring Boot 基于快速构建理念,通过约定大于配置,开箱即用的方式,希望能够在蓬勃发展的快速应用开发领域成为其领导者。

2.2 关键特性

SpringBoot 其核心特性包含如下几个方面:

  • 起步依赖(Starter Dependency)
  • 自动配置(Auto Configuration)
  • 健康检查(Actator)-监控
  • 嵌入式服务(Tomcat,Jetty)等

2.3 工程创建及启动

SpringBoot 工程中由 SpringBootApplication 注解描述的类为启动入口类,例如:

在这里插入图片描述

package com.cy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; 
@SpringBootApplication
public class Application {//Application.class
   public static void main(String[] args) {//Main Thread
      SpringApplication.run(Application.class,args);
   }
}

运行启动类检测输出结果,了解其启动过程,如图所示:
在这里插入图片描述
其中,项目在启动时要做哪些基础操作。

  1. 基于线程调用i/o 从磁盘读取类,将其加载到内存,此时会基于类创建字节码对象(其类型为 Class 类型)

  2. 基于 Class 对象( 字节码对象) 读取类的配置信息( 例如类上有什么注解- 例如@Component,属性上有什么注解… )

  3. 基于类的配置进行相应的配置存储 ( 交给 spring 管理的类的配置 )- Map<String,BeanDefinition>

  4. 基 于 类 的 配 置 借 助 BeanFactory 创 建 类 的 实 例 ( 对象 ), 多 个 对 象 存 储 到Map<String,Object>

3HikariCP 连接池应用

      池化思想是我们项目开发过程中的一种非常重要的思想,如整数池,字符串池,对象池、连接池、线程池等都是池化思想一种应用,都是通过复用对象,以减少因创建和释放对象所带来的资源消耗,进而来提升系统性能。

例如 Integer 对象的内部池应用,代码如下:

package com.cy.java.pool;
public class TestInteger01 {
   public static void main(String[] args) {
        //演示整数池(-128 ~ 127)
        Integer n1=100;//Integer.valueOf(100) 编译时优化
        Integer n2=100; 
        //对整数而言为什么不将所有整数都放到池中,而只是存储了一部分数据呢?
        //池设计的目的是? 以空间换时间,这块空间中应该存储一些常用的整数数据
        Integer n3=200;
        Integer n4=200;// 池 中 没 有 则 new Integer(200) 
        System.out.println(n1==n2);//true,池存储着-128~+127 
        System.out.println(n3==n4);//false
        //所有的池设计都会用到一种设计
     }
}

      项目开发过程中应用程序与数据库交互时,“获得连接”或“释放连接”是非常消耗系统资源的两个过程,频繁地进行数据库连接的建立和关闭会极大影响系统的性能,若多线程并发量很大,这样耗时的数据库连接就可能让系统变得卡顿。因为 TCP 连接的创建开支十分昂贵,并且数据库所能承载的 TCP 并发连接数也有限制,针对这种场景,数据库连接池应运而生。如图-1 所示:
在这里插入图片描述
      Java 官方,为了在应用程序中更好的应用连接池技术,定义了一套数据源规范,例如javax.sql.DataSource 接口,基于这个接口,很多团队或个人创建了不同的连接池对象。然后我们的应用程序中通过耦合与 DataSource 接口,便可以方便的切换不同厂商的连接池。Java 项目中通过连接池获取连接的一个基本过程,如图-2 所示:
在这里插入图片描述

      在图-2 中,用户通过 DataSource 对象的 getConnection()方法,获取一个连接。假如池中有连接,则直接将连接返回给用户。假如池中没有连接,则会调用 Dirver(驱动, 由数据库厂商进行实现)对象的 connect 方法从数据库获取,拿到连接以后,可以将连接在池中放一份,然后将连接返回给调用方。连接需求方再次需要连接时,可以从池中获取, 用完以后再还给池对象。

      数据库连接池的江湖。

      数据库连接池在 Java 数据库相关中间件产品群中,应该算是底层最基础的一类产品, 作为企业应用开发必不可少的组件,无数天才们为我们贡献了一个又一个的优秀产品,它们有的随时代发展,功成身退,有的则还在不断迭代,老而弥坚,更有新生代产品,或性能无敌,或功能全面。目前市场上常见的连接池有 DBCP、C3P0,DRUID,HikariCP 等。
      数据库连接池的原理分析。
      在系统初始化的时候,在内存中开辟一片空间,将一定数量的数据库连接作为对象存储在对象池里,并对外提供数据库连接的获取和归还方法。用户访问数据库时,并不是建立一个新的连接,而是从数据库连接池中取出一个已有的空闲连接对象;使用完毕归还后的连接也不会马上关闭,而是由数据库连接池统一管理回收,为下一次借用做好准备。如果由于高并发请求导致数据库连接池中的连接被借用完毕,其他线程就会等待,直到有连接被归还。整个过程中,连接并不会关闭,而是源源不断地循环使用,有借有还。数据库连接池还可以通过设置其参数来控制连接池中的初始连接数、连接的上下限数,以及每个连接的最大使用次数、最大空闲时间等,也可以通过其自身的管理机制来监视数据库连接的数量、使用情况等。

       数据库连接池的构成分析。

      数据库连接池以连接池的管理为核心,主要支持连接池的建立和释放这两大核心功能。“麻雀虽小,五脏俱全”,数据库连接池还可以支持其他非常实用的功能。一款商用的数据库连接池、一款能够被开发者广泛使用的数据库连接池、一款能够在开源社区持续活跃发展的数据库连接池还必须再支持一些实用的功能,如并发(锁性能优化乃至无锁)、连接数控制(不同的系统对连接数有不同的需求)、监控(一些自身管理机制来监视连接的数量及使用情况等)、外部配置(各种主流数据库连接池官方文档最核心的部分)、资源重用(数据库连接池的核心思想)、检测及容灾(面对一些网络、时间等问题的自愈)、多库多服务(如不同的数据库、不同的用户名和密码、分库分表等情况)、事务处理(对数据库的操作符合 ALL- ALL-NOTHING 原则)、定时任务(如空闲检查、最小连接数控制)、缓存(如 PSCache 等避免对 SQL 重复解析)、异常处理(对 JDBC 访问的异常统一处理)、组件维护(如连接状态、JDBC 封装的维护)等。

3.2数据初始化

打开 mysql 控制台,然后按如下步骤执行 notice.sql 文件。
链接:https://pan.baidu.com/s/1J8ZOFDoiP1ic4aXWGaW8XQ
提取码:k7xg

第一步:登录 mysql。

mysql –uroot –proot

第二步:设置控制台编码方式。

set names utf8;

第三步:执行 notice.sql 文件(切记不要打开文件复制到 mysql 客户端运行)。

source d:/notice.sql

在这里插入图片描述

3.3快速入门实践

      连接池产品 HikariCP 拥有强劲的性能和稳定性,再加上它自身小巧的身形,在当前的“云时代、微服务”的背景下,HakariCP 得到了越来越多的人青睐。HiKariCP 号称是目

前世界上最快的连接池,有江湖一哥的称号。

3.3.1添加依赖

添加依赖,其 pom 文件关键元素如下:

<!--链接 mysql 时使用的 mysql 驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<!--当添加 data-jdbc 依赖时会自动下载 HikariCp 依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>

打开Settings -> Plugins ->下载插件
在这里插入图片描述在这里插入图片描述启动快捷键 Alt+insert
在这里插入图片描述选择ok
在这里插入图片描述
可以选择需要增加的依赖
在这里插入图片描述

3.3.2基本参数配置

打开 application.properties 配置文件,添加如下内容。

# spring datasource
spring.datasource.url=jdbc:mysql:///db_notice?serverTimezone=GMT%2B8 
spring.datasource.username=root
spring.datasource.password=root

hikariCP 其它额外配置,可通过搜索引擎进行查阅。

3.3.3编写及运行单元测试。

在项目中添加单元测试类及测试方法,代码如下:
在这里插入图片描述

package com.cy.pj.sys.dao;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.sql.DataSource;
import java.sql.*;

/**
 * 为springboot中的单元测试类
 * 说明:
 * 1.springboot中的单元测试类必须放在启动类所在包或子包中
 * 2.springboot中的单元测试类必须使用@SpringBootTest注解描述
 */
@SpringBootTest
public class DataSourceTests {
    /**
     * 在项目中添加了数据库相关依赖以后,
     * springboot底层会自动帮我们配置一个数据源(DataSource)对象,
     * 此对象是连接池的规划。
     * @Autowired 注解描述属性时,是告诉spring框架,要基于反射机制为属性赋值(依赖注入)
     * 赋值时,首先会基于属性类型从spring容器查找相匹配的对象,假如只有一个则直接注入
     * 有多个相同类型的对象时,还会比较属性名(检查属性名是否与bean名字相同),有相同的
     * 则直接注入(没有相同的则直接抛出异常)
     *
     */

    @Autowired
    //private javax.sql.DataSource dataSource; //引入包名千万不要引入错了
    private DataSource dataSource; //简写
    @Test //org.junit.jupiter.api.Test
    void testGetConnection() throws SQLException {
        //获取链接时,
        Connection conn = dataSource.getConnection();
        System.out.println("conn="+conn);
        //conn=HikariProxyConnection@1269826537 wrapping com.mysql.cj.jdbc.ConnectionImpl@12aa4996
    }
}

第四步:API 运行原理分析
在这里插入图片描述

小结:


SpringBoot技术
1.是什么?
基于spring技术实现的一个脚手架。

2.基于此脚手架需要做什么?
快速构建项目开发环境。

3.为什么要使用springboot搭建环境?
提供了开箱即用的特性(依赖),自动装箱…

4.springboot工程的创建,目录结构,启动分析
1)创建(基于http://start.spring.io官方提供的构建结构进行实现)

2)目录结构?
   2.1)src/main/java (java业务代码)
   2.2)src/main/resoutces (项目配置文件。静态资源)
   2.3)src/test/java (单元测试代码)
   2.4)pom.xml(服务端依赖,maven插件配置)

3)项目的启动
   3.1)启动类(@SpringBootTest)
   3.2)启动过程(了解启动过程)

FAQ?

1)我们写的类所在包与启动类所在包要建立什么关系? .
2)启动类在启动时我如何知道加载了哪些类? (-XX: +TraceClassLoading)


HikariCP连接池

1.池化思想 (空间换时间,实现对象复用,进行减少对象创建销毁带来的系统开销)
  1)整数池(Integer[])
  2)字符串(char[])
  3)线程池(ThreadPoolExecutor)
  4)对象池(Spr ing中的bean池)
  5)…


2.Java中连接池规范(DataSource)
  1)为什么要创建连接池?(连接的创建和销毁非常耗时-TCP/IP-3次握手,4次挥手)
  2) java中连接池的规范? (javax. sql. DataSource)
  3) java中连接池的江湖? (c3p0, dbcp , Druid , hikariCP ,…)
  4)连接池设计思考?(数据存储结构,算法,线程安全,参数设计,… )

3.Java中的连接池规范实现-HikariCP
  1)特点:日本人->设计小而精,性能高,稳定…
  2)SpringBoot默认优先加载? (添加了spring - jdbc依赖会自动配置hikaricp)
  3)连接池的测试应用?
   3.1)添加依赖? (mysq 1驱动, spring- jdbc依赖)
   3.2)连接数据库的配置(url,username,password)
   3.3)构建单元测试类及方法对连接池进行单元测试?(获取连接)
   3.4)连接获取原理分析?(@Test->DataSource->HikariDataSource->HikariPool->.)



3.4JDBC 操作练习

基于 JDBC 技术查询数据库表中的通告信息,并进行单元测试。

连接数据库

在这里插入图片描述

# spring datasource
spring.datasource.url=jdbc:mysql:///db_notice?serverTimezone=GMT%2B8&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=root

第一步:定义单元测试类,关键代码如下:

package com.cy.pj.sys.dao;
import org.junit.jupiter.api.Test;
import javax.sql.DataSource;
import java.sql.*;
/**
* 通过此单元测试类获取数据源对象,并且通过数据对象获取数据库连接
* @SpringBootTest 注解描述的类
* 为 springboot 中的单元测试类
* 说明:
* 1)springboot 中的单元测试类必须放在启动类所在包
* 或子包中
* 2)springboot 中的单元测试类必须使用@SpringBootTest 注解描述
*/
@SpringBootTest
public class JdbcTests {//is a Object
 @Autowired
 private DataSource dataSource;//HikariDataSource (类)
}

第二步:在单元测试类中添加向表中写入数据的方法,关键代码如下:

@Test
 void testSaveNotice01()throws SQLException{
 //1.建立连接 (负责与数据库进行通讯)
 Connection conn= dataSource.getConnection();
 //2.创建 statement(sql 传送器->负责与将 sql 发送到数据库端)
 String sql="insert into sys_notice " +
 " (title,content,type,status,
createdTime,createdUser,modifiedTime,modifiedUser) " +
 " values ('加课通知','本周六加课','1','0',
now(),'tony',now(),'tony') ";
//这种方式 Statement 的创建,适合 sql 中不需要动态传入值的方式。
 Statement stmt=conn.createStatement();
 //3.发送 sql
 stmt.execute(sql);
 //4.处理结果
 //5.释放资源(后续释放资源要写到 finally 代码块中)
 stmt.close();
 conn.close();//将连接返回池中
 }

第三步:在类中定义向表中写入数据的另一种方式,关键代码如下:

 @Test
 void testSaveNotice02()throws SQLException{
 //1.建立连接 (负责与数据库进行通讯)
 Connection conn= dataSource.getConnection();
 //2.创建 statement(sql 传送器->负责与将 sql 发送到数据库端)
 String sql="insert into sys_notice " +
 " (title,content,type,status,
createdTime,createdUser,modifiedTime,modifiedUser) " +
 " values (?,?,?,?,?,?,?,?) ";//?表示占位符
 PreparedStatement stmt=
conn.prepareStatement(sql);//预编译方式创建 Statement 对象
 //3.发送 sql
 //3.1 为 sql 中的?号赋值
 stmt.setString(1,"开学通知");
 stmt.setString(2,"2021 年 3 月 18 号 开学");
 stmt.setString(3,"1");
 stmt.setString(4,"0");
 stmt.setTimestamp(5,
new Timestamp(System.currentTimeMillis()));
 stmt.setString(6,"jason");
 stmt.setTimestamp(7,
new Timestamp(System.currentTimeMillis()));
 stmt.setString(8,"tony");
 //3.2 发送 sql
 stmt.execute();
 //4.处理结果
 //5.释放资源(后续释放资源要写到 finally 代码块中)
 stmt.close();
 conn.close();//将连接返回池中
 }
}

第四步,添加查询通知的单元测试方法,关键代码如下:

@Test
void testSelectNotices01()throws SQLException{
 //1.建立连接
 Connection conn=dataSource.getConnection();
 //2.创建 Statement
 String sql="select id,title,content,status,type,createdTime "
+" from sys_notice where id>=?";
 PreparedStatement pstmt=conn.prepareStatement(sql);
 //3.发送 sql(发送到数据库)
 pstmt.setInt(1,2);
 boolean flag=pstmt.execute();
 //4.处理结果
 ResultSet rs=null;
 if(flag){//true 表示查询,有结果集
 //获取结果集(二维表结构)
 rs=pstmt.getResultSet();
 List<Map<String,Object>> list=new ArrayList<>();
 while(rs.next()){//一行记录应为一个 map 对象 (行映射)
 //构建 map,用于存储当前行记录
 Map<String,Object> map=new HashMap();
 //将取出类的数据存储到 map (key 为字段名,值为字段 value)
 map.put("id",rs.getInt("id"));
 map.put("title",rs.getString("title"));
 map.put("content",rs.getString("content"));
 map.put("status",rs.getString("status"));
 map.put("type",rs.getString("type"));
 map.put("createdTime",
rs.getTimestamp("createdTime"));
 //...
 //将每行记录对应的 map 对象存储到 list 集合
 System.out.println(map);
 list.add(map);
 }
 }
 //5.释放资源
 rs.close();
 pstmt.close();
 conn.close();
 }

第五步,通过元数据让查询映射更加灵活,关键代码如下:

 @Test
 void testSelectNotices02()throws SQLException{
 //1.建立连接
 Connection conn=dataSource.getConnection();
 //2.创建 Statement
 String sql="select id,title,content,status,type,createdTime "
+" from sys_notice where id>=?";
 PreparedStatement pstmt=conn.prepareStatement(sql);
 //3.发送 sql(发送到数据库)
 pstmt.setInt(1,2);
 boolean flag=pstmt.execute();
 //4.处理结果
 ResultSet rs=null;
 if(flag){//true 表示查询,有结果集
 //获取结果集(二維表結構)
 rs=pstmt.getResultSet();
 List<Map<String,Object>> list=new ArrayList<>();
 //获取结果集中的元 shuju (表名,字段名)
 ResultSetMetaData rsmd=rs.getMetaData();
 while(rs.next()){//一行记录应为一个 map 对象 (行映射)
 Map<String,Object> map=new HashMap();
 //将取出类的数据存储到 map (key 为字段名,值为字段 value)
 for(int i=1;i<=rsmd.getColumnCount();i++){
//getColumnCount();獲取列的數量
 map.put(rsmd.getColumnLabel(i),
rs.getObject(rsmd.getColumnLabel(i)));
 //rsmd.getColumnLabel(i) 获取第 i 列的名字
 }
 //将每行记录对应的 map 对象存储到 list 集合
 System.out.println(map);
 list.add(map);
 }
 }
 //5.释放资源
 rs.close();
 pstmt.close();
 conn.close();
 }

执行测试两种连接方式:

getConnection() 方法,连接MySQL数据库。

statement(sql传送器->负责与将sql发送到数据库端)

setTimestamp 方法,使用 Date 型设置指定列的值(在mysql中 date数据类型 只能存放年月日,所以只能用datetime类型,那在jdbc中,就要调用setTimestamp()方法,取出数据的时候,用结果集调用getTimstamp()方法。)

currentTimeMillis()计算方式与时间的单位转换

package com.cy.pj.sys.dao;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.sql.DataSource;
import java.sql.*;

/**
 * 为springboot中的单元测试类
 * 说明:
 * 1.springboot中的单元测试类必须放在启动类所在包或子包中
 * 2.springboot中的单元测试类必须使用@SpringBootTest注解描述
 */
@SpringBootTest
public class DataSourceTests {
    /**
     * 在项目中添加了数据库相关依赖以后,
     * springboot底层会自动帮我们配置一个数据源(DataSource)对象,
     * 此对象是连接池的规划。
     * @Autowired 注解描述属性时,是告诉spring框架,要基于反射机制为属性赋值(依赖注入)
     * 赋值时,首先会基于属性类型从spring容器查找相匹配的对象,假如只有一个则直接注入
     * 有多个相同类型的对象时,还会比较属性名(检查属性名是否与bean名字相同),有相同的
     * 则直接注入(没有相同的则直接抛出异常)
     *
     */

    @Autowired
    //private javax.sql.DataSource dataSource; //引入包名千万不要引入错了
    private DataSource dataSource; //简写
    @Test //org.junit.jupiter.api.Test
    void testGetConnection() throws SQLException {
        //获取链接时,
        Connection conn = dataSource.getConnection();
        System.out.println("conn="+conn);
        //conn=HikariProxyConnection@1269826537 wrapping com.mysql.cj.jdbc.ConnectionImpl@12aa4996
    }
    @Test
    void testSaveNotice01()throws SQLException{//homework (通过此方法基于jdbc向数据库写入一条数据)
        //JDBC (是java中推出的连接数据库的一组API,是规范)
        //数据库厂商提供JDBC驱动(jdbc规范的实现)负责实现数据库的操作.
        //1.建立连接 (负责与数据库进行通讯)
        Connection conn= dataSource.getConnection();
        //2.创建statement(sql传送器->负责与将sql发送到数据库端)
        String sql="insert into sys_notice " +
                " (title,content,type,status,createTime,createdUser,modifiedTime,modifiedUser) " +
                "  values ('加课通知','本周六加课','1','0',now(),'tony',now(),'tony') ";
        Statement stmt=conn.createStatement();
        //3.发送sql
        stmt.execute(sql);
        //4.处理结果
        //5.释放资源(后续释放资源要写到finally代码块中)
        stmt.close();
        conn.close();//将连接返回池中
    }
    @Test
    void testSaveNotice02()throws SQLException{//homework (通过此方法基于jdbc向数据库写入一条数据)
        //JDBC (是java中推出的连接数据库的一组API,是规范)
        //数据库厂商提供JDBC驱动(jdbc规范的实现)负责实现数据库的操作.
        //1.建立连接 (负责与数据库进行通讯)
        Connection conn= dataSource.getConnection();
        //2.创建statement(sql传送器->负责与将sql发送到数据库端)
        String sql="insert into sys_notice " +
                " (title,content,type,status,createTime,createdUser,modifiedTime,modifiedUser) " +
                "  values (?,?,?,?,?,?,?,?) ";//?表示占位符
        PreparedStatement stmt=conn.prepareStatement(sql);//预编译方式创建Statement对象
        //3.发送sql
        stmt.setString(1,"开学通知");
        stmt.setString(2,"2021年2月18号 开学");
        stmt.setString(3,"1");
        stmt.setString(4,"0");
        stmt.setTimestamp(5,new Timestamp(System.currentTimeMillis()));
        stmt.setString(6,"jason");
        stmt.setTimestamp(7,new Timestamp(System.currentTimeMillis()));
        stmt.setString(8,"tony");
        stmt.execute();
        //4.处理结果
        //5.释放资源(后续释放资源要写到finally代码块中)
        stmt.close();
        conn.close();//将连接返回池中
    }
}

测试两种连接数据库 运行结果:
在这里插入图片描述

分析:

1.通过池中的连接借助JDBCAPI向数据库表中写入一 条通告信息.
  1.1)JDBC编码步骤?
  1.2)建立连接(Connect ion, Da taSource)
  1.3)创建Statement (Statement, PreparedS tatement)
  1.4)发送SQL
  1.5)处理结果(查询操作需要处理结果)
  1.6)释放资源

2.JDBC编程过程的问题分析
  2. 1)步骤固定(记住了过后,有手就能写,没必要反复写)
  2. 2)占位符参数赋值代码简单,但需要重复编写.
  2.3)行映射远程代码比较简单,但是需要的代码量比较大,需要反复编写
  2.4)SQL语句不够灵活(不支持分支结构,很难基于条件进行动态SQL的定制)

思考第三种查询的方法
     //第三种连接方式(查询)
    @Test
    void testSelectNotice( )throws SQLException{
        //1.建立连接
        Connection conn=dataSource.getConnection();
        //2.创建查询语句,基于id查询
        String sq1="select id,title,content,status, type,createTime "+
                " from sys_notice where id >= ?";
        PreparedStatement pstmt = conn.prepareStatement(sq1);
        //3.发送sql
        pstmt.setInt(1, 2);
        boolean flag=pstmt.execute();
        //4.处理结果
        if (flag){//true表示查询,有结果集
            ResultSet rs=pstmt.getResultSet();
            List<Map<String,Object>> list=new ArrayList<>();

            while (rs.next()){//一行记录应为一map对象(行映射)
                Map<String,Object> map=new HashMap();//将来也可以使用pojo对象
                // 将取出类的数据存储到map(key为字段名,值为字段value)
                map.put("id",rs.getInt("id"));//取第一列的值
                map.put("title",rs.getString("title"));//取第二列的值
                map.put("content",rs.getString("content"));//取第二列的值
                map.put("status",rs.getString("status"));//取第二列的值
                map.put("type",rs.getString("type"));//取第二列的值
                map.put("createTime",rs.getTimestamp("createTime"));//取第二列的值
                //将每行记录对应的map对象存储到List集合
                System.out.println(map);
                list.add(map);
            }
            rs.close();
        }
        //5.释放资源
        pstmt.close();
        conn.close();
    }
}

运行结果:
在这里插入图片描述

在这里插入图片描述

思考如何优化?
    //第四种连接方式(优化)
    @Test
    void testSelectNotice2( )throws SQLException{
        //1.建立连接
        Connection conn=dataSource.getConnection();
        //2.创建查询语句,基于id查询
        String sql="select id,title,content,status,type,createTime " +
                " from sys_notice where id>=?";
        PreparedStatement pstmt=conn.prepareStatement(sql);
        //3.发送sql(发送到数据库)
        pstmt.setInt(1,2);
        boolean flag=pstmt.execute();

        //4.处理结果
        //获取结果集中的元数据
        if (flag){//true表示查询,有结果集
            ResultSet rs=pstmt.getResultSet();
            List<Map<String,Object>> list=new ArrayList<>();
            //获取结果集中的元shuju (表名,字段名)
            ResultSetMetaData rsmd=rs.getMetaData();
            while (rs.next()){//一行记录应为一map对象(行映射)
                Map<String,Object> map=new HashMap();//将来也可以使用pojo对象(HashMap底层重写了toString方法)
                // 将取出类的数据存储到map(key为字段名,值为字段value)
                for (int i = 1; i < rsmd.getColumnCount(); i++) {
                    map.put(rsmd.getColumnLabel(i), rs.getObject(rsmd.getColumnLabel(i)));
                    //rsmd.getColumnLabel(i) 获取第i列的名字
                }
                //下面这种写法更加简单,但是代码量比较大
              /*  map.put("id",rs.getInt("id"));//取第一列的值
                map.put("title",rs.getString("title"));//取第二列的值
                map.put("content",rs.getString("content"));//取第二列的值
                map.put("status",rs.getString("status"));//取第二列的值
                map.put("type",rs.getString("type"));//取第二列的值
                map.put("createTime",rs.getTimestamp("createTime"));//取第二列的值*/
                //将每行记录对应的map对象存储到List集合
                System.out.println(map);
                list.add(map);
            }
            rs.close();
        }
        //5.释放资源
        pstmt.close();
        conn.close();
    }
}

运行结果:
在这里插入图片描述

3.5小节总结(summary)

3.5.1重难点分析

▪ 理解 DataSource 规范及规范的实现。
▪ 掌握单元测试类、测试方法编写规范。
▪ 理解 JDBC 编程过程以及存在的一些问题.

3.5.2FAQ 分析

Java 中与数据库建立连接需要什么?数据库驱动
这个数据库驱动的设计需要遵守什么规范吗?JDBC
当我们通过 JDBC API 获取到一个连接以后,应用结束我们会将连接返回到池中吗?(会)
连接池在应用时有什么弊端吗?(会带来一定的内存开销,以空间换时间)
假如现在让你去设计一个连接池,你会考虑哪些问题?(存储,算法,线程,参数)

3.5.3BUG 分析

位置错误,当出现如图-情况时,先检测单元测试类是否写到了src/test/java 目录。如图所示:
在这里插入图片描述
类引入错误,DataSource 为javax.sql 包中的类型,如图所示:
在这里插入图片描述
连接错误:数据库连接不上,如图所示:
在这里插入图片描述

4. MyBatis 持久化设计实现

4.1概述

      Mybatis 是一个优秀的持久层框架,底层基于 JDBC 实现与数据库的交互。并在 JDBC 操作的基础上做了封装和优化,它借助灵活的 SQL 定制,参数及结果集的映射方式,更好的适应了当前互联网技术的发展。Mybatis 框架的简单应用架构,如图-15 所示:

在这里插入图片描述

      在当今的互联网应用中项目,mybatis 框架通常会由 spring 框架进行资源整合,作为数据层技术实现数据交互操作。

4.2添加依赖

第一步:添加 mybatis 启动依赖。

启动快捷键 Alt+insert
在这里插入图片描述

在这里插入图片描述

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>

      我们添加了 mybatis 依赖以后,spring 框架启动时会对 mybatis 进行自动配置。例如 SqlSessionFactory 工厂对象的创建。
第二步:Mybatis 简易配置实现。
      假如需要对 mybatis 框架进行简易配置,可以打开 application.properties 文件,在此文件中进行基本配置(可选,暂时可以不配置),例如:

mybatis.configuration.default-statement-timeout=30 
mybatis.configuration.map-underscore-to-camel-case=true 
mybatis.mapper-locations=classpath:/mapper/*/*.xml

配置 mybatis 中的 sql 日志的输出:(com.cy 为我们写的项目的根包)

logging.level.com.cy=DEBUG

4.3 环境测试代码实现

在 src/test/java 目录中添加测试类,对 mybatis 框架整合进行基本测试,代码如下:

package com.cy.pj.sys.dao;
import org.apache.ibatis.session.SqlSession;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.sql.Connection;

@SpringBootTest
public class MyBatisTests {
    /**
     * sqlsession是mybatis框架中实现与数据库进行绘画的入口对象
     * 加入我们可以通过此对象获取与数据库的来凝结,表示可以通过mybatis框架实现与数据库会话
     */
    @Autowired
    //这里的sqlSession指向的对象是谁?
    // (sqlSessionTemplate实现了线程接口,并且线程安全
    //不能随意修改接口,行业规范
    private SqlSession sqlSession;

    @Test
    public void testGetConnection(){
        //连接来自哪里?(来源与连接池(底层会自动将连接池注入给mybatis框架))
        Connection conn=sqlSession.getConnection();
        System.out.println("conn="+conn);
    }
}

      在 SpringBoot 脚手架工程中,Spring 框架会基于 MyBatis 框架底层配置,创建SqlSessionFactory 对象,然后再通过此工厂对象创建 SqlSession,最后基于 Springku 框架为测试类注入 SqlSession 对象,接下来,我们可以通过 SqlSession 对象实现与数据库的会话了。

4.4通告业务设计及实现

4.4.1业务描述

基于 SpringBoot 脚手架工程对 MyBatis 框架的整合,实现对通告数据的操作。

API 设计分析

在这里插入图片描述

4.4.2Pojo 类设计

创建 SysNotice 类,借助此类对象封装公告(通知)数据。

package com.cy.pj.sys.pojo;

/**
 * SysNotice对象用于存储通知数据(例如)
 * Java中的对象可以简单分为两大类型:
 * 1) 一类是用于执行逻辑(打天下-控制逻辑,业务逻辑,数据持久逻辑)的对象
 * 2)一 类是用于存储数据(守天下-pojo)的对象
 */
public class SysNotice {
     private static final long serialVersionUID = 1L;

     /** 公 告 ID */ 
     private Long id;

     /** 公 告 标 题 */ 
     private String title;

     /** 公告类型(1 通知 2 公告) */
     private String type;

     /** 公告内容 */
     private String content;

     /** 公告状态(0 正常 1 关闭) */ 
     private String status;

     /** 创建时间 */
     private Date createdTime;

     /** 修改时间*/
     private Date modifiedTime;

     /** 创建用户 */
     private String createdUser;

     /** 修改用户*/
     private String modifiedUser;
     
     /** 备注*/
     private String setRemark;
     
     //自己添加 set/get/toString 方法
}
4.4.3 Dao 接口及方法

第一步:定义通告业务数据层接口及业务方法。

package com.cy.pj.sys.dao; 
@Mapper
public interface NoticeDao {
     List<Notice> selectNotices(Notice notice);
     int deleteById(Long… id);

     @Select("select * from sys_notice where id=#{id}")//注解方式声明sql,复杂sql写入xml中
     Notice selectById(Integer id); //基于id去查询
     int insertNotice(Notice notice); //往表中写数据
     int updateNotice(Notice notice);//修改表数据
}

      其中:@Mapper 是由 MyBatis 框架中定义的一个描述数据层接口的的注解(所有的注解只起到一个描述性的作用),用于告诉 Spring 框架此接口的实现由 mybatis 创建,并将其实现类对象存储到 spring 容器。

      第二步:创建 NoticeDao 接口对应的 SQL 映射文件,名字为 SysNoticeMapper.xml,
存储在 resources/mapper/sys 目录下,同时定义 xml 文件头以及根元素,代码如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
   PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
   "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
   <mapper namespace="com.cy.pj.sys.dao.NoticeDao">
</mapper>

第三步:在映射文件中添加 insertNotice 方法对应的 Sql 映射,关键代码如下:

<insert id="insertNotice"  parameterType="com.cy.pj.sys.pojo.SysNotice"> 
     insert into sys_notice
     (title,type,content,status,remark, 
     createdTime,modifiedTime,createdUser,modifiedUser) 
     values 
     (#{title},#{type},#{content},#{status},#{remark},
     now(),now(),#{createdUser},#{modifiedUser})
</insert>

第四步:在映射文件中定义更新公告对应的 SQL 映射,关键代码如下:

 <update id="updateNotice"
       parameterType="com.cy.pj.sys.pojo.SysNotice">
        update sys_notice
        set title=#{title},
           content=#{content},
           type=#{type},
            status=#{status},
            remark=#{remark},
            modifiedTime=now(),
            modifiedUser=#{modifiedUser}
      where id=#{id}
 </update>

第五步:在映射文件中添加基于条件的 SQL 查询映射,关键代码如下:

<select id="selectNotices"
    parameterType="com.cy.pj.sys.pojo.SysNotice"
    resultType="com.cy.pj.sys.pojo.SysNotice">
    select *
    from sys_notice
 <where>
 <if test="type!=null and type!=''">
    type=#{type}
 </if>
 <if test="title!=null and title!=''">
    and title like concat("%",#{title},"%")
 </if>
 <if test="modifiedUser!=null and modifiedUser!=''">
    and modifiedUser like
   concat("%",#{modifiedUser},"%")
    </if>
 </where>
 order by createdTime desc
 </select>

第六步:在映射文件中定义删除操作对应的 SQL 映射,关键代码如下:

<delete id="deleteById"><!--(1,2,3,4,5)-->
 <!--foreach 用于迭代一个数组或集合-->
 delete from sys_notice
 <where>
 <if test="ids!=null and ids.length>0">
 id in
 <foreach collection="ids" open="(" close=")" separator=","item="id">
 #{id}
</foreach>
    </if>
    or 1=2
    </where>
 </delete>
4.4.5 单元测试实现及分析

第一步:在 src/java/test 目录下定义测试类,对 NoticeDao 对象进行应用测试。

package com.cy.pj.sys.dao;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class NoticeDaoTests {

 @Autowired
 private NoticeDao noticeDao;.
}

第二步:在单元测试类中添加 insert 操作的单元测试方法,代码如下:

@Test
 void testInsertNotice(){
    //创建 SysNotice 对象,通过此对象封装要写入到数据库的数据
    SysNotice notice=new SysNotice();
    notice.setTitle("CGB2011 结课时间");
    notice.setContent("2021/3/20 正式结课");
    notice.setStatus("0");
    notice.setType("1");
    notice.setCreatedUser("tony");
    notice.setModifiedUser("tony");
    //将 SysNotice 对象持久化到数据库
    sysNoticeDao.insertNotice(notice);
   //此方法的实现内部会通过 SQLSession 向表中写数据。
 }

第三步:定义基于 id 进行查询的单元测试方法,代码如下:

@Test
 void testSelectById(){
    SysNotice notice=sysNoticeDao.selectById(1L);
    System.out.println(notice);
 }

第四步:定义基于 id 执行修改的单元测试方法,代码如下:

@Test
 void testUpdateNotice(){
    //基于 id 查询通知对象
    SysNotice notice=sysNoticeDao.selectById(1L);
    notice.setType("2");
    notice.setContent("2021/07/09 春节假期");
    notice.setModifiedUser("json");
    //将更新以后的内容持久化到数据库
    sysNoticeDao.updateNotice(notice);
 }

第五步:定义单元测试方法,代码如下:

@Test
 void testDeleteById(){
    int rows=
       sysNoticeDao.deleteById(1,3,4);
       System.out.println("rows="+rows);
 }

第六步:定义基于条件查询通过信息的单元测试方法,代码如下:

 @Test
 void testSelectNotices(){
    SysNotice notice=new SysNotice();
    notice.setType("1");
    notice.setTitle("开学");
    notice.setModifiedUser("tony");
    List<SysNotice> list=
    sysNoticeDao.selectNotices(notice);
    for(SysNotice n:list){
       System.out.println(n);
    }
 }

4.5小节总结(Summary)

4.5.1重难点分析

▪ MyBatis 框架核心优势
▪ MyBatis 框架应用架构及核心 API
▪ MyBatis 框架在 SpringBoot 工程中的整合
▪ MyBatis 框架与数据库会话的入口及会话过程
▪ MyBatis 框架中的动态 SQL 应用

4.5.2FAQ 分析

▪ MyBatis 是什么?(持久层框架,半成品-只解决了数据的持久化问题)
▪ 为什么选择 MyBatis?(开源,简单,功能强大,灵活,稳定,…)
▪ MyBatis 应用场景?(互联网软件应用都倾向于使用 mybatis 做数据持久化)
▪ MyBatis 框架实现与数据库交互的入口对象?SqlSession
▪ SqlSession 对象是如何创建的?SqlSessionFactory
▪ 你了解哪些具体的 SqlSession 对象呢?(DefaultSqlSession,SqlSessionTemplate)
▪ @Mapper 注解的作用是什么?
▪ MyBatis 为我们的 Dao 接口创建的实现类及其方法内部做了什么?

4.5.3BUG 分析

▪ BindingException
▪ BadSqlGrammarException
▪ SqlSyntaxErrorException
▪ UnstatisfiedDependencyException
▪ BeanInstantiationException



5Service 对象设计及实现

5.1概述

业务描述

      分层架构设计中通常会借助业务逻辑对象处理请求的核心和拓展业务,例如,调用数据逻辑对象执行数据操作,通过日志 API 进行日志记录,事务 API 处理事务等等

5.2快速入门实践

5.2.1 业务描述

通过通告业务对象进行业务逻辑操作的具体设计和实现。

5.2.2 API

在这里插入图片描述


设计分析

5.2.3 业务接口设计

定义通知业务逻辑接口及相关方法,关键代码如下:

package com.cy.pj.sys.service;
import com.cy.pj.sys.pojo.SysNotice;
import java.util.List;
/**
* 此对象为通告业务逻辑对象负责对通告业务做具体落地实现
* 1)调用数据逻辑对象执行数据操作
* 2)日志记录
* 3)事务处理
* 4)缓存
* 5)权限
* 6).............
*/
public interface SysNoticeService {
 /**
 * 新增通告信息
 * @param notice
 * @return
 */
 int saveNotice(SysNotice notice);
 /**
 * 基于条件查询通告信息
 * @param notice
 * @return
 */
 List<SysNotice> findNotices(SysNotice notice);
 /**
 * 基于通告删除通告信息
 * @param ids
 * @return
 */
 int deleteById(Long… ids);
 /**
 * 基于 id 查询某条通告
 * @param id
 * @return
 */
 SysNotice findById(Long id);
 /**
 * 更新通告信息
 * @param notice
 * @return
 */
 int updateNotice(SysNotice notice);
}

5.2.4 日志规约描述

业务描述

      日志是进行数据分析,异常分析的一种有效手段,我们在业务执行过程中对业务相关操作,进行日志记录是必须的一道程序。在进行日志的记录是,日志 API 选型也会影响到后续日志操作的可维护性和可扩展性。本次业务日志我们选择SLF4J,这是一套 Java 中基于门面模式而设计的一套日志 API 规范,其关键应用如下:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

 /**创建日志门面 API 对象*/
 private static final Logger log=
    //通过工厂创建日志对象
    LoggerFactory.getLogger(SysNoticeServiceImpl.class);

其中,SLF4J 基于此标准的实现有 logback,log4j 等具体的实现,我们编程时,建议耦合与规范或标准进行日志记录。

5.2.5 业务实现类设计

定义通知业务逻辑接口的实现类,并重写业务方法,重写过程中对相关业务进行日志记
录,关键代码如下:

package com.cy.pj.sys.service.impl;
import com.cy.pj.sys.dao.SysNoticeDao;
import com.cy.pj.sys.pojo.SysNotice;
import com.cy.pj.sys.service.SysNoticeService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @Service 注解由 spring 提供,一般用于描述分层架构中的业务逻辑对象,这样
的类
* 会交给 spring 管理
*/
@Service
public class SysNoticeServiceImpl implements SysNoticeService {
 /**创建日志门面 API 对象*/
   private static final Logger log=
      //通过工厂创建日志对象
      LoggerFactory.getLogger(SysNoticeServiceImpl.class);
    @Autowired
    private SysNoticeDao sysNoticeDao;
      //重写方法的生成 (选中类,然后 alt+enter)
    @Override
    public int saveNotice(SysNotice notice) {
      int rows=sysNoticeDao.insertNotice(notice);
      return rows;
    }
    @Override
    public List<SysNotice> findNotices(SysNotice notice) {
      //System.out.println(
        "system.out.start:"+System.currentTimeMillis());
        log.debug("start: {}",System.currentTimeMillis());
        //这里的{}表示占位符号
        List<SysNotice> list=sysNoticeDao.selectNotices(notice);
        log.debug("end: {}",System.currentTimeMillis());
        return list;
    }
    @Override
    public int deleteById(Long... ids) {
        int rows= sysNoticeDao.deleteById(ids);
        return rows;
    }

    @Override
    public SysNotice findById(Long id) {
        SysNotice notice=sysNoticeDao.selectById(id);
        return notice;
    }
    @Override
    public int updateNotice(SysNotice notice) {
        int rows=sysNoticeDao.updateNotice(notice);
        return rows;
   }
}

5.2.6 单元测试分析

定义单元测试类,对业务方法进行单元测试

package com.cy.pj.sys.service;
import com.cy.pj.sys.pojo.SysNotice;
import org.junit.jupiter.api.Test;
@SpringBootTest
public class SysNoticeServiceTests {
 @Autowired
 private SysNoticeService sysNoticeService;
     @Test 
     void testFindNotices(){
         SysNotice notice=new SysNotice();
         notice.setType("1");
         notice.setModifiedUser("tony");
         sysNoticeService.findNotices(notice);
    }
}


5.3 小节总结(Summary)

5.3.1 重难点分析

▪ 分层架构中 service 是如何定义的?
▪ 公告业务接口及方法的设计
▪ 公告业务实现与公告数据层接口的关系

5.3.2 FAQ 分析

▪ @Service 注解是谁定义的,有什么作用?
▪ 为什么要记录日志,一般都会记录什么信息?
▪ @Service 描述的类中是如何应用日志 API 的?
▪ 你知道哪些日志级别,在哪里进行配置?
▪ 日志要写到文件的基本配置是什么?
▪ ….

5.3.3 Bug 分析

▪ NoSuchBeanDefinitionException
▪ NullPointerException
▪ ….


6 Spring Web 模块设计及实现

6.1概述

      MybatisSpring MVC 是 Spring 框架中基于 MVC 设计思想,实现的一个用于处理 Web 请求的模块。这个模块封装了对 Servlet 的技术的应用,简化了程序员对请求和响应过程中数据的处理,其简易架构分析如图所示:
在这里插入图片描述
其中,核心组件分析:

▪ DispatcherServlet :前端控制器, 处理请求的入口。
▪ HandlerMapping:映射器对象, 用于管理 url 与对应 controller 的映射关系。
▪ Interceptors:拦截器,实现请求响应的共性处理。
▪ Controller:后端控制器-handler, 负责处理请求的控制逻辑。
▪ ViewResolver:视图解析器,解析对应的视图关系(前缀+viewname+后缀)。


当我们在浏览器中输入一个 url 访问服务端资源时,在 spring web 模块应用中,其请求处理流程分析如下:

  1. 基于域名获取 ip 地址(例如:127.0.0.1)
  2. 基于 ip 地址找到网络中计算机(ip 地址是网络计算机的唯一标识)
  3. 基于端口找到 tomcat 服务(port 是计算机中应用的唯一标识)
  4. tomcat 服务会基于 thread 并借助 io 读取网络 http 协议中请求数据
  5. tomcat 内部会创建请求(request)和响应(response)对象,用于封装请求和响应信息
  6. tomcat 会调用 Filter 对象对 request 数据进行过滤
  7. Filter 对 request 过滤以后,会将请求交给 DispatcherServlet 进行处理?
  8. DispatcherServlet 读取请求中的 url,并基于 url 从 requestMapping 中找到对应的 handler 以及方法
  9. DispatcherServlet 基于反射技术调用 handler 方法
  10. DispatcherServlet 会将 handler 方法返回的 view 等相关信息进行封装,然后交给视图解析器进行解析
  11. ViewResolver 视图解析器对为 view name 添加前缀,后缀,并将 model 中数据填充到 view 中
  12. DispatcherServlet 将视图解析器解析的结果封装到 response 对象,并将其响应到客户端。
    备注:假如希望了解 Spring MVC 的详细处理流程可以基于断点调试法进行跟踪。
package com.cy.java.net;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.Socket;
//客户端 (类似浏览器)
public class MainClient {
    public static void main(String[] args) throws IOException {
        //浏览器,向指定服务建立连接
        Socket socket=new Socket("127.0.0.1",6666);
        //向服务端写一句话
        ObjectOutputStream os=
                new ObjectOutputStream(socket.getOutputStream());
        os.writeUTF("hello server");
        os.flush();
        socket.close();
    }
}
package com.cy.java.net;

import java.io.ObjectInputStream;
import java.net.ServerSocket;
import java.net.Socket;
//Server(服务器),类似tomcat
public class MainServer {
    public static void main(String[] args) throws Exception {
        //tomcat
        //创建ServerSocket对象,并在6666端口进行监听
        ServerSocket serverSocket=new ServerSocket(6666);
        System.out.println("server start on 666 port ");
        while(true) {//不断等待客户端的链接
            Socket socket = serverSocket.accept();
            //客户连接到服务端以后会继续向下执行
            System.out.println("client 来了");
            //通过流读取客户端向服务端发送的请求数据
            //String threadName=Thread.currentThread().getName();
            //System.out.println("threadName="+threadName);
            ObjectInputStream in=new ObjectInputStream(socket.getInputStream());
            String content=in.readUTF();
            System.out.println("client say:"+content);
        }
    }
}

6.2 初始配置

添加 Spring Web 依赖(提供了 spring mvc 依赖支持),代码如下:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

其中,这个依赖添加时,会关联下载一个 tomcat 依赖(这个 tomcat 为一个嵌入式tomcat 服务器)


6.3快速入门实践

6.3.1 业务描述

定义 Controller 对象处理客户端 Notice 数据请求。

6.3.2 API 设计分析

在这里插入图片描述

6.3.3 Controller 类及方

第一步:创建 SysNoticeController 类,并关联 service 对象,关键代码如下

package com.cy.pj.sys.web.controller;
@RestController
@RequestMapping("/notice/")
public class SysNoticeController {
   @Autowired
   private SysNoticeService sysNoticeService;
}

第二步:添加处理请求的方法,关键代码如下:

@RequestMapping("doUpdateNotice")
Public String doUpdateNotice(Notice notice){ 
      sysNoticeService.updateNotice(notice);
       return "update ok";
     }
      @RequestMapping("doFindById/{id}")
      public Notice doFindById(@PathVariable Integer id){
       return sysNoticeService.findById(id);
      }

      @RequestMapping ("doSaveNotice")
      Public String doSaveNotice (Notice notice){
       sysNoticeService.saveNotice(notice);
        return "save ok";
      }

      @RequestMapping("doDeleteById")
      public Notice doDeleteById(Integer… ids){
         return sysNoticeService.deleteById(ids);
      }

      @RequestMapping("doFindNotices")
      public List<Notice> doFindNotices(Notice){
         return sysNoticeService.findNotices(Notice);
      }
6.3.4 Postman 应用测试

基于 Postman 向服务端发送请求,进行访问测试。


6.4 响应标准设计

6.4.1 概述

业务描述

      在基于 C/S 架构的编程模型中,客户端往往需要对服务端返回的数据,基于状态的不同进行不同的处理。例如,正确的状态数据一种呈现方式,错误的状态数据是另外一种呈现方式。于是服务端响应数据的标准化设计油然而生。

6.4.2 响应 API 设计

业务描述

      在响应数据标准化设计时,首先要对响应数据进行分析,哪些数据要响应到客户端,对这些数据进行怎样的状态设计等。假如现在响应的业务数据包含三部分:状态,消息,具体数据。我们可以这样设计,例如:

package com.cy.pj.sys.web.pojo;

/**
* 基于此对象封装服务端响应到客户端的数据
*/
public class JsonResult {
 /**响应状态码(有的人用 code)*/
 private Integer state=1;//1 表示 ok,0 表示 error,.....
 /**状态码对应的信息*/
 private String message="ok";
 /**正确的响应数据*/
 private Object data;
    public JsonResult(){}
     public JsonResult(String message){
        this.message=message;
      }
      public JsonResult(Object data){
        this.data=data;
      }
      public JsonResult (Throwable e){
        this.state=0;
        this.message=e.getMessage();
      }
      public Integer getState() {
        return state;
      }
      public void setState(Integer state) {
        this.state = state;
      }
      public String getMessage() {
        return message;
      }
      public void setMessage(String message) {
        this.message = message;
      }
      public Object getData() {
        return data;
      }
     public void setData(Object data) {
        this.data = data;
        }
     }
6.4.3 控制逻辑方法返回值设计

修改控制层方法返回值,关键代码如下:

 @RequestMapping("doUpdateNotice")
 Public JsonResult doUpdateNotice(SysNotice notice){
 sysNoticeService.updateNotice(notice);
        return new JsonResult("update ok");
 }
 @RequestMapping("doFindById/{id}")
 public JsonResult doFindById(@PathVariable Integer id){
        return new JsonResult(sysNoticeService.findById(id));
 }
 @RequestMapping ("doSaveNotice")
 Public JsonResult doSaveNotice (SysNotice notice){
 sysNoticeService.saveNotice(notice);
        return new JsonResult("save ok");
 }
 @RequestMapping("doDeleteById")
 public JsonResult doDeleteById(Integer… ids){
        return new JsonResult(sysNoticeService.deleteById(ids));
 }
 @RequestMapping("doFindNotices")
 public JsonResult doFindNotices(SysNotice notice){
        return new JsonResult(sysNoticeService.findNotices(notice));
 }

6.5 统一异常处理

6.5.1 概述

业务描述

      在项目的开发中,不管是对底层的数据逻辑操作过程,还是业务逻辑的处理过程,还是控制逻辑的处理过程,都不可避免会遇到各种可预知的、不可预知的异常。处理好异常对系统有很好的保护作用,同时会大大提高用户的体验。

6.5.2 异常处理

业务描述

      Java 项目中处理异常方式无非两种,要么执行 trycatch 操作,要么执行 throw 操作(抛给其它对象处理),无论采用哪种方式,其目的是让我们的系统对异常要有反馈。但现在的问题是我们如何让这种反馈代码的编写即简单又直观、友好呢?我们在处理异常的过程中通常要遵循一定的设计规范,例如:
▪ 捕获异常时与抛出的异常必须完全匹配,或者捕获异常是抛出异常的父类类型。
▪ 避免直接抛出 RuntimeException,更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常(例如 ServiceException)。
▪ 捕获异常后必须进行处理(例如记录日志)。如果不想处理它,需要将异常抛给它的调用者。
▪ 最外层的逻辑必须处理异常,将其转化成用户可以理解的内容。
▪ 避免出现重复的代码(Don’t Repeat Yourself),即 DAY 原则。

6.5.3 Web 全局异常处理

业务描述

      当项目由多个控制层类中有多个共性异常的处理方法定义时,我们可以将这些方法提取到公共的父类对象中,但是这种方式是一种强耦合的实现,不利于代码的维护。我们还可以借助 spring 框架中 web 模块定义的全局异常处理规范进行实现,例如定义全局异常处理类,代码如下:

package com.cy.pj.sys.web.advice;

@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
 public JsonResult doHandleRuntimeException(RuntimeException e){
        e.printStackTrace();
        return new JsonResult(e);
    }
}

6.6 拦截器技术应用

6.6.1 概述

业务描述

      Spring Web 中的拦截器(Interceptor)基于回调机制,可以在目标方法执行之前,先进行业务检测,满足条件则放行,不满足条件则进行拦截,拦截器原理分析如下图所示:
在这里插入图片描述

6.6.2 拦截器定义

通过拦截器拦截对 Spring Web Handler 进行时间访问拦截,其关键代码定义如下,

package com.cy.pj.common.web;

/**
* Spring MVC 中拦截器
* @author Administrator
*/
public class TimeAccessInterceptor implements HandlerInterceptor {
/**
* preHandle 在控制层目标方法执行之前执行
*/
@Override
public boolean preHandle(
HttpServletRequest request,HttpServletResponse response, Object handler)throws Exception {
       System.out.println("preHandler()");
       //获取 java 中的日历对象
       Calendar c=Calendar.getInstance();
       c.set(Calendar.HOUR_OF_DAY, 6);
       c.set(Calendar.MINUTE, 0);
       c.set(Calendar.SECOND, 0);
       long start=c.getTimeInMillis();
       c.set(Calendar.HOUR_OF_DAY,24);
       long end=c.getTimeInMillis();
       long cTime=System.currentTimeMillis();
       if(cTime<start||cTime>end)
         throw new ServiceException("不在访问时间之内");
       return true;
       }
}
6.6.3 拦截器配置

定义配置类,实现对拦截器的注册,关键代码如下

package com.cy.pj.sys.web.config;

@Configuration
public class SpringWebConfig implements WebMvcConfigurer{//web.xml
       //配置 spring mvc 拦截器
      @Override
      public void addInterceptors(InterceptorRegistry registry) {
          registry.addInterceptor(new TimeAccessInterceptor())
          .addPathPatterns("/notice/*");
      }
}
6.6.4 访问测试

启动服务,对 notice 模块的 url 进行,访问测试,检测请求是否被拦截处理了。


6.7 小节总结(Summary)

6.7.1 重难点分析

▪ MVC 设计思想的理解
▪ Spring Web 模块中 MVC 设计的设计
▪ Spring MVC 中核心组件及其作用。
▪ 基于 Spring MVC 实现请求处理的过程。

6.7.2 FAQ 分析

▪ 什么是 MVC(分层设计思想,一种编程套路)
▪ MVC 具体指的是哪些单词的缩写(Model,View,Controller)
▪ 举个生活中 MVC 的例子(全聚德饭店:菜单-view,服务员-controller,厨师-model)
▪ Spring Web 模块是如何基于 MVC 设计思想做具体实现的?
▪ @Controller 注解与@RestController 注解有什么关系?
▪ @ResponseBody 注解的作用是什么?
▪ @RequestMapping 的作用是什么,注解内部都有什么属性?
▪ @RestController 描述的类中,其方法返回值是如何转换为 json 的。
▪ 你知道 rest 风格的 url 定义方式吗,这种方式有什么好处?

6.7.3 BUG 分析

▪ 404 (资源没有找到)
▪ 400 (请求参数与服务端参数不匹配)
▪ 405 (请求方式与服务端不匹配)
▪ 500 (服务端处理请求的过程中出现了问题)



7总结(Sumamry)

7.1重难点分析

▪ 通知通告业务分析
▪ 通知通告设计稿(原型)设计
▪ 连接池应用的目的及原理
▪ Mybatis 框架封装 jdbc 执行数据持久化的逻辑
▪ Service 层对象存在的意义以及可能要实现的逻辑
▪ Spring Web 模块对 MVC 设计思想的落地实现

7.2FAQ 分析

▪ 为什么要选择 hikaricp 作为项目的连接池
▪ 为什么选择 mybatis 作为项目的数据持久层框架
▪ Spring Web 模块中 MVC 的设计思想是什么?
▪ 做项目的过程中你都用过哪些注解?
▪ 说说 autowired 注解描述属性时,spring 为属性注入值的过程是怎样的。
▪ 为什么将对象交给 Spring 管理,它管理对象有什么优势吗?
▪ 你在做项目时遇到问题是如何解决的?

7.3Bug 分析

▪ 参考 bug 集

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐