目录

  • 知识储备
    • 一、阶段路线
      • 1.基础阶段
      • 2. JavaWeb
      • 3. 提升
      • 4. 关于个人博客项目
    • 二、具体知识点
      • 1.SQL
        • 1) 外键
        • 2)索引
        • 3)查询
        • 4) 修改数据
        • 5) 实用SQL语句
          • 1. 插入或替换
          • 2. 插入或更新
          • 3. 插入或忽略
          • 4. 快照
          • 5. 写入查询结果集
          • 6. 强制使用指定索引
        • 6)事务
      • 2. Java
        • 1)反射
          • 1. 反射机制
          • 2.反射的举例
        • 2)泛型
          • 1. 什么是泛型?
          • 2. 泛型的使用
        • 3)集合
        • 4)多线程
          • 1.进程
          • 2.进程 vs 线程
          • 3. 多线程
          • 4. 创建新线程
          • 5. 线程的状态
          • 6)线程同步
        • 5)IO
        • 6)Maven 基础
          • 1. Maven 介绍
          • 2. Maven项目结构
          • 3. 依赖管理
        • 7)JDBC
          • 1.JDBC
          • 2.JDBC连接
          • 3. JDBC连接
          • 4. JDBC查询
          • 5. SQL注入
          • 6. 插入
          • 7. 更新
          • 8. 删除
          • 9. 连接池
        • 8)Spring 开发
          • 1. 什么是Spring?
          • 2. Spring Framework
          • 3. IOC容器
            • 1)IOC容器原理
            • 2)依赖注入方式
            • 3)无侵入容器
            • 4)理解
            • 5)装配 Bean
            • 6)ApplicationContext
            • 7)使用Annotation配置
          • 4. 使用 AOP
        • 9)SpringBoot
          • 1. 什么是SpringBoot
          • 2. SpringBoot运行
        • 10)redis
        • 11)rabbitMQ
          • 1. 搜索与商品服务的问题
          • 2. 什么是消息队列

知识储备

一、阶段路线

1.基础阶段

  1. javase 贪吃蛇小游戏

  2. mysql 宿舍管理系统

  3. html 个人站点

  4. js Bon Cake

  5. jq 一起来捉妖

  6. vue 淘宝

2. JavaWeb

  1. javaee第一阶段 MyProject

  2. javaee第二阶段 SSMTest

  3. javaee第三阶段 SpringBoot

3. 提升

  1. SpringData

  2. SpringCloud 乐优商城

  3. 开发项目(SpringBoot)个人博客项目

4. 关于个人博客项目

1. 情景:当时面对什么困难?

​ 当时因为疫情,只能在家复习,当时结束学习乐优商城项目一段时间,心态比较放松,看视频没有状态,恰好世伟也面临这种境况,所以他提出来一起从头到尾开发一个项目,一方面能让自己了解项目流程,一方面能够将自己所学的知识整合进去,同时有了目标之后还能提升状态,和老师谈了之后,鼓励我们开发项目。所以就开始了这个项目。

2. 目标:你的目标是什么?

通过这个项目了解开发项目的流程,需求分析、ER图、原型图、前台、后台、后端、运维。给自己以后做项目积累经验,能够快速的上手。同时有一个自己全程参与的项目。以后所学的新知识都能都整合进去,不断的优化。

3. 行动:你做了什么 ?

这个项目两个人全程参与,前期准备工作共同讨论完成,编写代码,分模块编写,难题共同解决。

项目中遇到的问题

1) 原型图设计

2)评论回复表设计,采用数据结构

4.怎么解决的

项目中的亮点

1)评论回复采用链表结构

2)多表查询

3)点赞

现在存在的问题

1)访客没有登录功能,导致每添加一条评论、回复、留言。都需要新增。如果有访客多次操作,会导致数据冗余。

  1. 结果:效果如何?

运用了很多之前了解到的知识,第一版只是基础功能的实现,后边如果有时间,会计划将更优秀的技术运用进去,使其更加精致。

二、具体知识点

1.SQL

1) 外键

关系数据库通过外键可以实现一对多、多对多和一对一的关系。外键既可以通过数据库来约束,也可以不设置约束,仅依靠应用程序的逻辑来保证。

一些应用会把一个大表拆成两个一对一的表,目的是把经常读取和不经常读取的字段分开,以获得更高的性能。例如,把一个大的用户表分拆为用户基本信息表user_info和用户详细信息表user_profiles,大部分时候,只需要查询user_info表,并不需要查询user_profiles表,这样就提高了查询速度。

2)索引

索引的效率取决于索引列的值是否散列,即该列的值如果越互不相同,那么索引效率越高。反过来,如果记录的列存在大量相同的值,例如gender列,大约一半的记录值是M,另一半是F,因此,对该列创建索引就没有意义。

可以对一张表创建多个索引。索引的优点是提高了查询效率,缺点是在插入、更新和删除记录时,需要同时修改索引,因此,索引越多,插入、更新和删除记录的速度就越慢。

对于主键,关系数据库会自动对其创建主键索引。使用主键索引的效率是最高的,因为主键会保证绝对唯一。

3)查询

  1. 聚合查询

除了COUNT()函数外,SQL还提供了如下聚合函数:

函数 说明
SUM 计算某一列的合计值,该列必须为数值类型
AVG 计算某一列的平均值,该列必须为数值类型
MAX 计算某一列的最大值
MIN 计算某一列的最小值

注意,MAX()MIN()函数并不限于数值类型。如果是字符类型,MAX()MIN()会返回排序最后和排序最前的字符。

  1. 连接查询

对于这么多种JOIN查询,到底什么使用应该用哪种呢?其实我们用图来表示结果集就一目了然了。

假设查询语句是:

SELECT ... FROM tableA ??? JOIN tableB ON tableA.column1 = tableB.column2;

我们把tableA看作左表,把tableB看成右表,那么INNER JOIN是选出两张表都存在的记录:

LEFT OUTER JOIN是选出左表存在的记录:

RIGHT OUTER JOIN是选出右表存在的记录:

FULL OUTER JOIN则是选出左右表都存在的记录:

  1. 小结

JOIN查询需要先确定主表,然后把另一个表的数据“附加”到结果集上;

INNER JOIN是最常用的一种JOIN查询,它的语法是SELECT ... FROM <表1> INNER JOIN <表2> ON <条件...>

JOIN查询仍然可以使用WHERE条件和ORDER BY排序。

4) 修改数据

INSERT语句的基本语法是:

INSERT INTO <表名> (字段1, 字段2, ...) VALUES (值1, 值2, ...);

UPDATE语句的基本语法是:

UPDATE <表名> SET 字段1=值1, 字段2=值2, ... WHERE ...;

DELETE语句的基本语法是:

DELETE FROM <表名> WHERE ...;

5) 实用SQL语句

在编写SQL时,灵活运用一些技巧,可以大大简化程序逻辑。

1. 插入或替换

如果我们希望插入一条新记录(INSERT),但如果记录已经存在,就先删除原记录,再插入新记录。此时,可以使用REPLACE语句,这样就不必先查询,再决定是否先删除再插入:

REPLACE INTO students (id, class_id, name, gender, score) VALUES (1, 1, '小明', 'F', 99);

id=1的记录不存在,REPLACE语句将插入新记录,否则,当前id=1的记录将被删除,然后再插入新记录。

2. 插入或更新

如果我们希望插入一条新记录(INSERT),但如果记录已经存在,就更新该记录,此时,可以使用INSERT INTO ... ON DUPLICATE KEY UPDATE ...语句:

