传输层协议

  1. TCP:当一台计算机想要与另一台计算机通讯时,两台计算机之间的通信需要畅通且可靠(会进行三次握手,断开也会进行四次挥手),这样才能保证正确收发数据,因此TCP更适合一些可靠的数据传输场景
  2. UDP:它是一种无连接协议,数据想发就发,而且不会建立可靠传输,也就是说传输过程中有可能会导致部分数据丢失,但是它比TCP传输更加简单高效,适合视频直播之类的

Socket

  1. 实现两台计算机之间的通信,Socket也叫套接字,是操作系统底层提供的一项通信技术,它支持TCP和UDP
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    //服务端
    public static void main(String[] args) {
    try(ServerSocket server = new ServerSocket(1234)){ //将服务端创建在端口1234上
    System.out.println("正在等待客户端连接...");
    while (true){ //无限循环等待客户端连接
    Socket socket = server.accept();
    System.out.println("客户端已连接,IP地址为:"+socket.getInetAddress().getHostAddress());
    }
    }catch (IOException e){
    e.printStackTrace();
    }
    }

    //客户端
    public static void main(String[] args) {
    try (Socket socket = new Socket("localhost", 1234)){
    System.out.println("已连接到服务端!");
    }catch (IOException e){
    System.out.println("服务端连接失败!");
    e.printStackTrace();
    }
    }

Socket数据传输

  1. 通过I/O流进行网络数据传输
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    //服务端
    try(ServerSocket server = new ServerSocket(1234)){ //将服务端创建在端口8080上
    System.out.println("正在等待客户端连接...");
    Socket socket = server.accept();
    System.out.println("客户端已连接,IP地址为:"+socket.getInetAddress().getHostAddress());
    BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); //通过
    System.out.print("接收到客户端数据:");
    System.out.println(reader.readLine());

    OutputStreamWriter writer = new OutputStreamWriter(socket.getOutputStream());
    writer.write("已收到!");
    writer.flush();
    }catch (IOException e){
    e.printStackTrace();
    }

    //客户端
    try (Socket socket = new Socket("localhost", 1234);
    Scanner scanner = new Scanner(System.in)){
    System.out.println("已连接到服务端!");
    OutputStream stream = socket.getOutputStream();
    OutputStreamWriter writer = new OutputStreamWriter(stream); //通过转换流来帮助我们快速写入内容
    System.out.println("请输入要发送给服务端的内容:");
    String text = scanner.nextLine();
    writer.write(text+'\n'); //因为对方是readLine()这里加个换行符
    writer.flush();
    System.out.println("数据已发送:"+text);
    BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    System.out.println("收到服务器返回:"+reader.readLine());
    }catch (IOException e){
    System.out.println("服务端连接失败!");
    e.printStackTrace();
    }finally {
    System.out.println("客户端断开连接!");
    }
  2. 手动关闭单向的流
    1
    2
    socket.shutdownOutput();  //关闭输出方向的流
    socket.shutdownInput(); //关闭输入方向的流
  3. 设定IO超时,服务端等待太久自动关闭连接
    1
    socket.setSoTimeout(3000);
  4. 如果连接的双方发生意外而通知不到对方,导致一方还持有连接,这样就会占用资源,可以使用setKeepAlive();当客户端连接后,如果设置了KeepAlive为true,当对方没有发送任何数据过来,超过一个时间(看系统内核参数配置),服务端会发送一个ack探测包发到对方,探测双方的TCP/IP连接是否有效
    1
    socket.setKeepAlive(true);
  5. TCP在传输过程中,有一个缓冲区用于数据的发送和接收,默认8192;可以手动调整大小优化传输效率
    1
    2
    socket.setReceiveBufferSize(25565);   //TCP接收缓冲区
    socket.setSendBufferSize(25565); //TCP发送缓冲区

浏览器访问Socket服务器

  1. 浏览器访问服务端,输入http://localhost:1234http://127.0.0.1:1234连接我们本地开放的服务器
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    try(ServerSocket server = new ServerSocket(1234)){    //将服务端创建在端口1234上
    System.out.println("正在等待客户端连接...");
    Socket socket = server.accept();
    System.out.println("客户端已连接,IP地址为:"+socket.getInetAddress().getHostAddress());
    InputStream in = socket.getInputStream(); //通过
    System.out.println("接收到客户端数据:");
    while (true){
    int i = in.read();
    if(i == -1) break;
    System.out.print((char) i);
    }
    }catch (Exception e){
    e.printStackTrace();
    }

JDBC操作数据库

下载安装

  1. 网址:https://dev.mysql.com/downloads/connector/j/
  2. 说明1:操作系统选Platform Independent,下载ZIP包,并进行解压
  3. 说明2:接着把mysql-connector-j-8.0.33.jar文件丢到项目中,鼠标右键添加为库

JDBC连接数据库

  1. JDBC连接数据库格式,修改为自己的数据库,表名只要学过MySQL都知道,查看结果会打印出第一列信息
    • 连接URL:可以从idea数据库获取
    • 用户名和密码:安装MySQL时就会让你创建
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      //1. 通过DriverManager来获得数据库连接
      try (Connection connection = DriverManager.getConnection("连接URL","用户名","密码");
      //2. 创建一个用于执行SQL的Statement对象
      Statement statement = connection.createStatement()){ //注意前两步都放在try()中,因为在最后需要释放资源!
      //3. 执行SQL语句,并得到结果集
      ResultSet set = statement.executeQuery("select * from 表名");
      //4. 查看结果
      while (set.next()){
      System.out.println(set.getString(1));
      }
      }catch (SQLException e){
      e.printStackTrace();
      }
      //5. 释放资源,try-with-resource语法会自动帮助我们close
  2. DriverManager是管理数据库的驱动,通过调用getConnection()来进行数据库的链接
  3. Connection是数据库的连接对象,可以通过连接对象来创建一个Statement用于执行SQL语句
  4. Statement用来执行SQL语句
    • executeQuery():执行select语句
    • executeUpdate():执行一个DML或是DDL语句,返回一个int类型,表示执行后受影响的行数,判断DML语句是否执行成功
    • execute():执行任意的SQL语句,返回boolean表示执行结果是一个ResultSet还是一个int,通过使用getResultSet()或是getUpdateCount()来获取

执行DML操作

  1. 增删改操作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    try(Connection connection = DriverManager.getConnection(
    "jdbc:mysql://localhost:3306/study","root","d6NFV8SYsEWPgTvq");
    Statement statement = connection.createStatement()){

    //增
    statement.executeUpdate("insert into student values(202301, '阿良良木历', '男')");
    //删
    statement.executeUpdate("delete from student where sid=202306");
    //改
    statement.executeUpdate("update student set name='忍野咩咩', sex='女' where sid=202302");

    }catch (SQLException e) {
    e.printStackTrace();
    }

执行DQL操作

  1. 执行DQL操作会返回一个ResultSet对象,我们来看看如何从ResultSet中去获取数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ResultSet set = statement.executeQuery("select * from student");

    //首先要明确,select返回的数据类似于一个excel表格
    while (set.next()) {
    //每调用一次next()就会向下移动一行,首次调用会移动到第一行
    //移动行数后,就可以通过set中提供的方法,来获取每一列的数据
    System.out.println(set.getInt("sid")); //可以指定属性或第n行,行数从1开始
    System.out.println(set.getString("name"));
    System.out.println(set.getString(3));
    }

执行批处理操作

  1. 执行很多条语句时,不用一次一次地提交,而是一口气全部交给数据库处理,这样会节省很多的时间
    1
    2
    3
    4
    5
    6
    statement.addBatch("insert into teach values(1, 202301)");
    statement.addBatch("insert into teach values(1, 202302)");
    statement.addBatch("insert into teach values(1, 202303)");
    statement.addBatch("insert into teach values(2, 202304)");
    statement.addBatch("insert into teach values(2, 202305)");
    statement.executeBatch();

查询结果映射为对象

  1. 数据转换为一个类来进行操作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class Student {
    Integer sid;
    String name;
    String sex;

    public Student(Integer sid, String name, String sex) {
    this.sid = sid;
    this.name = name;
    this.sex = sex;
    }

    public void say() {
    System.out.println("我叫:" + name + ",学号为:" + sid + ",性别是:" + sex);
    }
    }

    ResultSet set = statement.executeQuery("select * from student");
    while (set.next()) {
    Student student = new Student(
    set.getInt(1),
    set.getString(2),
    set.getString(3));
    student.say();
    }

    /*
    * 我叫:阿良良木历,学号为:202301,性别是:男
    * 我叫:忍野咩咩,学号为:202302,性别是:女
    * 我叫:凯露,学号为:202303,性别是:女
    * 我叫:灵梦,学号为:202304,性别是:女
    * 我叫:帕帕子,学号为:202305,性别是:男
    * */
  2. 利用反射机制来将查询结果映射为对象,使用反射的好处是,无论什么类型都可以通过我们的方法来进行实体类型映射
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    private static <T> T convert(ResultSet set, Class<T> clazz){
    try {
    Constructor<T> constructor = clazz.getConstructor(clazz.getConstructors()[0].getParameterTypes()); //默认获取第一个构造方法
    Class<?>[] param = constructor.getParameterTypes(); //获取参数列表
    Object[] object = new Object[param.length]; //存放参数
    for (int i = 0; i < param.length; i++) { //是从1开始的
    object[i] = set.getObject(i+1);
    if(object[i].getClass() != param[i])
    throw new SQLException("错误的类型转换:"+object[i].getClass()+" -> "+param[i]);
    }
    return constructor.newInstance(object);
    } catch (ReflectiveOperationException | SQLException e) {
    e.printStackTrace();
    return null;
    }
    }
  3. 通过反射来将查询结果转换为一个对象
    1
    2
    3
    4
    while (set.next()) {
    Student student = convert(set, Student.class);
    if(student != null) student.say();
    }

SQL注入攻击

  1. SQL注入攻击指的是用户可以通过输入内容直接破坏SQL语句
  2. PreparedStatement可以防止用户输入一些SQL语句关键字,使用?作为占位符,它会预编译一个SQL语句,直接将我们的内容进行替换的方式来填写数据;简单来说,不管用户如何输入SQL语句,预编译阶段会进行转义,最终编译成一串字符
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    try (Connection connection = DriverManager.getConnection(
    "jdbc:mysql://localhost:3306/study","root","d6NFV8SYsEWPgTvq");
    PreparedStatement statement = connection.prepareStatement("select * from user where username= ? and password=?;");
    Scanner scanner = new Scanner(System.in)){

    statement.setString(1, scanner.nextLine());
    statement.setString(2, scanner.nextLine());

    ResultSet res = statement.executeQuery();
    while (res.next()){
    String username = res.getString(1);
    System.out.println(username+" 登陆成功!");
    }

    }catch (SQLException e) {
    e.printStackTrace();
    }

管理事务

  1. 事务可以应用于银行业务等,此类地方需要严格审核数据,一旦出错可以立即取消,避免出现严重的问题
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    try (Connection connection = DriverManager.getConnection(
    "jdbc:mysql://localhost:3306/study","root","d6NFV8SYsEWPgTvq");
    Statement statement = connection.createStatement()){

    connection.setAutoCommit(false); //关闭自动提交,现在将变为我们手动提交
    statement.executeUpdate("insert into user values ('a', 1234)");
    statement.executeUpdate("insert into user values ('b', 1234)");
    statement.executeUpdate("insert into user values ('c', 1234)");
    connection.commit(); //如果前面任何操作出现异常,将不会执行commit(),之前的操作也就不会生效

    }catch (SQLException e){
    e.printStackTrace();
    }
  2. 设置回滚点以及使用回滚点,相当于游戏🎮的存档读档
    1
    2
    3
    4
    5
    6
    7
    connection.setAutoCommit(false);
    statement.executeUpdate("insert into user values ('a', 1234)");
    Savepoint savepoint = connection.setSavepoint(); //设置回滚点
    statement.executeUpdate("insert into user values ('b', 1234)");
    statement.executeUpdate("insert into user values ('c', 1234)");
    connection.rollback(savepoint); //回到回滚点,撤销前面的操作
    connection.commit();

Lombok注解开发

下载安装

  1. 网址:https://projectlombok.org/download
  2. 说明1:点击download下载,接着把lombok.jar文件丢到项目中,鼠标右键添加为库
  3. 说明2:idea自带lombok插件,不需要我们自己去安装,最后重启一下idea
  4. 说明3:首次使用要idea开启注释处理器;打开设置-构建执行部署-编译器-注解处理器-启动注解处理

常用注解

  1. @Data代表以下注解
    1
    2
    3
    4
    5
    @Setter
    @Getter
    @ToString
    @EqualsAndHashCode
    @RequiredArgsConstructor
    • @Setter与@Getter:生成set/get方法,静态与final字段无法生成set方法
    • @Accessors:控制生成Getter和Setter的样式
    • @ToString:生成预设的toString方法
    • @EqualsAndHashCode:生成比较和哈希值方法
    • @RequiredArgsConstructor:生成参数只包含final或标记为@NonNull的成员
  2. chain开启链式调用,fluent开启名字优化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Getter
    @Setter
    @Accessors(chain = true)
    public class Student {
    private String name;
    private int age;
    }

    Student student = new Student();
    student
    .setName("幽蓝")
    .setAge(9);
    System.out.println(student.getName() + student.getAge());

    //======================================================
    @Accessors(fluent = true)
    student
    .name("幽蓝")
    .age(9);
    System.out.println(student.name() + student.age());
  3. includeFieldNames省略字段,exclude不需要,of只需要,Include方法可以排序、取别名
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    @AllArgsConstructor
    @ToString(includeFieldNames = false)
    public class Student {
    private String name;
    private int age;
    private String state;
    }

    Student student = new Student("幽蓝", 9, "睡觉");
    System.out.println(student); //默认Student(name=幽蓝, age=9, state=睡觉),关闭后Student(幽蓝, 9, 睡觉)

    //======================================================
    @ToString(exclude = {"name", "age"})
    Student student = new Student("幽蓝", 9, "睡觉");
    System.out.println(student); //Student(state=睡觉)

    //======================================================
    @ToString(of = {"name", "age"})
    Student student = new Student("幽蓝", 9, "睡觉");
    System.out.println(student); //Student(name=幽蓝, age=9)

    //======================================================
    @ToString
    public class Student {
    @ToString.Include(rank = 0, name = "你的名字")
    private String name;
    @ToString.Include(rank = 2)
    private int age;
    @ToString.Include(rank = 1)
    private String state;
    }
    Student student = new Student();
    System.out.println(student); //Student(age=0, state=null, 你的名字=null)

其他注解

  1. @AllArgsConstructor和@NoArgsConstructor:生成全参构造和无参构造
  2. @Value:它与@Data类似,但是并不会生成setter并且成员属性都是final类型
  3. @SneakyThrows:生成try-catch代码块
    1
    2
    3
    4
    5
    @SneakyThrows(IOException.class)
    public static void main(String[] args) {
    InputStream inputStream = new FileInputStream("lombok.jar");
    inputStream.close();
    }
  4. @Cleanup:作用与局部变量,在最后自动调用其close()方法(可以自由更换)
    1
    2
    3
    4
    5
    @SneakyThrows(IOException.class)
    public static void main(String[] args) {
    @Cleanup
    InputStream inputStream = new FileInputStream("lombok.jar");
    }
  5. @Builder:生成建造者模式
    • @Builder.Default:指定默认值
    • @Builder.ObtainVia:指定默认值的获取方式
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      @ToString
      @Builder
      public class Student {
      private String name;
      private int age;
      private String state;
      }

      Student student = Student
      .builder()
      .name("幽蓝")
      .age(9)
      .state("跑步🏃")
      .build();
      System.out.println(student.toString()); //Student(name=幽蓝, age=9, state=跑步🏃)

      //======================================================
      @Builder.Default
      private int age = 10;

      Student student = Student
      .builder()
      .name("幽蓝")
      .state("跑步🏃")
      .build();
      System.out.println(student.toString()); //Student(name=幽蓝, age=10, state=跑步🏃)

Mybatis操作数据库框架

XML语言

  1. 创建不解析区域,里面的内容不会解析成标签,功能挺好的省去写一堆转义符
    1
    2
    3
    <test>
    <name><![CDATA[我看你<><><>是一点都不懂哦>>>]]></name>
    </test>

下载安装

  1. 网址:https://github.com/mybatis/mybatis-3/releases
  2. 说明1:翻墙打开网站,点最新的下载,比如mybatis-3.5.13.zip
  3. 说明2:老规矩解压,再把jar文件丢到项目中,再添加为库

初次使用

  1. 根目录创建mybatis-config.xml配置文件,填写mysql驱动,以及数据库的相关信息
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "https://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
    <environments default="development">
    <environment id="development">
    <transactionManager type="JDBC"/>
    <dataSource type="POOLED">
    <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/study"/>
    <property name="username" value="root"/>
    <property name="password" value="d6NFV8SYsEWPgTvq"/>
    </dataSource>
    </environment>
    </environments>
    </configuration>
  2. 注解用来简化try-catch语句,运行不报错表示配置成功

    SqlSessionFactory创建多个新的会话;SqlSession对象负责接收会话,相当于不同的地方登陆一个账号去访问数据库,跟JDBC中的Statement对象一样,会话之间相互隔离,没有任何关联;SqlSession定义了大量的数据库操作方法,只需要通过一个对象就能完成数据库的交互,极大的简化流程

    1
    2
    3
    4
    5
    6
    @SneakyThrows(IOException.class)
    public static void main(String[] args) throws FileNotFoundException {
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(new FileInputStream("mybatis-config.xml"));
    @Cleanup
    SqlSession session = sqlSessionFactory.openSession(true);
    }

初次使用2

  1. 指定位置com.test.entity下创建Student类,使用lombok创建方法
    1
    2
    3
    4
    5
    6
    7
    @Data
    public class Student {
    int sid; //名称最好和数据库字段名称保持一致,不然可能会映射失败导致查询结果丢失
    String name;
    String sex;

    }
  2. 根目录创建TestMapper.xml作为映射器,namespace就是命名空间,每个Mapper都是唯一的,因此需要用一个命名空间来区分,它还可以用来绑定一个接口。我们在里面写入了一个select标签,表示添加一个select操作,同时id作为操作的名称,resultType指定为我们刚刚定义的实体类,表示将数据库结果映射为Student类,然后就在标签中写入我们的查询语句
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <?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="TestMapper">
    <select id="selectStudent" resultType="com.test.entity.Student">
    select * from student
    </select>
    </mapper>
  3. 配置文件中添加mapper映射器,告诉mybatis这里定义了一个mapper读取一下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "https://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
    <environments default="development">
    <environment id="development">
    <transactionManager type="JDBC"/>
    <dataSource type="POOLED">
    <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/study"/>
    <property name="username" value="root"/>
    <property name="password" value="d6NFV8SYsEWPgTvq"/>
    </dataSource>
    </environment>
    </environments>

    <mappers>
    <mapper url="file:TestMapper.xml"/>
    </mappers>

    </configuration>
  4. main方法使用定义好的Mapper
    1
    2
    3
    4
    5
    6
    7
    8
    @SneakyThrows(IOException.class)
    public static void main(String[] args) throws FileNotFoundException {
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(new FileInputStream("mybatis-config.xml"));
    @Cleanup
    SqlSession session = sqlSessionFactory.openSession(true);
    List<Student> list = session.selectList("selectStudent");
    list.forEach(System.out::println);
    }