INSERT INTO students (id, class_id, name, gender, score) VALUES (1, 1, '小明', 'F', 99) ON DUPLICATE KEY UPDATE name='小明', gender='F', score=99;

id=1的记录不存在,INSERT语句将插入新记录,否则,当前id=1的记录将被更新,更新的字段由UPDATE指定。

3. 插入或忽略

如果我们希望插入一条新记录(INSERT),但如果记录已经存在,就啥事也不干直接忽略,此时,可以使用INSERT IGNORE INTO ...语句:

INSERT IGNORE INTO students (id, class_id, name, gender, score) VALUES (1, 1, '小明', 'F', 99);

id=1的记录不存在,INSERT语句将插入新记录,否则,不执行任何操作。

4. 快照

如果想要对一个表进行快照,即复制一份当前表的数据到一个新表,可以结合CREATE TABLESELECT

-- 对class_id=1的记录进行快照,并存储为新表students_of_class1:
CREATE TABLE students_of_class1 SELECT * FROM students WHERE class_id=1;

新创建的表结构和SELECT使用的表结构完全一致。

5. 写入查询结果集

如果查询结果集需要写入到表中,可以结合INSERTSELECT,将SELECT语句的结果集直接插入到指定表中。

例如,创建一个统计成绩的表statistics,记录各班的平均成绩:

CREATE TABLE statistics (id BIGINT NOT NULL AUTO_INCREMENT,class_id BIGINT NOT NULL,average DOUBLE NOT NULL,PRIMARY KEY (id)
);

然后,我们就可以用一条语句写入各班的平均成绩:

INSERT INTO statistics (class_id, average) SELECT class_id, AVG(score) FROM students GROUP BY class_id;

确保INSERT语句的列和SELECT语句的列能一一对应,就可以在statistics表中直接保存查询的结果:

> SELECT * FROM statistics;
+----+----------+--------------+
| id | class_id | average      |
+----+----------+--------------+
|  1 |        1 |         86.5 |
|  2 |        2 | 73.666666666 |
|  3 |        3 | 88.333333333 |
+----+----------+--------------+
3 rows in set (0.00 sec)
6. 强制使用指定索引

在查询的时候,数据库系统会自动分析查询语句,并选择一个最合适的索引。但是很多时候,数据库系统的查询优化器并不一定总是能使用最优索引。如果我们知道如何选择索引,可以使用FORCE INDEX强制查询使用指定的索引。例如:

> SELECT * FROM students FORCE INDEX (idx_class_id) WHERE class_id = 1 ORDER BY id DESC;

指定索引的前提是索引idx_class_id必须存在。

6)事务

把多条语句作为一个整体进行操作的功能,被称为数据库事务。数据库事务可以确保该事务范围内的所有操作都可以全部成功或者全部失败。如果事务失败,那么效果就和没有执行这些SQL一样,不会对数据库数据有任何改动。

可见,数据库事务具有ACID这4个特性:

  • A:Atomic,原子性,将所有SQL作为原子工作单元执行,要么全部执行,要么全部不执行;
  • C:Consistent,一致性,事务完成后,所有数据的状态都是一致的,即A账户只要减去了100,B账户则必定加上了100;
  • I:Isolation,隔离性,如果有多个事务并发执行,每个事务作出的修改必须与其他事务隔离;
  • D:Duration,持久性,即事务完成后,对数据库数据的修改被持久化存储。

2. Java

1)反射

JVM为每个加载的classinterface创建了对应的Class实例来保存classinterface的所有信息;

获取一个class对应的Class实例后,就可以获取该class的所有信息;

通过Class实例获取class信息的方法称为反射(Reflection);

JVM总是动态加载class,可以在运行期根据条件来控制加载class。

1. 反射机制

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

2.反射的举例

①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;

②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:

  1. 将程序内所有 XML 或 Properties 配置文件加载入内存中;

2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息;

3)使用反射机制,根据这个字符串获得某个类的Class实例;

4)动态配置实例的属性

2)泛型

1. 什么是泛型?

泛型就是编写模板代码来适应任意类型;

泛型的好处是使用时不必对类型进行强制转换,它通过编译器对类型进行检查;

注意泛型的继承关系:可以把ArrayList<Integer>向上转型为List<Integer>T不能变!),但不能把ArrayList<Integer>向上转型为ArrayList<Number>T不能变成父类)。

2. 泛型的使用

使用泛型时,把泛型参数<T>替换为需要的class类型,例如:ArrayList<String>ArrayList<Number>等;

可以省略编译器能自动推断出的类型,例如:List<String> list = new ArrayList<>();

不指定泛型参数类型时,编译器会给出警告,且只能将<T>视为Object类型;

可以在接口中定义泛型类型,实现此接口的类必须实现正确的泛型类型。

3)集合

ArrayList把添加和删除的操作封装起来,让我们操作List类似于操作数组,却不用关心内部元素如何移动。

我们考察List<E>接口,可以看到几个主要的接口方法:

  • 在末尾添加一个元素:void add(E e)
  • 在指定索引添加一个元素:void add(int index, E e)
  • 删除指定索引的元素:int remove(int index)
  • 删除某个元素:int remove(Object e)
  • 获取指定索引的元素:E get(int index)
  • 获取链表大小(包含元素的个数):int size()

4)多线程

现代操作系统(Windows,macOS,Linux)都可以执行多任务。多任务就是同时运行多个任务

1.进程

在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。

某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。

进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。

2.进程 vs 线程

进程和线程是包含关系,但是多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。

具体采用哪种方式,要考虑到进程和线程的特点。

和多线程相比,多进程的缺点在于:

  • 创建进程比创建线程开销大,尤其是在Windows系统上;
  • 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。

而多进程的优点在于:

多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。

3. 多线程

Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。

因此,对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。

和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。

Java多线程编程的特点又在于:

  • 多线程模型是Java程序最基本的并发模型;
  • 后续读写网络、数据库、Web开发等都依赖Java多线程模型。
4. 创建新线程

Java语言内置了多线程支持。当Java程序启动的时候,实际上是启动了一个JVM进程,然后,JVM启动主线程来执行main()方法。在main()方法中,我们又可以启动其他线程。

要创建一个新线程非常容易,我们需要实例化一个Thread实例,然后调用它的start()方法:

// 多线程
public class Main {public static void main(String[] args) {Thread t = new Thread();t.start(); // 启动新线程}
}

Run

但是这个线程启动后实际上什么也不做就立刻结束了。我们希望新线程能执行指定的代码,有以下几种方法:

方法一:从Thread派生一个自定义类,然后覆写run()方法:

// 多线程
public class Main {public static void main(String[] args) {Thread t = new MyThread();t.start(); // 启动新线程}
}class MyThread extends Thread {@Overridepublic void run() {System.out.println("start new thread!");}
}

Run

超出执行限额:请等待20秒后再试。

执行上述代码,注意到start()方法会在内部自动调用实例的run()方法。

方法二:创建Thread实例时,传入一个Runnable实例:

public class Main {public static void main(String[] args) {Thread t = new Thread(new MyRunnable());t.start(); // 启动新线程}
}class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("start new thread!");}
}

或者用Java8引入的lambda语法进一步简写为:

// 多线程
public class Main {public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("start new thread!");});t.start(); // 启动新线程}
}
5. 线程的状态

在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:

  • New:新创建的线程,尚未执行;
  • Runnable:运行中的线程,正在执行run()方法的Java代码;
  • Blocked:运行中的线程,因为某些操作被阻塞而挂起;
  • Waiting:运行中的线程,因为某些操作在等待中;
  • Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
  • Terminated:线程已终止,因为run()方法执行完毕。