配置Mybatis

  1. 可以给类型起一个别名,以简化Mapper的编写
    1
    2
    3
    4
    <!-- 需要在environments的上方 -->
    <typeAliases>
    <typeAlias type="com.test.entity.Student" alias="papazi"/>
    </typeAliases>
  2. 现在Mapper可以直接使用别名了
    1
    2
    3
    4
    5
    <mapper namespace="com.test.mapper.TestMapper">
    <select id="selectStudent" resultType="papazi">
    select * from student
    </select>
    </mapper>
  3. 也可以让Mybatis去扫描一个包,并将包下的所有类自动起别名(别名为首字母小写的类名)
    1
    2
    3
    <typeAliases>
    <package name="com.test.entity"/>
    </typeAliases>
  4. 不使用实体类,可以将属性映射为Map,就会以键值对的形式存放这些结果
    1
    2
    3
    <select id="selectStudent" resultType="Map">
    select * from student
    </select>
    1
    2
    3
    public interface TestMapper {
    List<Map> selectStudent();
    }
  5. 设定一个resultType属性,让Mybatis知道查询结果需要映射为哪个实体类,要求字段名称保持一致。如果我们不希望按照这样的规则来映射呢?可以自定义resultMap来设定映射规则,通过指定映射规则,现在名称和性别一栏就发生了交换,因为我们将映射字段进行了交换
    1
    2
    3
    4
    5
    <resultMap id="Test" type="Student">
    <result column="sid" property="sid"/>
    <result column="sex" property="name"/>
    <result column="name" property="sex"/>
    </resultMap>
  6. 如果一个类中存在多个构造方法,这时就需要使用constructor标签来指定构造方法;指定构造方法后,若此字段被填入了构造方法作为参数,将不会通过反射给字段单独赋值,而构造方法中没有传入的字段,依然会被反射赋值
    1
    2
    3
    4
    5
    6
    <resultMap id="test" type="Student">
    <constructor>
    <arg column="sid" javaType="Integer"/>
    <arg column="name" javaType="String"/>
    </constructor>
    </resultMap>
  7. 如果数据库存在一个带下划线的字段,通过设置让其映射为以驼峰命名的字段,比如my_test映射为myTest;如果不设置,默认为不开启,也就是默认需要名称保持一致
    1
    2
    3
    <settings>
    <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>

增删改查

  1. 通过maven生成的项目结构,配置文件的路径也需要跟着修改,比如resource属性指向resources文件夹,用来管理配置文件,IO读取的文件则是以src文件作为根路径
    mybatis项目结构
  2. mybatis-config配置文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "https://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
    <typeAliases>
    <package name="com.test.entity"/>
    </typeAliases>

    <environments default="development">
    <environment id="development">
    <transactionManager type="JDBC"/>
    <dataSource type="POOLED">
    <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/study"/>
    <property name="username" value="root"/>
    <property name="password" value="d6NFV8SYsEWPgTvq"/>
    </dataSource>
    </environment>
    </environments>

    <mappers>
    <mapper resource="mapper/TestMapper.xml"/>
    </mappers>

    </configuration>
  3. TestMapper.xml映射器

    通过使用#{xxx}或是${xxx}来填入我们给定的属性,实际上Mybatis本质也是通过PreparedStatement首先进行一次预编译,有效地防止SQL注入问题,但是如果使用${xxx}就不再是通过预编译,而是直接传值,因此我们一般都使用#{xxx}来进行操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <?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.test.TestMapper">
    <select id="selectStudent" resultType="student">
    select * from student
    </select>

    <select id="getStudentBySid" resultType="student">
    select * from student where sid = #{sid}
    </select>

    <insert id="addStudent">
    insert into student(sid, name, sex) values(#{sid}, #{name}, #{sex})
    </insert>

    <delete id="deleteStudent">
    delete from student where sid = #{sid}
    </delete>

    <update id="updateStudent">
    update student set name = #{newName} where sid = #{sid}
    </update>
    </mapper>
  4. TestMapper接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.test;

    import com.test.entity.Student;

    import java.util.List;

    public interface TestMapper {
    List<Student> selectStudent();
    Student getStudentBySid(int sid);
    void addStudent(Student student);
    void deleteStudent(int sid);
    void updateStudent(int sid, String newName);
    }
  5. Student类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.test.entity;

    import lombok.Data;
    import lombok.experimental.Accessors;

    @Data
    @Accessors(chain = true)
    public class Student {
    int sid;
    String name;
    String sex;

    }
  6. MyBatisUtil类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    package com.test;

    import org.apache.ibatis.session.SqlSession;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.apache.ibatis.session.SqlSessionFactoryBuilder;

    import java.io.FileInputStream;
    import java.io.FileNotFoundException;

    public class MyBatisUtil {
    //在类加载时就进行创建
    private static SqlSessionFactory sqlSessionFactory;
    static {
    try {
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(new FileInputStream("src/main/resources/mybatis-config.xml"));
    } catch (FileNotFoundException e) {
    e.printStackTrace();
    }
    }

    /**
    * 获取一个新的会话
    * @param autoCommit 是否开启自动提交(跟JDBC是一样的,如果不自动提交,则会变成事务操作)
    * @return SqlSession对象
    */
    public static SqlSession getSession(boolean autoCommit){
    return sqlSessionFactory.openSession(autoCommit);
    }
    }
  7. Main主程序
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.test;

    import com.test.entity.Student;
    import org.apache.ibatis.session.SqlSession;

    import java.util.List;

    public class Main {
    public static void main(String[] args) {
    SqlSession session = MyBatisUtil.getSession(true);
    List<Student> list = session.selectList("selectStudent");
    list.forEach(System.out::println); //遍历集合全部学生信息

    TestMapper mapper = session.getMapper(TestMapper.class);
    System.out.println(mapper.getStudentBySid(202303)); //获取指定sid信息
    mapper.addStudent(new Student().setName("阿虚").setSex("男").setSid(202306)); //添加学生信息
    mapper.deleteStudent(202306); //删除指定sid的全部数据
    }
    }

复杂查询

  1. 一对多查询;映射为Teacher对象时,同时将教授的所有学生映射为List列表,这时需要进行复杂查询,通过resultMap来自定义映射规则

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Data
    public class Teacher {
    int tid;
    String name;
    List<Student> studentList;
    }

    //main方法
    System.out.println(mapper.getTeacherByTid(1));
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <select id="getTeacherByTid" resultMap="asTeacher">
    select *, teacher.name as tname from student inner join teach on student.sid = teach.sid
    inner join teacher on teach.tid = teacher.tid where teach.tid = #{tid}
    </select>

    <resultMap id="asTeacher" type="Teacher">
    <id column="tid" property="tid"/>
    <result column="tname" property="name"/>
    <collection property="studentList" ofType="Student">
    <id column="sid" property="sid"/>
    <result column="name" property="name"/>
    <result column="sex" property="sex"/>
    </collection>
    </resultMap>

    查询结果是一个多表联查的结果,联查的数据就是我们需要的映射数据(比如这里是一个老师有N个学生,联查的结果也是这一个老师对应N个学生的N条记录)

    其中id标签用于在多条记录中辨别是否为同一个对象的数据,比如上面的查询语句得到的结果中,tid这一行始终为1,因此所有的记录都应该是tid=1的教师的数据,而不应该变为多个教师的数据,如果不加id进行约束,那么会被识别成多个教师的数据

    通过使用collection来表示将得到的所有结果合并为一个集合,比如上面的数据中每个学生都有单独的一条记录,因此tid相同的全部学生的记录就可以最后合并为一个List,得到最终的映射结果

  2. 多对一查询;查询到一个Student对象时都带上它的老师,同样的,我们也可以使用resultMap来实现(先修改一下老师的类定义,不然会很麻烦)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @Data
    @Accessors(chain = true)
    public class Student {
    private int sid;
    private String name;
    private String sex;
    private Teacher teacher;
    }

    @Data
    public class Teacher {
    int tid;
    String name;
    }

    //TestMapper接口
    public interface TestMapper {
    List<Student> selectStudent(int tid);
    省略......
    }

    //main方法
    mapper.selectStudent(1).forEach(System.out::println);
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <select id="selectStudent" resultMap="test2">
    select *, teacher.name as tname from student left join teach on student.sid = teach.sid
    left join teacher on teach.tid = teacher.tid
    </select>

    <resultMap id="test2" type="Student">
    <id column="sid" property="sid"/>
    <result column="name" property="name"/>
    <result column="sex" property="sex"/>
    <association property="teacher" javaType="Teacher">
    <id column="tid" property="tid"/>
    <result column="tname" property="name"/>
    </association>
    </resultMap>

    通过使用association进行关联,形成多对一的关系,实际上和一对多是同理的,都是对查询结果的一种处理方式罢了

事务操作

  1. 获取SqlSession关闭自动提交来开启事务模式,和JDBC其实都差不多
    1
    2
    3
    4
    5
    6
    7
    8
    public static void main(String[] args) {
    SqlSession session = MyBatisUtil.getSession(false); //参数false,自动提交已关闭
    TestMapper mapper = session.getMapper(TestMapper.class);

    mapper.addStudent(new Student().setSid(202310).setName("小光").setSex("男"));
    session.rollback(); //回滚
    session.commit(); //提交
    }

动态SQL

  1. 动态SQL是MyBatis的强大特性之一;如果你使用过JDBC或其它类似的框架,你应该能理解根据不同条件拼接SQL语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态SQL,可以彻底摆脱这种痛苦
  2. if语句:试着进行算术运算sid % 2 ==0,然而并不生效;换成比较运算符sid == 202302,这样才生效
    1
    2
    3
    4
    5
    6
    <select id="getStudentBySid" resultType="student">
    select * from student where sid = #{sid}
    <if test="sid % 2 == 0">
    and sex = '女'
    </if>
    </select>
  3. choose、when、otherwise语句,不想使用所有的条件,而是从多个条件中选择一个使用;otherwise表示其他条件都不满足,最终进入这个条件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <select id="getStudentBySid" resultType="student">
    select * from student where sid = #{sid}
    <choose>
    <when test="sid == 202301">
    and sex = '女'
    </when>
    <when test="sid == 202302">
    and sex = '男'
    </when>
    <otherwise>
    and sex = '女'
    </otherwise>
    </choose>
    </select>

缓存机制

  1. Mybatis内置了一个缓存机制,我们查询时,如果缓存中存在数据,那么我们就可以直接从缓存中获取,而不是再去向数据库进行请求
    mybatis缓存机制
  2. Mybatis存在一、二级缓存;二级缓存默认是关闭状态,开启二级缓存需要在映射器XML文件中添加<cache/>; FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的
    • FIFO:先进先出,按对象进入缓存的顺序来移除它们
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      <!--mybatis-config配置文件开启缓存-->
      <settings>
      <setting name="cacheEnabled" value="true"/>
      </settings>

      <!--TestMapper配置文件添加缓存参数-->
      <cache
      eviction="FIFO"
      flushInterval="60000"
      size="512"
      readOnly="true"/>
  3. 第一个会话在进行读操作,完成后会结束会话,而第二个操作重新创建了一个新的会话,再次执行了同样的查询,我们发现得到的依然是缓存的结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Student student;
    try (SqlSession sqlSession = MyBatisUtil.getSession(true)){
    TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
    student = testMapper.getStudentBySid(202301);
    }

    try (SqlSession sqlSession2 = MyBatisUtil.getSession(true)){
    TestMapper testMapper2 = sqlSession2.getMapper(TestMapper.class);
    Student student2 = testMapper2.getStudentBySid(202301);
    System.out.println(student2 == student);
    }
  4. 如果不希望某个方法开启缓存,可以添加useCache属性用来关闭缓存
    1
    2
    3
    <select id="getStudentBySid" resultType="Student" useCache="false">
    select * from student where sid = #{sid}
    </select>
  5. 如果你希望执行一次清一次缓存,可以添加flushCache属性
    1
    2
    3
    <select id="getStudentBySid" resultType="Student" flushCache="true">
    select * from student where sid = #{sid}
    </select>

    取顺序:二级缓存 => 一级缓存 => 数据库

  6. 多个CPU在操作自己的缓存时,可能会出现各自的缓存内容不同步的问题;可以使用Redis、Ehcache、Memcache等缓存框架,能够很好地解决缓存一致性问题

注解开发

  1. 优点是不用再去写配置文件了,统一使用注解形式来开发
  2. 项目结构继续大改,resources文件夹依旧存放配置文件,不同的是mapper文件夹移动到com.test下面
    注解开发项目结构
  3. mybatis配置文件改用package标签,扫描com.test.mapper路径下的全部注解映射器
    1
    2
    3
    4
    <mappers>
    <!--<mapper class="com.test.mapper.TestMapper"/>-->
    <package name="com.test.mapper"/>
    </mappers>
  4. TestMapper接口,先写方法后写注解sql语句,反过来写一直红标😮‍💨
    1
    2
    3
    4
    5
    6
    7
    public interface TestMapper {
    @Insert("insert into student(name, sex) values(#{name}, #{sex})")
    void addStudent(Student student);

    @Select("select * from student where sid = #{sid}")
    Student getStudentBySid(int sid);
    }
  5. main方法使用方式不变,可以给sid设置自动递增,sex默认男,这样就能偷懒了
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Data
    @Accessors(chain = true)
    public class Student {
    private int sid;
    private String name;
    private String sex = "男";
    }

    public static void main(String[] args) {
    @Cleanup
    SqlSession sqlSession = MyBatisUtil.getSession(true);
    TestMapper mapper = sqlSession.getMapper(TestMapper.class);
    mapper.addStudent(new Student().setName("阿虚"));
    mapper.addStudent(new Student().setName("长门有希").setSex("女"));
    }
  6. 使用@CacheNamespace注解定义在接口上,通过使用@Options来控制单个操作的缓存启用
    1
    2
    3
    4
    5
    6
    @CacheNamespace(readWrite = false)
    public interface MyMapper {

    @Select("select * from student")
    @Options(useCache = false)
    List<Student> getAllStudent();

动态代理机制

  1. 好比我开了个大棚,里面栽种的西瓜,那么西瓜成熟了是不是得去卖掉赚钱,而我们的西瓜非常多,一个人肯定卖不过来,肯定就要去多找几个开水果摊的帮我们卖,这就是一种代理。实际上是由水果摊老板在帮我们卖瓜,我们只告诉老板卖多少钱,而至于怎么卖的是由水果摊老板决定的
    mybatis代理
  2. 首先定义一个接口用于规范行为
    1
    2
    3
    4
    public interface Shopper {
    //卖瓜行为
    void saleWatermelon(String customer);
    }
  3. 实现一下卖瓜行为,也就是我们要告诉老板卖多少钱,这里就直接写成功出售
    1
    2
    3
    4
    5
    6
    7
    public class ShopperImpl implements Shopper{
    //卖瓜行为的实现
    @Override
    public void saleWatermelon(String customer) {
    System.out.println("成功出售西瓜给 ===> "+customer);
    }
    }
  4. 最后老板代理后肯定要用自己的方式去出售这些西瓜,成交之后再按照我们告诉老板的价格进行出售
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class ShopperProxy implements Shopper{

    private final Shopper impl;

    public ShopperProxy(Shopper impl){
    this.impl = impl;
    }

    //代理卖瓜行为
    @Override
    public void saleWatermelon(String customer) {
    //首先进行 代理商讨价还价行为
    System.out.println(customer + ":哥们,这瓜多少钱一斤啊?");
    System.out.println("老板:两块钱一斤。");
    System.out.println(customer + ":你这瓜皮子是金子做的,还是瓜粒子是金子做的?");
    System.out.println("老板:你瞅瞅现在哪有瓜啊,这都是大棚的瓜,你嫌贵我还嫌贵呢。");
    System.out.println(customer + ":给我挑一个。");

    impl.saleWatermelon(customer); //讨价还价成功,进行我们告诉代理商的卖瓜行为
    }
    }

    这样的操作称为静态代理,也就是说我们需要提前知道接口的定义并进行实现才可以完成代理,而Mybatis这样的是无法预知代理接口的,我们就需要用到动态代理;JDK提供的反射框架就为我们很好地解决了动态代理的问题

  5. 通过实现InvocationHandler来成为一个动态代理,我们发现它提供了一个invoke方法,用于调用被代理对象的方法并完成我们的代理工作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class ShopperProxy implements InvocationHandler {

    Object target;
    public ShopperProxy(Object target){
    this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String customer = (String) args[0];
    System.out.println(customer + ":哥们,这瓜多少钱一斤啊?");
    System.out.println("老板:两块钱一斤。");
    System.out.println(customer + ":你这瓜皮子是金子做的,还是瓜粒子是金子做的?");
    System.out.println("老板:你瞅瞅现在哪有瓜啊,这都是大棚的瓜,你嫌贵我还嫌贵呢。");
    System.out.println(customer + ":行,给我挑一个。");
    return method.invoke(target, args);
    }
    }
  6. 现在就可以通过 Proxy.newProxyInstance来生成一个动态代理类
    1
    2
    3
    4
    5
    6
    7
    public static void main(String[] args) {
    Shopper impl = new ShopperImpl();
    Shopper shopper = (Shopper) Proxy.newProxyInstance(impl.getClass().getClassLoader(),
    impl.getClass().getInterfaces(), new ShopperProxy(impl));
    shopper.saleWatermelon("小强");
    System.out.println(shopper.getClass());
    }

JUnit单元测试

  1. 名称单元测试框架,用来评估某个模块或是功能的耗时和性能,快速排查导致程序运行缓慢的问题

下载安装

  1. 网址:https://repo1.maven.org/maven2/junit/junit/4.13.2/
  2. 网址:https://repo1.maven.org/maven2/org/hamcrest/hamcrest-core/1.1/
  3. 说明1:翻墙打开网站,点击junit-4.13.2.jar,另一个网址点击hamcrest-core-1.1.jar
  4. 说明2:两个jar包丢到项目中,添加为库

尝试JUnit

  1. 断言工具类,能测试结果是否符合预期
    1
    2
    3
    4
    5
    6
    7
    public class TestMain {
    @Test
    public void method(){
    System.out.println("我是测试案例!");
    Assert.assertEquals(1, 2); //参数1是期盼值,参数2是实际测试结果值
    }
    }
  2. 测试一个案例,比如我们想查看冒泡排序的编写是否正确
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class MainTest {
    @Test
    public void method(){
    int[] arr = {0, 4, 5, 2, 6, 9, 3, 1, 7, 8};

    //错误的冒泡排序
    for (int i = 0; i < arr.length - 1; i++) {
    for (int j = 0; j < arr.length - 1 - i; j++) {
    if(arr[j] > arr[j + 1]){
    int tmp = arr[j];
    arr[j] = arr[j+1];
    // arr[j+1] = tmp; //解除注释,再跑一次看看
    }
    }
    }
    System.out.println(Arrays.toString(arr)); //[0, 1, 1, 1, 1, 1, 1, 1, 7, 8]
    Assert.assertArrayEquals(new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, arr);
    }
    }
  3. 测试从数据库中取数据是否为我们预期的数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class MainTest {
    @Test
    public void method(){
    try (SqlSession sqlSession = MyBatisUtil.getSession(true)){
    TestMapper mapper = sqlSession.getMapper(TestMapper.class);
    Student student = mapper.getStudentBySidAndSex(202301, "男");

    Assert.assertEquals(new Student().setName("阿良良木历").setSex("男").setSid(202301), student);
    }
    }
    }
  4. 如果测试之前需要做一些前置操作,可通过@Before注解在测试用例开始前执行前置操作
    1
    2
    3
    4
    5
    6
    @Before
    public void before(){
    System.out.println("测试前置正在初始化...");
    前置操作......
    System.out.println("测试初始化完成,正在开始测试案例...");
    }

JUL日志

  1. JDK提供了一个日志框架,位于java.util.logging包下,可以使用此框架来实现日志的规范化打印
    1
    2
    3
    4
    5
    6
    7
    8
    public class Main {
    public static void main(String[] args) {
    // 首先获取日志打印器
    Logger logger = Logger.getLogger(Main.class.getName());
    // 调用info来输出一个普通的信息,直接填写字符串即可
    logger.info("我是普通的日志");
    }
    }
  2. 日志打印结果
    1
    2
    十一月 15, 2021 12:55:37 下午 com.test.Main main
    信息: 我是普通的日志

日志级别

  1. 日志分为7个级别,详细信息可以在Level类中查看
    • SEVERE(最高值):一般用于代表严重错误
    • WARNING:一般用于表示某些警告,但是不足以判断为错误
    • INFO(默认级别):常规消息
    • CONFIG
    • FINE
    • FINER
    • FINEST(最低值)
  2. 通过log方法设定日志的输出级别
    1
    2
    3
    4
    5
    6
    7
    public static void main(String[] args) {
    Logger logger = Logger.getLogger(Main.class.getName());
    logger.log(Level.SEVERE, "严重的错误", new IOException("我就是错误"));
    logger.log(Level.WARNING, "警告的内容");
    logger.log(Level.INFO, "普通的信息");
    logger.log(Level.CONFIG, "级别低于普通信息");
    }
  3. 低于默认级别的日志信息,无法输出到控制台,我们可以通过设置来修改日志的打印级别
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public static void main(String[] args) {
    Logger logger = Logger.getLogger(Main.class.getName());

    //修改日志级别
    logger.setLevel(Level.CONFIG);
    //不使用父日志处理器
    logger.setUseParentHandlers(false);
    //使用自定义日志处理器
    ConsoleHandler handler = new ConsoleHandler();
    handler.setLevel(Level.CONFIG);
    logger.addHandler(handler);

    logger.log(Level.SEVERE, "严重的错误", new IOException("我就是错误"));
    logger.log(Level.WARNING, "警告的内容");
    logger.log(Level.INFO, "普通的信息");
    logger.log(Level.CONFIG, "级别低于普通信息");
    }
  4. 每个Logger都有一个父日志打印器,我们可以通过getParent()
    1
    2
    3
    4
    public static void main(String[] args) throws IOException {
    Logger logger = Logger.getLogger(Main.class.getName());
    System.out.println(logger.getParent().getClass());
    }

    结果是java.util.logging.LogManager$RootLogger这个类,默认使用的是ConsoleHandler,且日志级别为INFO;每一个日志打印器都会使用父类的处理器,因此我们需要在之前关闭父类,然后使用自己的处理器

  5. 使用文件处理器来处理日志信息;参数true,表示开启追加模式,向尾部添加内容
    1
    2
    3
    4
    //添加输出到本地文件
    FileHandler fileHandler = new FileHandler("test.log", true);
    fileHandler.setLevel(Level.WARNING);
    logger.addHandler(fileHandler);
  6. 配置打印样式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public static void main(String[] args) throws IOException {
    Logger logger = Logger.getLogger(Main.class.getName());
    logger.setUseParentHandlers(false);

    //为了让颜色变回普通的颜色,通过代码块在初始化时将输出流设定为System.out
    ConsoleHandler handler = new ConsoleHandler(){{
    setOutputStream(System.out);
    }};
    //创建匿名内部类实现自定义的格式
    handler.setFormatter(new Formatter() {
    @Override
    public String format(LogRecord record) {
    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    String time = format.format(new Date(record.getMillis())); //格式化日志时间
    String level = record.getLevel().getName(); // 获取日志级别名称
    // String level = record.getLevel().getLocalizedName(); // 获取本地化名称(语言跟随系统)
    String thread = String.format("%10s", Thread.currentThread().getName()); //线程名称(做了格式化处理,留出10格空间)
    long threadID = record.getThreadID(); //线程ID
    String className = String.format("%-20s", record.getSourceClassName()); //发送日志的类名
    String msg = record.getMessage(); //日志消息

    //\033[33m作为颜色代码,30~37都有对应的颜色,38是没有颜色,IDEA能显示,但是某些地方可能不支持
    return "\033[38m" + time + " \033[33m" + level + " \033[35m" + threadID
    + "\033[38m --- [" + thread + "] \033[36m" + className + "\033[38m : " + msg + "\n";
    }
    });
    logger.addHandler(handler);

    logger.info("我是测试消息1...");
    logger.log(Level.INFO, "我是测试消息2...");
    logger.log(Level.WARNING, "我是测试消息3...");
    }
  7. 设置过滤器,可以过滤掉不想要的日志信息
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public static void main(String[] args) throws IOException {
    Logger logger = Logger.getLogger(Main.class.getName());

    //自定义过滤规则
    logger.setFilter(record -> !record.getMessage().contains("普通"));

    logger.log(Level.SEVERE, "严重的错误", new IOException("我就是错误"));
    logger.log(Level.WARNING, "警告的内容");
    logger.log(Level.INFO, "普通的信息");
    }

Properties

  1. properties文件是Java的一种配置文件,以更简单的方式读取配置文件,格式为配置项=配置值
    1
    2
    name=Test
    desc=Description
  2. 通过Properties类读取为一个类似于Map一样的对象;Properties类继承自Hashtable,而Hashtable是实现Map的接口,Properties本质上就是一个Map一样的结构,它会把所有的配置项映射为一个Map,这样就可以快速地读取配置的值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public static void main(String[] args) throws IOException {
    Properties properties = new Properties();
    properties.load(new FileInputStream("src/main/resources/logging.properties"));

    System.out.println(properties); //{name=Test, desc=Description}
    System.out.println(properties.getProperty("name")); //Test
    System.out.println(properties.getProperty("yyds", "找不到key")); //找不到key
    properties.put("幽蓝", "9"); //{name=Test, 幽蓝=9, desc=Description}
    }

日志配置文件

  1. 配置默认值,也可以修改ConsoleHandler的默认配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # RootLogger 的默认处理器为
    handlers= java.util.logging.ConsoleHandler
    # RootLogger 的默认的日志级别
    .level= ALL

    # 指定默认日志级别
    java.util.logging.ConsoleHandler.level = ALL
    # 指定默认日志消息格式
    java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
    # 指定默认的字符集
    java.util.logging.ConsoleHandler.encoding = UTF-8
  2. 试试配置文件效果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public static void main(String[] args) throws IOException {
    //获取日志管理器
    LogManager manager = LogManager.getLogManager();
    //读取我们自己的配置文件
    manager.readConfiguration(new FileInputStream("src/main/resources/logging.properties"));
    //再获取日志打印器
    Logger logger = Logger.getLogger(Main.class.getName());
    logger.log(Level.CONFIG, "我是一条日志信息"); //通过自定义配置文件,我们发现默认级别不再是INFO了
    }

lombok开启日志

  1. 真方便呢,太适合我了
    1
    2
    3
    4
    5
    6
    7
    @Log
    public class Main {
    public static void main(String[] args) throws IOException {
    System.out.println(log.getName());
    log.info("我是日志信息");
    }
    }

mybatis日志系统

  1. Mybatis日志系统记录了所有的数据库操作,默认不开启
  2. 日志系统输出Mybatis的日志信息;配置为JDK_LOGGING表示使用JUL进行日志打印

    logImpl包括很多种配置项,包括 SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING

    1
    <setting name="logImpl" value="JDK_LOGGING" />
  3. 因为Mybatis的日志级别都比较低,因此需要设置下logging.properties的日志级别
    1
    2
    3
    handlers= java.util.logging.ConsoleHandler
    .level= ALL
    java.util.logging.ConsoleHandler.level = ALL
  4. 创建一个格式化类,用来修改日志的打印格式
    1
    2
    3
    4
    5
    6
    7
    8
    public class FormatterTest extends Formatter {
    @Override
    public String format(LogRecord record) {
    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    String time = format.format(new Date(record.getMillis())); //格式化日志时间
    return time + " : " + record.getMessage() + "\n";
    }
    }
  5. 再把格式化类,添加到JUL配置文件中,修改下默认的格式化实现
    1
    2
    3
    4
    handlers= java.util.logging.ConsoleHandler
    .level= ALL
    java.util.logging.ConsoleHandler.level = ALL
    java.util.logging.ConsoleHandler.formatter = com.test.FormatterTest
  6. 测试用例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @Log
    public class MainTest {
    private SqlSessionFactory sqlSessionFactory;
    @Before
    public void before(){
    try {
    sqlSessionFactory = new SqlSessionFactoryBuilder()
    .build(new FileInputStream("src/main/resources/mybatis-config.xml"));
    LogManager manager = LogManager.getLogManager();
    manager.readConfiguration(new FileInputStream("src/main/resources/logging.properties"));
    } catch (IOException e) {
    e.printStackTrace();
    }
    }

    @Test
    public void test(){
    try(SqlSession sqlSession = sqlSessionFactory.openSession(true)){
    TestMapper mapper = sqlSession.getMapper(TestMapper.class);
    log.info(mapper.getStudentBySidAndSex(202301, "男").toString());
    log.info(mapper.getStudentBySidAndSex(202301, "男").toString());
    }
    }
    }

Maven项目管理

maven项目结构

项目结构

  1. 部分文件说明
    • java:创建包,写代码的地方
    • resources:存放静态资源,比如图片、配置文件,项目打包时会将资源文件一起打包到Jar中
    • test:存放测试文件,不会跟着项目一起打包
    • .gitignore:过滤文件,指定哪些文件不上传到仓库
    • pom.xml:maven的配置文件
  2. 说说pom.xml配置文件;groupId、artifactId、version这三个元素合在一起,作为区别项目的坐标。通过Maven导入其他的依赖,只需要填写这三个基本元素就可以了,而无需再下载Jar文件,转为Maven自动下载依赖并导入
    • groupId:组号
    • artifactId:项目名
    • version:版本号,SNAPSHOT代表快照,表示处于开发中,正式版只带版本号
      1
      2
      3
      <groupId>org.example</groupId>
      <artifactId>java-test</artifactId>
      <version>1.0-SNAPSHOT</version>
  3. properties中一般都是一些变量和选项的配置,前两个是JDK版本,后一个是编码格式
    1
    2
    3
    4
    5
    <properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

依赖导入

  1. 网址:https://mvnrepository.com
  2. 说明1:搜索lombok,点最新版,会出现依赖坐标,复制到pom.xml配置文件,最后刷新下maven
  3. 说明2:网站打不开,谷歌搜某个项目的Maven依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    <dependencies>
    <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter -->
    <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.9.2</version>
    <scope>test</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.28</version>
    <scope>provided</scope>
    </dependency>
    <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.13</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j -->
    <dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.0.33</version>
    </dependency>
    </dependencies>
    依赖管理流程
  4. 项目依赖一般是存储在中央仓库中,也有可能存储在一些其他的远程仓库(私服);maven第一次导入依赖需要联网,然后从远程仓库进行下载,我们会发现本地存在一个.m2文件夹,这就是maven本地仓库文件夹,可以在项目结构中查看详细的路径
  5. 下次导入依赖时,如果maven发现本地仓库中已经存在某个依赖,就不会再去远程仓库下载
  6. 由于远程仓库在国外,下载依赖时会出现卡顿,建议配置下远程仓库地址,打开IDEA的安装目录,找到安装根目录/plugins/maven/lib/maven3/conf文件夹,找到settings.xml文件,添加以下内容:
    1
    2
    3
    4
    5
    6
    <mirror>
    <id>nexus-aliyun</id>
    <mirrorOf>*</mirrorOf>
    <name>Nexus aliyun</name>
    <url>http://maven.aliyun.com/nexus/content/groups/public</url>
    </mirror>
  7. 这样默认的远程仓库地址(国外),就配置为国内的阿里云仓库地址了(依赖的下载速度就会快起来了)
  8. 如果需要的依赖没有上传的远程仓库,只有一个Jar,添加system值,表示不从远程仓库获取,而是导入本地Jar包,在添加一个systemPath来指定jar文件的位置
    1
    2
    3
    4
    5
    6
    7
    <dependency>
    <groupId>javax.jntm</groupId>
    <artifactId>diyulan</artifactId>
    <version>2.0</version>
    <scope>system</scope>
    <systemPath>C://学习资料/4K高清无码/test.jar</systemPath>
    </dependency>

依赖作用域

  1. 依赖其他属性
    • type:依赖类型,对于项目坐标定义的packaging。大部分情况下,该元素不必声明,默认值为jar
    • scope:依赖范围
    • optional:标记依赖是否可选
    • exclusions:排除传递性依赖(一个项目可能依赖于其他项目,就像我们的项目,如果别人要用我们的项目作为依赖,那么就需要一起下载我们项目的依赖,如Lombok)
  2. scope属性依赖范围
    • compile :默认参数,编译、运行、测试均有效
    • provided :编译、测试有效,运行无效;比如Lombok只在编译阶段使用它,运行时不需要此依赖
    • runtime :运行、测试有效,编译无效;比如JDBC的实现用到JDK指定的接口,实际上在运行时是不用自带JDK的依赖,因此只保留我们自己写的内容即可。
    • test :只在测试时有效;比如在测试阶段使用JUnit

可选依赖

  1. optional标签表示此依赖是可选的,默认true不导入
    1
    <optional>true</optional>

排除依赖

  1. 字面意思,用来排除不需要的依赖,通常用来降低版本或使用其他版本
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.9.2</version>
    <scope>test</scope>

    <exclusions>
    <exclusion>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    </exclusion>
    <!--还能往下添加排除-->
    <exclusion>
    <groupId>...</groupId>
    <artifactId>...</artifactId>
    </exclusion>
    </exclusions>

    </dependency>

继承关系

  1. 新建一个模块,打开pom.xml文件;parent节点表示是个子项目,子项目直接继承父项目的groupId
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
    <groupId>org.example</groupId>
    <artifactId>java-test</artifactId>
    <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>maven-test</artifactId>

    <properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    </project>
  2. 谈谈继承关系,父项目的dependencies标签,默认全部继承,除了optional标签外;父项目的dependencyManagement标签,子项目需要指定哪些依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <!-- 以下的依赖全部被继承 -->
    <dependencies>
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.28</version>
    <scope>provided</scope>
    </dependency>
    ...
    </dependencies>

    <!-- 子项目需要指定哪些依赖 -->
    <dependencyManagement>
    <dependencies>
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.28</version>
    <scope>provided</scope>
    </dependency>
    ...
    </dependencies>
    </dependencyManagement>
  3. 父项目将所有的依赖进行集中管理,子项目需要什么再拿什么即可,同时子项目无需指定版本,所有的版本全部由父项目决定,子项目只需要使用即可
    1
    2
    3
    4
    5
    6
    7
    <dependencies>
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <scope>provided</scope>
    </dependency>
    </dependencies>

常用命令

  1. IDEA右上角Maven板块中,每个Maven项目都有一个生命周期,实际上这些是Maven的一些插件,每个插件都有各自的功能
    • clean:执行后会清理整个target文件夹,在之后编写SpringBoot项目时可以解决一些缓存没更新的问题
    • validate:可以验证项目的可用性
    • compile:可以将项目编译为.class文件
    • install:可以将当前项目安装到本地仓库,以供其他项目导入作为依赖使用
    • verify:可以按顺序执行每个默认生命周期阶段(validatecompilepackage等)
  2. test命令,可以一键测试所有位于test目录下的测试案例,注意以下要求
    • 测试类的名称必须是以Test结尾,比如MainTest
    • 测试方法上必须标注@Test注解
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      public class MainTest {
      //因为配置文件位于内部,我们需要使用Resources类的getResourceAsStream来获取内部的资源文件
      private static SqlSessionFactory factory;

      //在JUnit5中@Before被废弃,它被细分了:
      @BeforeAll // 一次性开启所有测试案例只会执行一次 (方法必须是static)
      // @BeforeEach 一次性开启所有测试案例每个案例开始之前都会执行一次
      @SneakyThrows
      public static void before(){
      factory = new SqlSessionFactoryBuilder()
      .build(Resources.getResourceAsStream("mybatis.xml"));
      }

      @DisplayName("Mybatis数据库测试") //自定义测试名称
      @RepeatedTest(3) //自动执行多次测试
      public void test(){
      try (SqlSession sqlSession = factory.openSession(true)){
      TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
      System.out.println(testMapper.getStudentBySid(1));
      }
      }
      }
  3. package命令,用来打包生成jar文件;如果需要打包成可执行文件,可以通过插件实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    <build>
    <plugins>
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.5.0</version>
    <configuration>
    <descriptorRefs>
    <descriptorRef>jar-with-dependencies</descriptorRef>
    </descriptorRefs>
    <archive>
    <manifest>
    <addClasspath>true</addClasspath>
    <mainClass>com.test.Main</mainClass>
    </manifest>
    </archive>
    </configuration>
    <executions>
    <execution>
    <id>make-assembly</id>
    <phase>package</phase>
    <goals>
    <goal>single</goal>
    </goals>
    </execution>
    </executions>
    </plugin>
    </plugins>
    </build>
  4. deploy命令,用于发布项目到本地仓库和远程仓库,一般情况下用不到
  5. site命令,用于生成当前项目的发布站点

Tomcat服务器

下载安装

  1. 网址:https://tomcat.apache.org/download-10.cgi
  2. 说明1:下载栏目点10.1,mac系统选Core版,下载tar.gz并解压,建议放在用户名下的资源库
  3. 说明2:tomcat的bin目录下输入指令
    1
    2
    3
    4
    5
    #执行权限修改,提示输入管理员密码
    sudo chmod +x *.sh

    #启动tomcat
    sudo sh startup.sh
  4. 说明3:打开浏览器,输入http://localhost:8080,如果能看到Apache Tomcat,表示成功运行tomcat
  5. tomcat相关操作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #启动tomcat
    sh startup.sh

    #停止tomcat
    sh shutdown.sh

    #重启
    sh shutdown.sh
    sh startup.sh
  6. 8080端被其他进程占用问题
    1
    2
    3
    4
    5
    #查询8080端口被哪个进程占用了
    sudo lsof -P -i tcp:8080 | grep -i "listen"

    #关闭进程,PID改为进程号
    sudo kill -9 PID

IDEA部署tomcat

  1. 新建Jakarta EE,模版选Web应用程序,应用程序服务器选择tomcat目录,版本Jakarta EE10,添加的依赖项Servlet
  2. 创建项目后,运行-编辑配置
    • 打开浏览器用谷歌,执行更新操作选重启服务器,切换出IDE选更新类和资源
    • Url可以修改:http://localhost:8080/yyds/
    • 部署-应用程序上下文:/yyds
    • 启动运行后,自动弹出hello world页面,表示部署成功
  3. maven按钮有个package命令,可以将项目打包成war包,接着放到tomcat目录下的webapps文件夹内,就可以直接运行我Java编写的Web应用程序了,访问路径为文件的名称

Servlet动态网页响应

  1. 通过实现Servlet来进行动态网页响应,使用Servlet,不再是直接由Tomcat服务器发送我们编写好的静态网页内容(HTML文件),而是由我们通过Java代码进行动态拼接的结果,它能够很好地实现动态网页的返回
  2. Servlet通常用于HTTP协议通信

创建Servlet

  1. 只需要实现Servlet类即可,并添加注解@WebServlet来进行注册
    1
    2
    3
    4
    @WebServlet("/test")
    public class TestServlet implements Servlet {
    ...实现接口方法
    }
  2. 输入http://localhost:8080/yyds/test, 能弹出空白页面表示成功
  3. 路径src/main/webapp,可以放个index.html进去,重启服务器时就会弹出这个页面

生命周期

  1. 启动一次服务器,然后访问我们定义的页面,然后再关闭服务器
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    @WebServlet("/test")
    public class TestServlet implements Servlet {

    public TestServlet(){
    System.out.println("我是构造方法!");
    }

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
    System.out.println("我是init");
    }

    @Override
    public ServletConfig getServletConfig() {
    System.out.println("我是getServletConfig");
    return null;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
    System.out.println("我是service");
    }

    @Override
    public String getServletInfo() {
    System.out.println("我是getServletInfo");
    return null;
    }

    @Override
    public void destroy() {
    System.out.println("我是destroy");
    }
    }

    我是构造方法!
    我是init
    我是service
    我是service(出现两次是因为浏览器请求了2次,是因为有一次是请求favicon.ico,浏览器通病)

    我是destroy

  2. Servlet的生命周期
    • 首先执行构造方法完成 Servlet 初始化
    • Servlet 初始化后调用init()方法
    • Servlet 调用service()方法来处理客户端的请求
    • Servlet 销毁前调用destroy()方法
    • 最后,Servlet是由JVM的垃圾回收器进行垃圾回收的
  3. service方法中有两个参数,ServletRequest和ServletResponse,用户发起的HTTP请求,就被Tomcat服务器封装为了一个ServletRequest对象,我们得到是Tomcat服务器帮助我们创建的一个实现类,HTTP请求报文中的所有内容,都可以从ServletRequest对象中获取,同理ServletResponse就是我们需要返回给浏览器的HTTP响应报文实体类封装
    service请求与发送
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
    //首先将其转换为HttpServletRequest(继承自ServletRequest,一般是此接口实现)
    HttpServletRequest request = (HttpServletRequest) servletRequest;

    System.out.println(request.getProtocol()); //获取协议版本
    System.out.println(request.getRemoteAddr()); //获取访问者的IP地址
    System.out.println(request.getMethod()); //获取请求方法
    //获取头部信息
    Enumeration<String> enumeration = request.getHeaderNames();
    while (enumeration.hasMoreElements()){
    String name = enumeration.nextElement();
    System.out.println(name + ": " + request.getHeader(name));
    }

    //转换为HttpServletResponse(同上)
    HttpServletResponse response = (HttpServletResponse) servletResponse;
    //设定内容类型以及编码格式(普通HTML文本使用text/html)
    response.setContentType("text/html;charset=UTF-8");
    //获取Writer直接写入内容
    response.getWriter().write("<h1>椰奶yyds!</h1>");
    //所有内容写入完成之后,再发送给浏览器
    }