用一个状态转移图表示如下:

         ┌─────────────┐│     New     │└─────────────┘│▼
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐┌─────────────┐ ┌─────────────┐
││  Runnable   │ │   Blocked   ││└─────────────┘ └─────────────┘
│┌─────────────┐ ┌─────────────┐││   Waiting   │ │Timed Waiting│
│└─────────────┘ └─────────────┘│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│▼┌─────────────┐│ Terminated  │└─────────────┘

当线程启动后,它可以在RunnableBlockedWaitingTimed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。

线程终止的原因有:

  • 线程正常终止:run()方法执行到return语句返回;
  • 线程意外终止:run()方法因为未捕获的异常导致线程终止;
  • 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。

一个线程还可以等待另一个线程直到其运行结束。例如,main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行:

6)线程同步

当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。

这个时候,有个单线程模型下不存在的问题就来了:如果多个线程同时读写共享变量,会出现数据不一致的问题。

我们假设n的值是100,如果两个线程同时执行n = n + 1,得到的结果很可能不是102,而是101,原因在于:

┌───────┐    ┌───────┐
│Thread1│    │Thread2│
└───┬───┘    └───┬───┘│            ││ILOAD (100) ││            │ILOAD (100)│            │IADD│            │ISTORE (101)│IADD        ││ISTORE (101)│▼            ▼

如果线程1在执行ILOAD后被操作系统中断,此刻如果线程2被调度执行,它执行ILOAD后获取的值仍然是100,最终结果被两个线程的ISTORE写入后变成了101,而不是期待的102

这说明多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待:

┌───────┐     ┌───────┐
│Thread1│     │Thread2│
└───┬───┘     └───┬───┘│             ││-- lock --   ││ILOAD (100)  ││IADD         ││ISTORE (101) ││-- unlock -- ││             │-- lock --│             │ILOAD (101)│             │IADD│             │ISTORE (102)│             │-- unlock --▼             ▼

通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。

可见,保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized关键字对一个对象进行加锁:

synchronized(lock) {n = n + 1;
}

5)IO

IO是指Input/Output,即输入和输出。以内存为中心:

  • Input指从外部读入数据到内存,例如,把文件从磁盘读取到内存,从网络读取数据到内存等等。
  • Output指把数据从内存输出到外部,例如,把数据从内存写入到文件,把数据从内存输出到网络等等。

为什么要把数据读到内存才能处理这些数据?因为代码是在内存中运行的,数据也必须读到内存,最终的表示方式无非是byte数组,字符串等,都必须存放在内存里。

从Java代码来看,输入实际上就是从外部,例如,硬盘上的某个文件,把内容读到内存,并且以Java提供的某种数据类型表示,例如,byte[]String,这样,后续代码才能处理这些数据。

因为内存有“易失性”的特点,所以必须把处理后的数据以某种方式输出,例如,写入到文件。Output实际上就是把Java表示的数据格式,例如,byte[]String等输出到某个地方。

IO流是一种顺序读写数据的模式,它的特点是单向流动。数据类似自来水一样在水管中流动,所以我们把它称为IO流。

6)Maven 基础

1. Maven 介绍

Maven是一个Java项目管理和构建工具,它可以定义项目结构、项目依赖,并使用统一的方式进行自动化构建,是Java项目不可缺少的工具。

在了解Maven之前,我们先来看看一个Java项目需要的东西。首先,我们需要确定引入哪些依赖包。例如,如果我们需要用到commons logging,我们就必须把commons logging的jar包放入classpath。如果我们还需要log4j,就需要把log4j相关的jar包都放到classpath中。这些就是依赖包的管理。

其次,我们要确定项目的目录结构。例如,src目录存放Java源码,resources目录存放配置文件,bin目录存放编译生成的.class文件。

此外,我们还需要配置环境,例如JDK的版本,编译打包的流程,当前代码的版本号。

最后,除了使用Eclipse这样的IDE进行编译外,我们还必须能通过命令行工具进行编译,才能够让项目在一个独立的服务器上编译、测试、部署。

这些工作难度不大,但是非常琐碎且耗时。如果每一个项目都自己搞一套配置,肯定会一团糟。我们需要的是一个标准化的Java项目管理和构建工具。

Maven就是是专门为Java项目打造的管理和构建工具,它的主要功能有:

  • 提供了一套标准化的项目结构
  • 提供了一套标准化的构建流程(编译,测试,打包,发布……);
  • 提供了一套依赖管理机制。
2. Maven项目结构

一个使用Maven管理的普通的Java项目,它的目录结构默认如下:

a-maven-project
├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   └── resources
│   └── test
│       ├── java
│       └── resources
└── target

项目的根目录a-maven-project是项目名,它有一个项目描述文件pom.xml,存放Java源码的目录是src/main/java,存放资源文件的目录是src/main/resources,存放测试源码的目录是src/test/java,存放测试资源的目录是src/test/resources,最后,所有编译、打包生成的文件都放在target目录里。这些就是一个Maven项目的标准目录结构。

所有的目录结构都是约定好的标准结构,我们千万不要随意修改目录结构。使用标准结构不需要做任何配置,Maven就可以正常使用。

我们再来看最关键的一个项目描述文件pom.xml,它的内容长得像下面:

<project ...><modelVersion>4.0.0</modelVersion><groupId>com.itranswarp.learnjava</groupId><artifactId>hello</artifactId><version>1.0</version><packaging>jar</packaging><properties>...</properties><dependencies><dependency><groupId>commons-logging</groupId><artifactId>commons-logging</artifactId><version>1.2</version></dependency></dependencies>
</project>

其中,groupId类似于Java的包名,通常是公司或组织名称,artifactId类似于Java的类名,通常是项目名称,再加上version,一个**Maven工程就是由groupIdartifactIdversion作为唯一标识。**我们在引用其他第三方库的时候,也是通过这3个变量确定。例如,依赖commons-logging

<dependency><groupId>commons-logging</groupId><artifactId>commons-logging</artifactId><version>1.2</version>
</dependency>

使用<dependency>声明一个依赖后,Maven就会自动下载这个依赖包并把它放到classpath中。

3. 依赖管理

依赖关系

Maven定义了几种依赖关系,分别是**compiletestruntimeprovided**:

scope 说明 示例
compile 编译时需要用到该jar包(默认) commons-logging
test 编译Test时需要用到该jar包 junit
runtime 编译时不需要,但运行时需要用到 mysql
provided 编译时需要用到,但运行时由JDK或某个服务器提供 servlet-api

唯一ID

对于某个依赖,Maven只需要3个变量即可唯一确定某个jar包:

  • groupId:属于组织的名称,类似Java的包名;
  • artifactId:该jar包自身的名称,类似Java的类名;
  • version:该jar包的版本。

通过上述3个变量,即可唯一确定某个jar包。Maven通过对jar包进行PGP签名确保任何一个jar包一经发布就无法修改。修改已发布jar包的唯一方法是发布一个新版本。

因此,某个jar包一旦被Maven下载过,即可永久地安全缓存在本地。

注:只有以-SNAPSHOT结尾的版本号会被Maven视为开发版本,开发版本每次都会重复下载,这种SNAPSHOT版本只能用于内部私有的Maven repo,公开发布的版本不允许出现SNAPSHOT。

构架流程

在实际开发过程中,经常使用的命令有:

mvn clean:清理所有生成的class和jar;

mvn clean compile:先清理,再执行到compile

mvn clean test:先清理,再执行到test,因为执行test前必须执行compile,所以这里不必指定compile

mvn clean package:先清理,再执行到package

大多数phase在执行过程中,因为我们通常没有在pom.xml中配置相关的设置,所以这些phase什么事情都不做。