HttpServlet

  1. Servlet有一个直接实现抽象类GenericServlet,这个类完善了配置文件读取和Servlet信息相关的的操作,但是依然没有去实现service方法,仅仅是用于完善一个Servlet的基本操作。HttpServlet遵循HTTP协议的一种Servlet,继承自GenericServlet,它根据HTTP协议的规则,完善了service方法
  2. 简单的说,只需要继承HttpServlet来编写我们的Servlet就可以了,它提前实现了一些操作,能帮我们省去很多的时间
    1
    2
    3
    4
    5
    6
    7
    8
    @WebServlet("/test")
    public class TestServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    resp.setContentType("text/html;charset=UTF-8");
    resp.getWriter().write("<h1>恭喜你解锁了全新玩法</h1>");
    }
    }

@WebServlet注解

  1. @WebServlet注解用来快速注册一个Servlet,name属性就是Servlet名称,而urlPatterns和value实际上是同样功能,就是代表当前Servlet的访问路径
  2. 所有匹配/test/随便什么的路径名称,都可以访问此Servlet
    1
    @WebServlet("/test/*")
  3. 获取任何以js结尾的文件
    1
    @WebServlet("*.js")
  4. 如果没有找到匹配当前访问路径的Servlet,那么就会使用此Servlet进行处理;另外,此路径和Tomcat默认为我们提供的Servlet冲突,直接替换掉默认的
    1
    @WebServlet("/")
  5. 配置多个访问路径
    1
    @WebServlet({"/test1", "/test2"})
  6. loadOnStartup属性,默认值为-1,表示不在启动时加载;修改为大于等于0的数来开启启动时加载,数字的大小决定了此Servlet的启动优先级
    1
    @WebServlet(value = {"/test1", "/test2"}, loadOnStartup = 1)

POST请求完成登陆

  1. 前端页面,点击登录按钮,会自动向后台发送一个POST请求,请求地址为当前地址+login
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>登录页面</title>
    <link rel="stylesheet" href="style.css">
    </head>
    <body>
    <h1>登录系统</h1>
    <hr>
    <form action="login" method="post">
    <div>
    <label>
    <input type="text" placeholder="用户名" id="username" name="username">
    </label>
    </div>
    <div>
    <label>
    <input type="text" placeholder="密码" id="password" name="password">
    </label>
    </div>
    <div>
    <input type="submit" value="登录" class="sub">
    </div>
    </form>
    </body>
    </html>
  2. maven配置文件添加mybatis、mysql、lombok包
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    <dependencies>
    <dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.0.33</version>
    </dependency>
    <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.13</version>
    </dependency>
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.28</version>
    </dependency>
    <dependency>
    <groupId>jakarta.servlet</groupId>
    <artifactId>jakarta.servlet-api</artifactId>
    <version>6.0.0</version>
    <scope>provided</scope>
    </dependency>
    <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>${junit.version}</version>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>${junit.version}</version>
    <scope>test</scope>
    </dependency>
    </dependencies>
  3. mybatis配置文件,格式跟以前一样
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
    <environments default="development">
    <environment id="development">
    <transactionManager type="JDBC"/>
    <dataSource type="POOLED">
    <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/study"/>
    <property name="username" value="root"/>
    <property name="password" value="d6NFV8SYsEWPgTvq"/>
    </dataSource>
    </environment>
    </environments>

    <mappers>
    <package name="com.example.mapper"/>
    </mappers>
    </configuration>
  4. 创建一个实体类
    1
    2
    3
    4
    5
    6
    @Data
    public class User {
    private int id;
    private String username;
    private String password;
    }
  5. mapper来进行用户信息查询
    1
    2
    3
    4
    public interface UserMapper {
    @Select("select * from users where username = #{username} and password = #{password}")
    User getUser(@Param("username") String username, @Param("password") String password);
    }
  6. 用户在前端发送账号密码请求,后端收到后,向数据库发送校验信息,数据库返回校验结果,后端再向前端发送校验成功或失败信息
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    @WebServlet(value = "/login", loadOnStartup = 1)
    public class LoginServlet extends HttpServlet {
    SqlSessionFactory factory;
    @SneakyThrows
    @Override
    public void init() throws ServletException {
    //初始化一次mybatis
    factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config.xml"));
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    //首先设置一下响应类型
    resp.setContentType("text/html;charset=UTF-8");
    //获取POST请求携带的表单数据
    Map<String, String[]> map = req.getParameterMap();
    //判断表单是否完整
    if(map.containsKey("username") && map.containsKey("password")) {
    String username = req.getParameter("username");
    String password = req.getParameter("password");
    try(SqlSession sqlSession = factory.openSession(true)) {
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    User user = userMapper.getUser(username, password);

    if (user != null) {
    resp.getWriter().write("用户:" + username + ",登录成功!");
    }else {
    resp.getWriter().write("密码不正确或此用户不存在");
    }
    }

    }else {
    resp.getWriter().write("错误,您的表单数据不完整!");
    }
    }
    }