经常用到的phase其实只有几个:

  • clean:清理
  • compile:编译
  • test:运行测试
  • package:打包

7)JDBC

1.JDBC

什么是JDBC?JDBC是Java DataBase Connectivity的缩写,它是Java程序访问数据库的标准接口。

使用Java程序访问数据库时,Java代码并不是直接通过TCP连接去访问数据库,而是通过JDBC接口来访问,而JDBC接口则通过JDBC驱动来实现真正对数据库的访问。

例如,我们在Java代码中如果要访问MySQL,那么必须编写代码操作JDBC接口。注意到JDBC接口是Java标准库自带的,所以可以直接编译。而具体的JDBC驱动是由数据库厂商提供的,例如,MySQL的JDBC驱动由Oracle提供。因此,访问某个具体的数据库,我们只需要引入该厂商提供的JDBC驱动,就可以通过JDBC接口来访问,这样保证了Java程序编写的是一套数据库访问代码,却可以访问各种不同的数据库,因为他们都提供了标准的JDBC驱动:

使用JDBC的好处是:

  • 各数据库厂商使用相同的接口,Java代码不需要针对不同数据库分别开发;
  • Java程序编译期仅依赖java.sql包,不依赖具体数据库的jar包;
  • 可随时替换底层数据库,访问数据库的Java代码基本不变。
2.JDBC连接

使用JDBC时,我们先了解什么是Connection。Connection代表一个JDBC连接,它相当于Java程序到数据库的连接(通常是TCP连接)。打开一个Connection时,需要准备URL、用户名和口令,才能成功连接到数据库。

3. JDBC连接

使用JDBC时,我们先了解什么是Connection。Connection代表一个JDBC连接,它相当于Java程序到数据库的连接(通常是TCP连接)。打开一个Connection时,需要准备URL、用户名和口令,才能成功连接到数据库。

URL是由数据库厂商指定的格式,例如,MySQL的URL是:

jdbc:mysql://<hostname>:<port>/<db>?key1=value1&key2=value2

假设数据库运行在本机localhost,端口使用标准的3306,数据库名称是learnjdbc,那么URL如下:

jdbc:mysql://localhost:3306/learnjdbc?useSSL=false&characterEncoding=utf8