上传和下载文件

  1. maven配置文件,添加一个commons-io包,作用是快速地编写IO代码
    1
    2
    3
    4
    5
    <dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.7</version>
    </dependency>
  2. 首先将image.png放入到resource文件夹中,前端页面添加下载与上传功能;enctype="multipart/form-data"表示此表单用于文件传输
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <a href="file" download="reimu.png">点我下载高清资源</a>
    <form method="post" action="file" enctype="multipart/form-data">
    <div>
    <input type="file" name="test-file">
    </div>
    <div>
    <button>上传文件</button>
    </div>
    </form>
  3. @MultipartConfig注解表示此Servlet用于处理文件上传请求;上传路径修改成自己本地的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @MultipartConfig
    @WebServlet("/file")
    public class FileServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    resp.setContentType("image/png");
    OutputStream outputStream = resp.getOutputStream();
    InputStream inputStream = Resources.getResourceAsStream("reimu.png");
    //直接使用copy方法完成转换
    IOUtils.copy(inputStream, outputStream);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    try(FileOutputStream stream = new FileOutputStream("/Users/ulan/Downloads/marisa.png")){
    Part part = req.getPart("test-file");
    IOUtils.copy(part.getInputStream(), stream);
    resp.setContentType("text/html;charset=UTF-8");
    resp.getWriter().write("文件上传成功!");
    }
    }
    }