后面的两个参数表示不使用SSL加密,使用UTF-8作为字符编码(注意MySQL的UTF-8是utf8

要获取数据库连接,使用如下代码:

// JDBC连接的URL, 不同数据库有不同的格式:
String JDBC_URL = "jdbc:mysql://localhost:3306/test";
String JDBC_USER = "root";
String JDBC_PASSWORD = "password";
// 获取连接:
Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
// TODO: 访问数据库...
// 关闭连接:
conn.close();

核心代码是DriverManager提供的静态方法getConnection()DriverManager会自动扫描classpath,找到所有的JDBC驱动,然后根据我们传入的URL自动挑选一个合适的驱动。

因为JDBC连接是一种昂贵的资源,所以使用后要及时释放。使用try (resource)来自动释放JDBC连接是一个好方法:

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {...
}
4. JDBC查询

获取到JDBC连接后,下一步我们就可以查询数据库了。查询数据库分以下几步:

第一步,通过Connection提供的createStatement()方法创建一个Statement对象,用于执行一个查询;

第二步,执行Statement对象提供的executeQuery("SELECT * FROM students")并传入SQL语句,执行查询并获得返回的结果集,使用ResultSet来引用这个结果集;

第三步,反复调用ResultSetnext()方法并读取每一行结果。

完整查询代码如下:

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {try (Statement stmt = conn.createStatement()) {try (ResultSet rs = stmt.executeQuery("SELECT id, grade, name, gender FROM students WHERE gender=1")) {while (rs.next()) {long id = rs.getLong(1); // 注意:索引从1开始long grade = rs.getLong(2);String name = rs.getString(3);int gender = rs.getInt(4);}}}
}

注意要点:

StatmentResultSet都是需要关闭的资源,因此嵌套使用try (resource)确保及时关闭;

rs.next()用于判断是否有下一行记录,如果有,将自动把当前行移动到下一行(一开始获得ResultSet时当前行不是第一行);

ResultSet获取列时,索引从1开始而不是0

必须根据SELECT的列的对应位置来调用getLong(1)getString(2)这些方法,否则对应位置的数据类型不对,将报错。

5. SQL注入

使用Statement拼字符串非常容易引发SQL注入的问题,这是因为SQL参数往往是从方法参数传入的。

我们来看一个例子:假设用户登录的验证方法如下:

User login(String name, String pass) {...stmt.executeQuery("SELECT * FROM user WHERE login='" + name + "' AND pass='" + pass + "'");...
}

其中,参数namepass通常都是Web页面输入后由程序接收到的。

如果用户的输入是程序期待的值,就可以拼出正确的SQL。例如:name = "bob",pass = "1234"

SELECT * FROM user WHERE login='bob' AND pass='1234'

但是,如果用户的输入是一个精心构造的字符串,就可以拼出意想不到的SQL,这个SQL也是正确的,但它查询的条件不是程序设计的意图。例如:name = "bob' OR pass=", pass = " OR pass='"

SELECT * FROM user WHERE login='bob' OR pass=' AND pass=' OR pass=''

这个SQL语句执行的时候,根本不用判断口令是否正确,这样一来,登录就形同虚设。

要避免SQL注入攻击,一个办法是针对所有字符串参数进行转义,但是转义很麻烦,而且需要在任何使用SQL的地方增加转义代码。

还有一个办法就是使用PreparedStatement。使用PreparedStatement可以完全避免SQL注入的问题,因为PreparedStatement始终使用?作为占位符,并且把数据连同SQL本身传给数据库,这样可以保证每次传给数据库的SQL语句是相同的,只是占位符的数据不同,还能高效利用数据库本身对查询的缓存。上述登录SQL如果用PreparedStatement可以改写如下:

User login(String name, String pass) {...String sql = "SELECT * FROM user WHERE login=? AND pass=?";PreparedStatement ps = conn.prepareStatement(sql);ps.setObject(1, name);ps.setObject(2, pass);...
}

所以,PreparedStatementStatement更安全,而且更快。

使用Java对数据库进行操作时,必须使用PreparedStatement,严禁任何通过参数拼字符串的代码!

我们把上面使用Statement的代码改为使用PreparedStatement

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {try (PreparedStatement ps = conn.prepareStatement("SELECT id, grade, name, gender FROM students WHERE gender=? AND grade=?")) {ps.setObject(1, "M"); // 注意:索引从1开始ps.setObject(2, 3);try (ResultSet rs = ps.executeQuery()) {while (rs.next()) {long id = rs.getLong("id");long grade = rs.getLong("grade");String name = rs.getString("name");String gender = rs.getString("gender");}}}
}

使用PreparedStatementStatement稍有不同,必须首先调用setObject()设置每个占位符?的值,最后获取的仍然是ResultSet对象。

另外注意到从结果集读取列时,使用String类型的列名比索引要易读,而且不易出错。

注意到JDBC查询的返回值总是ResultSet,即使我们写这样的聚合查询SELECT SUM(score) FROM ...,也需要按结果集读取:

ResultSet rs = ...
if (rs.next()) {double sum = rs.getDouble(1);
}
6. 插入

插入操作是INSERT,即插入一条新记录。通过JDBC进行插入,本质上也是用PreparedStatement执行一条SQL语句,不过最后执行的不是executeQuery(),而是executeUpdate()。示例代码如下:

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {try (PreparedStatement ps = conn.prepareStatement("INSERT INTO students (id, grade, name, gender) VALUES (?,?,?,?)")) {ps.setObject(1, 999); // 注意:索引从1开始ps.setObject(2, 1); // gradeps.setObject(3, "Bob"); // nameps.setObject(4, "M"); // genderint n = ps.executeUpdate(); // 1}
}

设置参数与查询是一样的,有几个?占位符就必须设置对应的参数。虽然Statement也可以执行插入操作,但我们仍然要严格遵循绝不能手动拼SQL字符串的原则,以避免安全漏洞。

当成功执行executeUpdate()后,返回值是int,表示插入的记录数量。此处总是1,因为只插入了一条记录。

7. 更新

更新操作是UPDATE语句,它可以一次更新若干列的记录。更新操作和插入操作在JDBC代码的层面上实际上没有区别,除了SQL语句不同:

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {try (PreparedStatement ps = conn.prepareStatement("UPDATE students SET name=? WHERE id=?")) {ps.setObject(1, "Bob"); // 注意:索引从1开始ps.setObject(2, 999);int n = ps.executeUpdate(); // 返回更新的行数}
}

executeUpdate()返回数据库实际更新的行数。返回结果可能是正数,也可能是0(表示没有任何记录更新)。

8. 删除

删除操作是DELETE语句,它可以一次删除若干列。和更新一样,除了SQL语句不同外,JDBC代码都是相同的:

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {try (PreparedStatement ps = conn.prepareStatement("DELETE FROM students WHERE id=?")) {ps.setObject(1, 999); // 注意:索引从1开始int n = ps.executeUpdate(); // 删除的行数}
}
9. 连接池

类似的,在执行JDBC的增删改查的操作时,如果每一次操作都来一次打开连接,操作,关闭连接,那么创建和销毁JDBC连接的开销就太大了。为了避免频繁地创建和销毁JDBC连接,我们可以通过连接池(Connection Pool)复用已经创建好的连接。

JDBC连接池有一个标准的接口javax.sql.DataSource,注意这个类位于Java标准库中,但仅仅是接口。要使用JDBC连接池,我们必须选择一个JDBC连接池的实现。常用的JDBC连接池有:

  • HikariCP
  • C3P0
  • BoneCP
  • Druid

目前使用最广泛的是HikariCP。我们以HikariCP为例,要使用JDBC连接池,先添加HikariCP的依赖如下:

<dependency><groupId>com.zaxxer</groupId><artifactId>HikariCP</artifactId><version>2.7.1</version>
</dependency>

紧接着,我们需要创建一个DataSource实例,这个实例就是连接池:

HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/test");
config.setUsername("root");
config.setPassword("password");
config.addDataSourceProperty("connectionTimeout", "1000"); // 连接超时:1秒
config.addDataSourceProperty("idleTimeout", "60000"); // 空闲超时:60秒
config.addDataSourceProperty("maximumPoolSize", "10"); // 最大连接数:10
DataSource ds = new HikariDataSource(config);

注意创建DataSource也是一个非常昂贵的操作,所以通常DataSource实例总是作为一个全局变量存储,并贯穿整个应用程序的生命周期。

有了连接池以后,我们如何使用它呢?和前面的代码类似,只是获取Connection时,把DriverManage.getConnection()改为ds.getConnection()

try (Connection conn = ds.getConnection()) { // 在此获取连接...
} // 在此“关闭”连接

通过连接池获取连接时,并不需要指定JDBC的相关URL、用户名、口令等信息,因为这些信息已经存储在连接池内部了(创建HikariDataSource时传入的HikariConfig持有这些信息)。一开始,连接池内部并没有连接,所以,第一次调用ds.getConnection(),会迫使连接池内部先创建一个Connection,再返回给客户端使用。当我们调用conn.close()方法时(在try(resource){...}结束处),不是真正“关闭”连接,而是释放到连接池中,以便下次获取连接时能直接返回。

因此,连接池内部维护了若干个Connection实例,如果调用ds.getConnection(),就选择一个空闲连接,并标记它为“正在使用”然后返回,如果对Connection调用close(),那么就把连接再次标记为“空闲”从而等待下次调用。这样一来,我们就通过连接池维护了少量连接,但可以频繁地执行大量的SQL语句。

通常连接池提供了大量的参数可以配置,例如,维护的最小、最大活动连接数,指定一个连接在空闲一段时间后自动关闭等,需要根据应用程序的负载合理地配置这些参数。此外,大多数连接池都提供了详细的实时状态以便进行监控。

8)Spring 开发

1. 什么是Spring?

Spring是一个支持快速开发Java EE应用程序的框架。它提供了一系列底层容器和基础设施,并可以和大量常用的开源框架无缝集成,可以说是开发Java EE应用程序的必备。

Spring最早是由Rod Johnson这哥们在他的《Expert One-on-One J2EE Development without EJB》一书中提出的用来取代EJB的轻量级框架。随后这哥们又开始专心开发这个基础框架,并起名为Spring Framework。

2. Spring Framework

Spring Framework主要包括几个模块:

  • 支持IoC和AOP的容器;
  • 支持JDBC和ORM的数据访问模块;
  • 支持声明式事务的模块;
  • 支持基于Servlet的MVC开发;
  • 支持基于Reactive的Web开发;
  • 以及集成JMS、JavaMail、JMX、缓存等其他模块。
3. IOC容器

Spring的核心就是提供了一个IoC容器,它可以管理所有轻量级的JavaBean组件,提供的底层服务包括组件的生命周期管理、配置和组装服务、AOP支持,以及建立在AOP基础上的声明式事务服务等。

1)IOC容器原理

Spring提供的容器又称为IoC容器,什么是IoC?

IoC全称Inversion of Control,直译为控制反转。那么何谓IoC?在理解IoC之前,我们先看看通常的Java组件是如何协作的。

我们假定一个在线书店,通过BookService获取书籍:

public class BookService {private HikariConfig config = new HikariConfig();private DataSource dataSource = new HikariDataSource(config);public Book getBook(long bookId) {try (Connection conn = dataSource.getConnection()) {...return book;}}
}

为了从数据库查询书籍,BookService持有一个DataSource。为了实例化一个HikariDataSource,又不得不实例化一个HikariConfig

现在,我们继续编写UserService获取用户:

public class UserService {private HikariConfig config = new HikariConfig();private DataSource dataSource = new HikariDataSource(config);public User getUser(long userId) {try (Connection conn = dataSource.getConnection()) {...return user;}}
}

因为UserService也需要访问数据库,因此,我们不得不也实例化一个HikariDataSource

在处理用户购买的CartServlet中,我们需要实例化UserServiceBookService

public class CartServlet extends HttpServlet {private BookService bookService = new BookService();private UserService userService = new UserService();protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {long currentUserId = getFromCookie(req);User currentUser = userService.getUser(currentUserId);Book book = bookService.getBook(req.getParameter("bookId"));cartService.addToCart(currentUser, book);...}
}

类似的,在购买历史HistoryServlet中,也需要实例化UserServiceBookService

public class HistoryServlet extends HttpServlet {private BookService bookService = new BookService();private UserService userService = new UserService();
}

上述每个组件都采用了一种简单的通过new创建实例并持有的方式。仔细观察,会发现以下缺点:

  1. 实例化一个组件其实很难,例如,BookServiceUserService要创建HikariDataSource,实际上需要读取配置,才能先实例化HikariConfig,再实例化HikariDataSource
  2. 没有必要让BookServiceUserService分别创建DataSource实例,完全可以共享同一个DataSource,但谁负责创建DataSource,谁负责获取其他组件已经创建的DataSource,不好处理。类似的,CartServletHistoryServlet也应当共享BookService实例和UserService实例,但也不好处理。
  3. 很多组件需要销毁以便释放资源,例如DataSource,但如果该组件被多个组件共享,如何确保它的使用方都已经全部被销毁?
  4. 随着更多的组件被引入,例如,书籍评论,需要共享的组件写起来会更困难,这些组件的依赖关系会越来越复杂。
  5. 测试某个组件,例如BookService,是复杂的,因为必须要在真实的数据库环境下执行。

从上面的例子可以看出,如果一个系统有大量的组件,其生命周期和相互之间的依赖关系如果由组件自身来维护,不但大大增加了系统的复杂度,而且会导致组件之间极为紧密的耦合,继而给测试和维护带来了极大的困难。

因此,核心问题是:

  1. 谁负责创建组件?
  2. 谁负责根据依赖关系组装组件?
  3. 销毁时,如何按依赖顺序正确销毁?

解决这一问题的核心方案就是IoC。

传统的应用程序中,控制权在程序本身,程序的控制流程完全由开发者控制,例如:

CartServlet创建了BookService,在创建BookService的过程中,又创建了DataSource组件。这种模式的缺点是,一个组件如果要使用另一个组件,必须先知道如何正确地创建它。

在IoC模式下,控制权发生了反转,即从应用程序转移到了IoC容器,所有组件不再由应用程序自己创建和配置,而是由IoC容器负责,这样,应用程序只需要直接使用已经创建好并且配置好的组件。为了能让组件在IoC容器中被“装配”出来,需要某种“注入”机制,例如,BookService自己并不会创建DataSource,而是等待外部通过setDataSource()方法来注入一个DataSource

public class BookService {private DataSource dataSource;public void setDataSource(DataSource dataSource) {this.dataSource = dataSource;}
}

不直接new一个DataSource,而是注入一个DataSource,这个小小的改动虽然简单,却带来了一系列好处:

  1. BookService不再关心如何创建DataSource,因此,不必编写读取数据库配置之类的代码;
  2. DataSource实例被注入到BookService,同样也可以注入到UserService,因此,共享一个组件非常简单;
  3. 测试BookService更容易,因为注入的是DataSource,可以使用内存数据库,而不是真实的MySQL配置。

因此,IoC又称为依赖注入(DI:Dependency Injection),它解决了一个最主要的问题:将组件的创建+配置与组件的使用相分离,并且,由IoC容器负责管理组件的生命周期。

因为IoC容器要负责实例化所有的组件,因此,有必要告诉容器如何创建组件,以及各组件的依赖关系。一种最简单的配置是通过XML文件来实现,例如:

<beans><bean id="dataSource" class="HikariDataSource" /><bean id="bookService" class="BookService"><property name="dataSource" ref="dataSource" /></bean><bean id="userService" class="UserService"><property name="dataSource" ref="dataSource" /></bean>
</beans>

上述XML配置文件指示IoC容器创建3个JavaBean组件,并把id为dataSource的组件通过属性dataSource(即调用setDataSource()方法)注入到另外两个组件中。

在Spring的IoC容器中,我们把所有组件统称为JavaBean,即配置一个组件就是配置一个Bean。

2)依赖注入方式

我们从上面的代码可以看到,依赖注入可以通过set()方法实现。但依赖注入也可以通过构造方法实现。

很多Java类都具有带参数的构造方法,如果我们把BookService改造为通过构造方法注入,那么实现代码如下:

public class BookService {private DataSource dataSource;public BookService(DataSource dataSource) {this.dataSource = dataSource;}
}

Spring的IoC容器同时支持属性注入和构造方法注入,并允许混合使用。

3)无侵入容器

在设计上,Spring的IoC容器是一个高度可扩展的无侵入容器。所谓无侵入,是指应用程序的组件无需实现Spring的特定接口,或者说,组件根本不知道自己在Spring的容器中运行。这种无侵入的设计有以下好处:

  1. 应用程序组件既可以在Spring的IoC容器中运行,也可以自己编写代码自行组装配置;
  2. 测试的时候并不依赖Spring容器,可单独进行测试,大大提高了开发效率
4)理解

IOC 控制翻转,传统的应用程序中,控制权在程序本身,程序的流控制流程完全由开发者控制。在IOC模式下,控制权发生了翻转,即从应用程序转移到了IOC容器,所有组件不再由应用程序自己创建,而是由IOC容器负责。

IoC又称为依赖注入(DI:Dependency Injection),它解决了一个最主要的问题:将组件的创建+配置与组件的使用相分离,并且,由IoC容器负责管理组件的生命周期。

因为IoC容器要负责实例化所有的组件,因此,有必要告诉容器如何创建组件,以及各组件的依赖关系

5)装配 Bean

我们前面讨论了为什么要使用Spring的IoC容器,因为让容器来为我们创建并装配Bean能获得很大的好处,那么到底如何使用IoC容器?装配好的Bean又如何使用?

我们来看一个具体的用户注册登录的例子。整个工程的结构如下:

spring-ioc-appcontext
├── pom.xml
└── src└── main├── java│   └── com│       └── itranswarp│           └── learnjava│               ├── Main.java│               └── service│                   ├── MailService.java│                   ├── User.java│                   └── UserService.java└── resources└── application.xml

首先,我们用Maven创建工程并引入spring-context依赖:

<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><groupId>com.itranswarp.learnjava</groupId><artifactId>spring-ioc-appcontext</artifactId><version>1.0-SNAPSHOT</version><packaging>jar</packaging><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target><java.version>11</java.version><spring.version>5.2.3.RELEASE</spring.version></properties><dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>${spring.version}</version></dependency></dependencies>
</project>

我们先编写一个MailService,用于在用户登录和注册成功后发送邮件通知:

public class MailService {private ZoneId zoneId = ZoneId.systemDefault();public void setZoneId(ZoneId zoneId) {this.zoneId = zoneId;}public String getTime() {return ZonedDateTime.now(this.zoneId).format(DateTimeFormatter.ISO_ZONED_DATE_TIME);}public void sendLoginMail(User user) {System.err.println(String.format("Hi, %s! You are logged in at %s", user.getName(), getTime()));}public void sendRegistrationMail(User user) {System.err.println(String.format("Welcome, %s!", user.getName()));}
}

再编写一个UserService,实现用户注册和登录:

public class UserService {private MailService mailService;public void setMailService(MailService mailService) {this.mailService = mailService;}private List<User> users = new ArrayList<>(List.of( // users:new User(1, "bob@example.com", "password", "Bob"), // bobnew User(2, "alice@example.com", "password", "Alice"), // alicenew User(3, "tom@example.com", "password", "Tom"))); // tompublic User login(String email, String password) {for (User user : users) {if (user.getEmail().equalsIgnoreCase(email) && user.getPassword().equals(password)) {mailService.sendLoginMail(user);return user;}}throw new RuntimeException("login failed.");}public User getUser(long id) {return this.users.stream().filter(user -> user.getId() == id).findFirst().orElseThrow();}public User register(String email, String password, String name) {users.forEach((user) -> {if (user.getEmail().equalsIgnoreCase(email)) {throw new RuntimeException("email exist.");}});User user = new User(users.stream().mapToLong(u -> u.getId()).max().getAsLong(), email, password, name);users.add(user);mailService.sendRegistrationMail(user);return user;}
}

注意到UserService通过setMailService()注入了一个MailService

然后,我们需要编写一个特定的application.xml配置文件,告诉Spring的IoC容器应该如何创建并组装Bean:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttps://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="userService" class="com.itranswarp.learnjava.service.UserService"><property name="mailService" ref="mailService" /></bean><bean id="mailService" class="com.itranswarp.learnjava.service.MailService" />
</beans>

注意观察上述配置文件,其中与XML Schema相关的部分格式是固定的,我们只关注两个<bean ...>的配置:

  • 每个<bean ...>都有一个id标识,相当于Bean的唯一ID;
  • userServiceBean中,通过<property name="..." ref="..." />注入了另一个Bean;
  • Bean的顺序不重要,Spring根据依赖关系会自动正确初始化。

把上述XML配置文件用Java代码写出来,就像这样:

UserService userService = new UserService();
MailService mailService = new MailService();
userService.setMailService(mailService);

只不过Spring容器是通过读取XML文件后使用反射完成的。

如果注入的不是Bean,而是booleanintString这样的数据类型,则通过value注入,例如,创建一个HikariDataSource

<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"><property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test" /><property name="username" value="root" /><property name="password" value="password" /><property name="maximumPoolSize" value="10" /><property name="autoCommit" value="true" />
</bean>

最后一步,我们需要创建一个Spring的IoC容器实例,然后加载配置文件,让Spring容器为我们创建并装配好配置文件中指定的所有Bean,这只需要一行代码:

ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");

接下来,我们就可以从Spring容器中“取出”装配好的Bean然后使用它:

// 获取Bean:
UserService userService = context.getBean(UserService.class);
// 正常调用:
User user = userService.login("bob@example.com", "password");

完整的main()方法如下:

public class Main {public static void main(String[] args) {ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");UserService userService = context.getBean(UserService.class);User user = userService.login("bob@example.com", "password");System.out.println(user.getName());}
}
6)ApplicationContext

我们从创建Spring容器的代码:

ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");

可以看到,Spring容器就是ApplicationContext,它是一个接口,有很多实现类,这里我们选择ClassPathXmlApplicationContext,表示它会自动从classpath中查找指定的XML配置文件。

获得了ApplicationContext的实例,就获得了IoC容器的引用。从ApplicationContext中我们可以根据Bean的ID获取Bean,但更多的时候我们根据Bean的类型获取Bean的引用:

UserService userService = context.getBean(UserService.class);

Spring还提供另一种IoC容器叫BeanFactory,使用方式和ApplicationContext类似:

BeanFactory factory = new XmlBeanFactory(new ClassPathResource("application.xml"));
MailService mailService = factory.getBean(MailService.class);

BeanFactoryApplicationContext的区别在于,BeanFactory的实现是按需创建,即第一次获取Bean时才创建这个Bean,而ApplicationContext会一次性创建所有的Bean。实际上,ApplicationContext接口是从BeanFactory接口继承而来的,并且,ApplicationContext提供了一些额外的功能,包括国际化支持、事件和通知机制等。通常情况下,我们总是使用ApplicationContext,很少会考虑使用BeanFactory

7)使用Annotation配置

使用Spring的IoC容器,实际上就是通过类似XML这样的配置文件,把我们自己的Bean的依赖关系描述出来,然后让容器来创建并装配Bean。一旦容器初始化完毕,我们就直接从容器中获取Bean使用它们。

使用XML配置的优点是所有的Bean都能一目了然地列出来,并通过配置注入能直观地看到每个Bean的依赖。它的缺点是写起来非常繁琐,每增加一个组件,就必须把新的Bean配置到XML中。

使用Annotation配合自动扫描能大幅简化Spring的配置,我们只需要保证:

  • 每个Bean被标注为@Component并正确使用@Autowired注入;
  • 配置类被标注为@Configuration@ComponentScan
  • 所有Bean均在指定包以及子包内。

使用@ComponentScan非常方便,但是,我们也要特别注意包的层次结构。通常来说,启动配置AppConfig位于自定义的顶层包(例如com.itranswarp.learnjava),其他Bean按类别放入子包。

4. 使用 AOP

9)SpringBoot

1. 什么是SpringBoot

我们已经在前面详细介绍了Spring框架,它的主要功能包括IoC容器、AOP支持、事务支持、MVC开发以及强大的第三方集成功能等。

那么,Spring Boot又是什么?它和Spring是什么关系?

Spring Boot是一个基于Spring的套件,它帮我们预组装了Spring的一系列组件,以便以尽可能少的代码和配置来开发基于Spring的Java应用程序。

以汽车为例,如果我们想组装一辆汽车,我们需要发动机、传动、轮胎、底盘、外壳、座椅、内饰等各种部件,然后把它们装配起来。Spring就相当于提供了一系列这样的部件,但是要装好汽车上路,还需要我们自己动手。而Spring Boot则相当于已经帮我们预装好了一辆可以上路的汽车,如果有特殊的要求,例如把发动机从普通款换成涡轮增压款,可以通过修改配置或编写少量代码完成。

因此,Spring Boot和Spring的关系就是整车和零部件的关系,它们不是取代关系,试图跳过Spring直接学习Spring Boot是不可能的。

Spring Boot的目标就是提供一个开箱即用的应用程序架构,我们基于Spring Boot的预置结构继续开发,省时省力。

其实人们把Spring Boot称为搭建程序的脚手架。其最主要作用就是帮我们快速的构建庞大的spring项目,并且尽可能的减少一切xml配置,做到开箱即用,迅速上手,让我们关注于业务而非配置。

我们可以使用SpringBoot创建java应用,并使用java –jar 启动它,就能得到一个生产级别的web工程。

2. SpringBoot运行

Spring Boot自动启动了嵌入式Tomcat,当看到Started Application in xxx seconds时,Spring Boot应用启动成功。

现在,我们在浏览器输入localhost:8080就可以直接访问页面。那么问题来了:

前面我们定义的数据源、声明式事务、JdbcTemplate在哪创建的?怎么就可以直接注入到自己编写的UserService中呢?

这些自动创建的Bean就是Spring Boot的特色:AutoConfiguration。

当我们引入spring-boot-starter-jdbc时,启动时会自动扫描所有的XxxAutoConfiguration

  • DataSourceAutoConfiguration:自动创建一个DataSource,其中配置项从application.ymlspring.datasource读取;
  • DataSourceTransactionManagerAutoConfiguration:自动创建了一个基于JDBC的事务管理器;
  • JdbcTemplateAutoConfiguration:自动创建了一个JdbcTemplate

因此,我们自动得到了一个DataSource、一个DataSourceTransactionManager和一个JdbcTemplate

类似的,当我们引入spring-boot-starter-web时,自动创建了:

  • ServletWebServerFactoryAutoConfiguration:自动创建一个嵌入式Web服务器,默认是Tomcat;
  • DispatcherServletAutoConfiguration:自动创建一个DispatcherServlet
  • HttpEncodingAutoConfiguration:自动创建一个CharacterEncodingFilter
  • WebMvcAutoConfiguration:自动创建若干与MVC相关的Bean。

引入第三方pebble-spring-boot-starter时,自动创建了:

  • PebbleAutoConfiguration:自动创建了一个PebbleViewResolver

Spring Boot大量使用XxxAutoConfiguration来使得许多组件被自动化配置并创建,而这些创建过程又大量使用了Spring的Conditional功能。例如,我们观察JdbcTemplateAutoConfiguration,它的代码如下:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })
@ConditionalOnSingleCandidate(DataSource.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties(JdbcProperties.class)
@Import({ JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class })
public class JdbcTemplateAutoConfiguration {}

当满足条件:

  • @ConditionalOnClass:在classpath中能找到DataSourceJdbcTemplate
  • @ConditionalOnSingleCandidate(DataSource.class):在当前Bean的定义中能找到唯一的DataSource

JdbcTemplateAutoConfiguration就会起作用。实际创建由导入的JdbcTemplateConfiguration完成:

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(JdbcOperations.class)
class JdbcTemplateConfiguration {@Bean@PrimaryJdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) {JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);JdbcProperties.Template template = properties.getTemplate();jdbcTemplate.setFetchSize(template.getFetchSize());jdbcTemplate.setMaxRows(template.getMaxRows());if (template.getQueryTimeout() != null) {jdbcTemplate.setQueryTimeout((int) template.getQueryTimeout().getSeconds());}return jdbcTemplate;}
}

创建JdbcTemplate之前,要满足@ConditionalOnMissingBean(JdbcOperations.class),即不存在JdbcOperations的Bean。

如果我们自己创建了一个JdbcTemplate,例如,在Application中自己写个方法:

@SpringBootApplication
public class Application {...@BeanJdbcTemplate createJdbcTemplate(@Autowired DataSource dataSource) {return new JdbcTemplate(dataSource);}
}

那么根据条件@ConditionalOnMissingBean(JdbcOperations.class),Spring Boot就不会再创建一个重复的JdbcTemplate(因为JdbcOperationsJdbcTemplate的父类)。

可见,Spring Boot自动装配功能是通过自动扫描+条件装配实现的,这一套机制在默认情况下工作得很好,但是,如果我们要手动控制某个Bean的创建,就需要详细地了解Spring Boot自动创建的原理,很多时候还要跟踪XxxAutoConfiguration,以便设定条件使得某个Bean不会被自动创建。

10)redis

11)rabbitMQ

1. 搜索与商品服务的问题

目前我们已经完成了商品详情和搜索系统的开发。我们思考一下,是否存在问题?

  • 商品的原始数据保存在数据库中,增删改查都在数据库中完成。
  • 搜索服务数据来源是索引库,如果数据库商品发生变化,索引库数据不能及时更新。
  • 商品详情做了页面静态化,静态页面数据也不会随着数据库商品发生变化。