XHR请求数据

  1. 实现点击按钮刷新当前时间的功能
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <div id="time"></div>
    <br>
    <button onclick="updateTime()">更新数据</button>
    <script>
    updateTime()
    function updateTime() {
    let xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
    document.getElementById("time").innerText = xhr.responseText
    }
    };
    xhr.open('GET', 'time', true);
    xhr.send();
    }
    </script>
  2. 用于处理时间更新请求
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @WebServlet("/time")
    public class TimeServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
    String date = dateFormat.format(new Date());
    resp.setContentType("text/html;charset=UTF-8");
    resp.getWriter().write(date);
    }
    }

重定向与请求转发

  1. sendRedirect调用后,状态码设置为302,并添加Location属性表示重定向到哪一个网址
    1
    resp.sendRedirect("time");
  2. 自定义状态码与重定向到小破站
    1
    2
    resp.setStatus(302);
    resp.setHeader("Location", "https://www.bilibili.com");
  3. 请求转发是指无法处理或转交给其他Servlet来处理;注意,路径规则需要填写Servlet上指明的路径,只能转发到内部的Servlet,不能转发给其他站点或是其他Web应用程序
    1
    req.getRequestDispatcher("/time").forward(req, resp); 
  4. 如果是在doPost方法内进行转发,接收的Servlet没有doPost方法,就会出现405页面,原因是请求参数也一起被传递,因此写一个doPost方法就能解决问题
    1
    2
    3
    4
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    this.doGet(req, resp);
    }
  5. 请求转发setAttribute方法可以携带数据,请求转发后,可以获取到数据
    1
    2
    req.setAttribute("test", "我是请求转发前的数据");
    req.getRequestDispatcher("/time").forward(req, resp);
    1
    System.out.println(req.getAttribute("test"));
  6. 重定向与请求转发的区别
    • 请求转发是一次请求,重定向是两次请求
    • 请求转发地址栏不会发生改变, 重定向地址栏会发生改变
    • 请求转发可以共享请求参数 ,重定向之后,就获取不了共享参数了
    • 请求转发只能转发给内部的Servlet

ServletContext对象

  1. ServletContext全局唯一属于整个Web应用程序,通过getServletContext()来获取对象
    1
    2
    3
    ServletContext context = getServletContext();
    context.setAttribute("test", "我是重定向之前的数据");
    resp.sendRedirect("time");
    1
    System.out.println(getServletContext().getAttribute("test"));
  2. 也可以请求转发
    1
    context.getRequestDispatcher("/time").forward(req, resp);

初始化参数

  1. 类似于初始化配置一些值,比如数据库连接相关信息,通过初始化参数来给予Servlet或是一些其他的配置项
    1
    2
    3
    @WebServlet(value = "/login", initParams = {
    @WebInitParam(name = "test", value = "我是一个默认的初始化参数")
    })
  2. 它以键值对形式保存,通过getInitParameter方法获取
    1
    System.out.println(getInitParameter("test"));
  3. web.xml内,还能定义全局初始化参数
    1
    2
    3
    4
    <context-param>
    <param-name>幽蓝</param-name>
    <param-value>我是全局初始化参数</param-value>
    </context-param>
  4. 使用ServletContext来读取全局初始化参数
    1
    2
    ServletContext context = getServletContext();
    System.out.println(context.getInitParameter("幽蓝"));

Cookie

  1. 添加cookie数据,并重定向到time页面
    1
    2
    3
    Cookie cookie = new Cookie("test", "yyds");
    resp.addCookie(cookie);
    resp.sendRedirect("time");
  2. 重定向后会携带cookie数据,通过getCookies获取数据并打印到控制台
    1
    2
    3
    for (Cookie cookie : req.getCookies()) {
    System.out.println(cookie.getName() + ": " + cookie.getValue());
    }
  3. cookie信息
    • name:Cookie的名称,Cookie一旦创建,名称便不可更改
    • value:Cookie的值,如果值为Unicode字符,需要为字符编码。如果为二进制数据,则需要使用BASE64编码
    • maxAge:Cookie失效的时间,单位秒。如果为正数,则该Cookie在maxAge秒后失效。如果为负数,该Cookie为临时Cookie,关闭浏览器即失效,浏览器也不会以任何形式保存该Cookie。如果为0,表示删除该Cookie。默认为-1
    • secure:该Cookie是否仅被使用安全协议传输。安全协议。安全协议有HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false
    • path:Cookie的使用路径。如果设置为”/sessionWeb/“,则只有contextPath为”/sessionWeb”的程序可以访问该Cookie。如果设置为”/“,则本域名下contextPath都可以访问该Cookie。注意最后一个字符必须为”/“
    • domain:可以访问该Cookie的域名。如果设置为”.google.com”,则所有以”google.com”结尾的域名都可以访问该Cookie。注意第一个字符必须为”.”
    • comment:该Cookie的用处说明,浏览器显示Cookie信息的时候显示该说明。
    • version:Cookie使用的版本号。0表示遵循Netscape的Cookie规范,1表示遵循W3C的RFC 2109规范

      最关键的其实是name、value、maxAge、domain属性

  4. 修改一下maxAge,再来看看失效时间
    1
    cookie.setMaxAge(20); 

Session

  1. Session会给浏览器设定一个叫做JSESSIONID的Cookie,值是一个随机的排列组合,而此Cookie就对应了你属于哪一个对话,只要我们的浏览器携带此Cookie访问服务器,服务器就会通过Cookie的值进行辨别,得到对应的Session对象,因此,这样就可以追踪到底是哪一个浏览器在访问服务器
  2. 用户登录成功后,再将用户对象添加到Session中,只要是此用户发起的请求,我们都可以从HttpSession中读取到存储在会话中的数据
    1
    2
    HttpSession session = req.getSession();
    session.setAttribute("user", user);
  3. 如果用户没有登录就去访问首页,那么我们将发送一个重定向请求,告诉用户需要进行登录才可以访问
    1
    2
    3
    4
    5
    6
    HttpSession session = req.getSession();
    User user = (User) session.getAttribute("user");
    if(user == null) {
    resp.sendRedirect("login");
    return;
    }
  4. 设置Session过期时间
    1
    2
    3
    <session-config>
    <session-timeout>1</session-timeout>
    </session-config>
  5. 立即失效
    1
    session.invalidate(); 

Filter

  1. 过滤器相当于在所有访问前加了一堵墙,来自浏览器的所有访问请求都会首先经过过滤器,只有过滤器允许通过的请求,才可以顺利地到达对应的Servlet,而过滤器不允许的通过的请求,我们可以自由地进行控制是否进行重定向或是请求转发。并且过滤器可以添加很多个,就相当于添加了很多堵墙
  2. 实现Filter接口,并添加@WebFilter注解
    1
    2
    3
    4
    5
    6
    7
    @WebFilter("/*")   //路径的匹配规则和Servlet一致,这里表示匹配所有请求
    public class TestFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) servletRequest;
    }
    }
  3. 允许经过此过滤器
    1
    filterChain.doFilter(servletRequest, servletResponse);
  4. 用户在未登录情况下,只允许静态资源和登陆页面请求通过,登陆之后畅行无阻
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @WebFilter("/*")
    public class MainFilter extends HttpFilter {
    @Override
    protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
    String url = req.getRequestURL().toString();
    //判断是否为静态资源
    if(!url.endsWith(".js") && !url.endsWith(".css") && !url.endsWith(".png")){
    HttpSession session = req.getSession();
    User user = (User) session.getAttribute("user");
    //判断是否未登陆
    if(user == null && !url.endsWith("login")){
    res.sendRedirect("login");
    return;
    }
    }
    //交给过滤链处理
    chain.doFilter(req, res);
    }
    }

Listener

  1. 在应用程序加载的时候,或是Session创建的时候,亦或是在Request对象创建的时候进行一些操作,可以使用监听器来实现
    1
    2
    3
    4
    5
    6
    7
    @WebListener
    public class TestListener implements HttpSessionListener {
    @Override
    public void sessionCreated(HttpSessionEvent se) {
    System.out.println("有一个Session被创建了");
    }
    }