如果我们在后台修改了商品的价格,搜索页面和商品详情页显示的依然是旧的价格,这样显然不对。该如何解决?

这里有两种解决方案:

  • 方案1:每当后台对商品做增删改操作,同时要修改索引库数据及静态页面
  • 方案2:搜索服务和商品页面服务对外提供操作接口,后台在商品增删改后,调用接口

以上两种方式都有同一个严重问题:就是代码耦合,后台服务中需要嵌入搜索和商品页面服务,违背了微服务的独立原则。

所以,我们会通过另外一种方式来解决这个问题:消息队列

2. 什么是消息队列

消息队列,即MQ,Message Queue。

消息队列是典型的:生产者、消费者模型。生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,这样就实现了生产者和消费者的解耦。

结合前面所说的问题:

  • 商品服务对商品增删改以后,无需去操作索引库或静态页面,只是发送一条消息,也不关心消息被谁接收。
  • 搜索服务和静态页面服务接收消息,分别去处理索引库和静态页面。

如果以后有其它系统也依赖商品服务的数据,同样监听消息即可,商品服务无需任何代码修改。

java学习知识储备相关推荐

  1. Java EE知识储备(二)

    Java EE知识储备(二) 目录: 1.前台线程和后台线程: 2.Spring: 3.加载驱动的方法: 4.线程的wait和notify方法: 5.线程安全问题: 6.抽象类与接口: 7.final ...

  2. java 学习知识汇总

    一:常见模式与工具 学习Java技术体系,设计模式,流行的框架与组件是必不可少的: 常见的设计模式,编码必备 Spring5,做应用必不可少的最新框架 MyBatis,玩数据库必不可少的组件 二:工程 ...

  3. 给北上奋进的你一份礼物(java面试知识储备攻略)

    写在前边: 自己带了近俩年的俩个小伙伴要脱离公司,去北上发展,自己其实提供不了太多帮助,想起自己那几年在帝都面试or被面试的场景,在看看他们迷茫的不知道该准备点什么知识去应对面试的情况下,思考了一下午 ...

  4. 黑猴子的家:Java SE 知识储备

    主要内容 概述 计算机硬件介绍 计算机发展史上的鼻祖 操作系统 万维网 软件开发介绍 计算机编程语言介绍 1.概述 概述: 计算机包括硬件(hardware)和软件(software)两部分.硬件包括 ...

  5. java 知识积累_java学习知识积累-spring常用注解

    java学习知识积累-spring常用注解 类的分层注解,由于我们后台开发都是分为三层进行开发的,所以Spring框架提供了三种对于不同层的注解方式: 控制层:@Controller 服务层:@Ser ...

  6. 接口测试学习入门(1)--前期知识储备

    转自:http://www.cnblogs.com/alwayswyy/p/5481574.html 接口测试学习入门(1)--前期知识储备 接口测试前必需知识了解: 1.接口测试的原理: 无论用那种 ...

  7. 小猪的Python学习之旅 —— 1.基础知识储备

    小猪的Python学习之旅 -- 1.基础知识储备 引言: (文章比较长,建议看目录按需学习-) 以前刚学编程的时候就对Python略有耳闻,不过学校只有C,C++,Java,C#. 和PHP有句&q ...

  8. 学习java的知识体系路线

    将网上的Java学习路线图进行归纳囊括,方便以后学习时弥补自身所欠缺的知识点,也算是给自己一个明确的学习方向.至于第一阶段,即JavaSE的基础,这里不给出. 第二阶段 技术名称 技术内容 数据库技术 ...

  9. 学习spring必须java基础知识-动态代理

    2019独角兽企业重金招聘Python工程师标准>>> Spring AOP使用动态代理技术在运行期织入增强的代码,为了揭示Spring AOP底层的工作机理,有必要对涉及到的Jav ...

最新文章

  1. configSections 位置引起的错误
  2. 如何用WordPress搭建自己的博客(转)
  3. npm ERR! { Error: EPERM: operation not permitted, mkdir 'C:\Program Files\nodejs\node_cache\_locks'
  4. 点到点 Ipsec *** 从一端能正常发起,另一端发起不成功的解决过程
  5. 【Spring】Spring Bean 生命周期
  6. python代码混淆工具_有没有好的 python 混淆器 推荐一个
  7. port 执行命令的封装和参数详解
  8. 社会管理不妨向大数据要效率
  9. fuel6.1搭建openstack
  10. Grammarly SCI语法检查总结
  11. CRISPR技术有效的防止脱靶现象方法
  12. 感应熔炼设备市场现状及未来发展趋势分析
  13. VS2015 解决 “有太多的错误导致IntelliSense引擎无法正常工作,其中有些错误无法在编辑其中查看”问题
  14. 阿里云视频直播视频存储回调功能的处理方法
  15. 杜伽K320W键盘使用手册
  16. Redis的过期策略以及内存淘汰机制
  17. 古罗马数字的基本知识
  18. IPFS白皮书中文版
  19. 关于食物消化,避免损害和改善肠道健康的建议(科学支持)
  20. [一分钟先生]于进才:绩效面谈的常用方法与技巧

热门文章

  1. [PTA] 7-23 币值转换(20 分)
  2. 【word如何删除(提)“行”、“空白页”、“分隔符”(非正常格式)】
  3. ELISA试剂盒选购指南
  4. Fabric v2.3 测试网络 - 脚本分析(二)- 创建通道createChannel.sh解析
  5. Arduino基础篇(十)-- 电源模块设计集合
  6. TypeScript学习(2)-变量声明 var let const
  7. 妨害安全策略的5大误区
  8. C++ Primer 学习笔记 第一,二章
  9. instanceof 与 isArray
  10. 我的“月账单系统”(ord(char)动态加密系统口令)