第一阶段
day01
java编译运行过程:
1) 编译期:.java源文件,经过编译,生成.class字节码文件
2) 运行期:JVM加载.class并运行.class
特点: 跨平台、一次编程到处使用
1) JVM: java虚拟机------------->专门翻译.class文件的软件
加载.class并运行.class
2) JRE: java运行环境------------>JRE 是Java Runtime Enviroment/ɪnˈvaɪrənmənt/的简称,即Java 运行时环境,它是Java程序运行所必须的环境集合,主要由Java虚拟机、Java平台核心类和若干支持文件组成。比如rt.jar文件,rt是runtime的缩写,包含了编程所使用的核心API(java.lang等),也就是核心类库编译后的class文件。还有一些配置文件,像java.util.logging的日志配置文件logging.properties。
除了包含JVM以外,还包含了运行java程序所必须的环境
JRE = JVM+java系统类库(小零件)
3) JDK: java开发工具包---------->JDK 是 Java Development/dɪˈveləpmənt
/ ToolKit/ˈtuːlkɪt
/ 的简称,也就是 Java 开发工具包。JDK 是整个 Java 的核心,包括 Java 运行环境(Java Runtime Envirnment,简称 JRE),Java 工具(比如 javac、java、javap 等等),以及 Java 基础类库(比如 rt.jar)。
最主流的 JDK 是 Oracle 公司发布的 JDK,除了 Oracle JDK(商业化,更稳定)之外,还有很多公司和组织开发了属于自己的 JDK,比较有名的有 IBM JDK(更适合 IBM) 和 OpenJDK(开源的)。每个 JDK 都有自己的优缺点,我们开发者只需要掌握 Oracle JDK 就好
JDK 有三种类型。
1)J2SE:Standard Edition,标准版,是我们通常用的一个版本,从 JDK 5.0 开始,改名为 Java SE。
2)J2EE:Enterprise Edition,企业版,从 JDK 5.0 开始,改名为 Java EE。
3)J2ME:Micro Edition,主要应用于移动设备、嵌入式设备,从 JDK 5.0 开始,改名为 Java ME
除了包含JRE以外,还包含了开发java程序所必须的命令工具
JDK=JRE+编译、运行等命令工具
结论:
1) 运行java程序的最小环境为JRE
2) 开发java程序的最小环境为JDK
SDK 和 API 有什么区别?
在开发中如果对方给你提供一个接口,这就是API;
开发中如果是一个工程提供另一个工程接口,这就是SDK;
SDK 和 java JDK有什么区别?
JAVA JDK 是 sdk 的一个子集,sdk 更加的广泛包含的更大。
JDK:Java Development Kit,中文译为Java开发工具包
SDK本质是什么?
开发工具的集合,SDK编程就是直接用windows API进行编程。
开发步骤:
2.1) 新建Java项目/ 工程---------------------------小区
2.2) 新建Java包--------------------------------------楼+单元
2.3) 新建Java类--------------------------------------房子
项目(包含)------->包(包含)------->类(包含)-------->代码
符合包含关系 , 举什么例子都可以
day02
八大基本类型从小到大:byte, short, int, long, float, double, char, boolean
int:整型,4个字节,-21个多亿到21个多亿
Java
1)整数直接量默认为int类型,但不能超出范围,若超范围则发生编译错误
2)两个整数相除,结果还是整数,小数位无条件舍弃(不会四舍五入)
3)运算时若超出范围,则发生溢出(溢出不是错误,但是需要避免)
long:长整型,8个字节
double:浮点型,8个字节
boolean: 布尔型,1个字节
char:字符型,2个字节
Java
1)采用的是Unicode字符集编码格式,一个字符对应一个码
表现的形式为字符char,但本质上是码int(0到65535之间)
2)字符型直接量必须放在单引号中,只能有一个
3)特殊符号需要通过\来转义
(ASCII码: 'a'--97 'A'--65 '0'--48)
命名:
1)只能包含字母、数字、_和$符,不能以数字开头
2)严格区分大小写
3)不能使用关键字
4)允许中文命名,但不建议,建议"英文的见名知意"
类型间的转换:
两种方式:
1)自动/隐式类型转换:小类型到大类型
2)强制类型转换:大类型到小类型
----语法: (要转换成为的数据类型)变量
----强转有可能发生溢出或丢失精度
两点规则:
1)整数直接量可以直接赋值给byte、short、char,但不能超范围
2)byte,short,char型数据参与运算时,系统将其一律先转为int再运算
day03
三目运算:
1)语法:
boolean?数1:数2
2)执行过程:
---计算boolean的值:
2.1)若为true,则整个表达式的结果为?号后的数1
2.2)若为false,则整个表达式的结果为:号后的数2
int num = 5;
int flag = num>0?1:-1;
System.out.println(flag); //1
if语句特殊写法:
if(true)
System.out.println(num + "是偶数");
常见面试题:
short s = 5;
s = s+10; 编译错误,需强转,改为:s=(short)(s+10);
s += 10; 正确,相当于:s=(short)(s+10);
++/--:自增1/自减1,可在变量前也可在变量后
1)单独使用时,在前在后都一样
2)被使用时,在前在后不一样
a++的值为a----------(a--的值为a)
++a的值为a+1--------(--a的值为a-1)
经典面试题:
- char类型变量能不能储存一个中文的汉字,为什么?
- 能否在不进行强制转换的情况下将一个 double 值赋值给 long 类型的变量?若需要 谁强转谁?
- boolean b = (3*0.1 == 0.3);
System.out.println(b):
控制台结果是true还是false?
day04
循环三要素:
- 循环变量的初始化
- 循环的条件(以循环变量为基础)
- 循环变量的改变(向着循环的结束变)
- 循环变量:在整个循环过程中所反复改变的那个数
- 循环结构:
while结构:先判断后执行,有可能一次都不执行
do...while结构:先执行后判断,至少执行一次
要素1与要素3相同时,首选do...while
变量的作用域/范围:从变量的声明开始,到包含它最近的大括号结束
Math.random()------------------0.0到0.9999999999999999...
*1000--------------------------0.0到999.99999999999999...
+1-----------------------------1.0到1000.9999999999999...
(int)--------------------------1到1000
(int)--------------------------0到999
+1-----------------------------1到1000
day05
- 循环结构:
- for结构: 应用率最高,与次数相关的循环
三种循环结构如何选择:
1)先判断循环是否与次数相关:
1.1)若相关---------------------------直接上for
1.2)若无关,再判断要素1与要素3是否相同:
1.2.1)若相同---------------------直接上do...while
1.2.2)若不同---------------------直接上while
break:跳出循环
continue:跳过循环体中剩余语句而进入下一次循环
经典基础面试题(全国):
求1-100之间的质数(质数又称素数。一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做质数;否则称为合数。)
思路:
1、外层循环作为被除数,内层循环作为除数。
2、定义一个Boolean,标记外层循环数是否为质数。默认为 true。
3、内层循环结束,如果开关还为true。即被除数为质数,打印出来。
boolean flag;
在编程中,标记(flag)是一个预先确定的位,或具有二进制值的位序列。一般地,程序用标记来记忆某件事或者用它来为另一个程序留下记号。
for (int i = 2; i <= 100; i++) {
flag=true;
for (int j = 2; j < i; j++) {
if (i % j == 0) {
flag=false;
break;
}
}
if (flag) {
System.out.println(i);
}
}
}
}
水仙花数
水仙花数(Narcissistic number)也被称为超完全数字不变数(pluperfect digital invariant, PPDI)、自恋数、自幂数、阿姆斯壮数或阿姆斯特朗数(Armstrong number),水仙花数是指一个 3 位数,它的每个位上的数字的 3次幂之和等于它本身(例如:1^3 + 5^3+ 3^3 = 153)。
for(int num=100;num<1000;num++){
int a=num/100;
int b=num/10%10;
int c=num%10;
if(a*a*a+b*b*b+c*c*c==num){
System.out.println(num);
day06
数组的复制:
是重新在内存中开辟一个存储空间
System.arraycopy(a,1,b,0,4);
int[] b = Arrays.copyOf(a,6);
a = Arrays.copyOf(a,a.length+1); 扩容
数组的排序:
Arrays.sort(arr); 对arr进行升序排列
方法的定义:五要素(很重要)
修饰词 返回值类型 方法名(参数列表){
方法体
}
return:
1)return 值; //1.1)结束方法的执行 1.2)返回结果给调用方------写在有返回值的方法中
2)return; //2.1)结束方法的执行-------------------------写在无返回值的方法中
return 、break和continue的区别和作用
1. return关键字并不是专门用于跳出循环的,return的功能是结束一个方法。 一旦在循环体内执行到一个return语句,return语句将会结束该方法,循环自然也随之结束。与continue和break不同的是,return直接结束整个方法,不管这个return处于多少层循环之内。
2.continue的功能和break有点类似,区别是continue只是中止本次循环,接着开始下一次循环。而break则是完全中止循环。
3.break用于完全结束一个循环,跳出循环体。不管是哪种循环,一旦在循环体中遇到break,系统将完全结束循环,开始执行循环之后的代码。 break不仅可以结束其所在的循环,还可结束其外层循环。此时需要在break后紧跟一个标签,这个标签用于标识一个外层循环。Java中的标签就是一个紧跟着英文冒号(:)的标识符。且它必须放在循环语句之前才有作用
// 外层循环,outer作为标识符
outer:
for (int i = 0 ; i < 5 ; i++ ){
// 内层循环
for (int j = 0; j < 3 ; j++ ){
System.out.println("i的值为:" + i + " j的值为:" + j);
if (j == 1){
// 跳出outer标签所标识的循环。
break outer;
}
}
}
拓展知识:
(主调函数, 被调函数 , 回调函数)
函数的英文是function,有功能的意思,函数的作用在于合理分配功能,增强程序的可读性。合理分解功能,降低程序的复杂性。隐藏函数内部的数据和实现,尽可能将问题局限于函数本身。
主调函数与被调函数是成对出现的。是主动与被动的关系。现在有A、B两个函数,A函数调用了B函数,那么,A函数就是主调函数,B函数就是被调函数。这和现实生活中的打电话是一样的,一个是主叫,一个是被叫。
回调函数就是一个被作为参数传递的函数。所谓的回调,就是程序员A写了一段程序(程序a),其中预留有回调函数接口,并封装好了该程序。程序员B要让a调用自己的程序b中的一个方法,于是,他通过a中的接口回调自己b中的方法。
下面是例子。
1. 首先定义一个类Caller,按照上面的定义就是程序员A写的程序a,这个类里面保存一个接口引用。
public class Caller {
private MyCallInterface callInterface;
public Caller() {
}
public void setCallFunc(MyCallInterface callInterface) {
this.callInterface = callInterface;
}
public void call() {
callInterface.printName();
}
}
2. 当然需要接口的定义,为了方便程序员B根据我的定义编写程序实现接口。
public interface MyCallInterface {
public void printName();
}
3. 第三是定义程序员B写的程序b
public class Client implements MyCallInterface {
@Override
public void printName() {
System.out.println("This is the client printName method");
}
}
4. 测试如下
public class Test {
public static void main(String[] args) {
Caller caller = new Caller();
caller.setCallFunc(new Client());
caller.call();
}
}
看到这脑子里大概会有一个模糊的框或者概念 什么是回调函数
5. 在测试方法中直接使用匿名类,省去第3步
public class Test {
public static void main(String[] args) {
Caller caller = new Caller();
// caller.setCallFunc(new Client());
caller.setCallFunc(new MyCallInterface() {
public void printName() {
System.out.println("This is the client printName method");
}
});
caller.call();
}
}
OopDay01
什么是类?什么是对象?
1)现实生活是由很多很多对象组成的,基于对象抽出了类
2)对象:软件中真实存在的单个个体/东西
类:类型/类别,代表一类个体
3)类是对象的模板,对象是类的具体的实例
类中可以包含:
1)对象所共有的属性/特征------------成员变量
2)对象所共有的行为/动作------------方法
一个类可以创建几个对象?( 多个 )
如何创建类?
Java中定义类的通用格式:修饰符 class 类名{成员,方法}
理解:修饰符是可选的,有public(公共)或不加;
class——关键词,必须有;
类名——首字母大写,且按驼峰命名规则起名,必须有;
成员——有成员属性和成员方法。
如何创建对象?
用new语句创建对象,这是最常用的创建对象的方式。(目前你们知道这个就可以了)
如何访问成员?
Java中通过类的实例(即对象)点(.)运算符来访问类的成员变量和方法。
OO:面向对象
OOA:面向对象分析
OOD:面向对象设计
OOAD:面向对象分析与设计
OOP:面向对象编程----------------你们所参与的
数据类型 引用类型变量 指向 对象
Student zs = new Student();
OopDay02
方法的签名:方法名+参数列表
- 方法的重载(overload):---------------------大大简化方法的调用
- 发生在同一类中,方法名相同,参数列表不同
- 编译器在编译时会根据方法的签名自动绑定调用的方法
- 构造方法:构造函数、构造器、构建器
- 给成员变量赋初值
- 与类同名,没有返回值类型(连void都没有)
- 在创建(new)对象时被自动调用
若自己不写构造方法,则编译器默认提供一个无参的构造方法,
- 若自己写了构造方法,则不再默认提供
- 构造方法可以重载
this:指代当前对象,哪个对象调用方法它指的就是哪个对象
只能用在方法中,方法中访问成员变量之前默认有个this.
this的用法:
1)this.成员变量名-----------访问成员变量(成员变量与局部变量同名时,访问成员变量时this不能省略)
2)this.方法名()-------------调用方法(一般不用)
3)this()-------------------调用构造方法(一般不用)
null:表示空,没有指向任何对象
若引用的值为null,则该引用不能再进行任何点操作了,
若操作则发生NullPointerException空指针异常
OopDay03
继承:
1)作用:代码复用
2)通过extends来实现继承
3)超类/父类:派生类所共有的属性和行为
派生类/子类:派生类所特有的属性和行为
4)派生类可以访问:派生类的+超类的,但超类只能访问超类的
5)一个超类可以有多个派生类,一个派生类只能有一个超类-----单一继承
6)继承具有传递性
7)java规定:构造派生类之前必须先构造超类
7.1)在派生类的构造方法中,若自己没有调用超类的构造方法
----则编译器默认super()调用超类的无参构造方法
7.2)在派生类的构造方法中,若自己调用了超类的构造方法
----则不再默认提供
注意:super()调用超类构造方法,必须位于派生类构造方法的第一行
super:指代当前对象的超类对象
1)super.成员变量名-------------------访问超类的成员变量
2)super.方法名()--------------------调用超类的方法----------明天下午讲
3)super()--------------------------调用超类的构造方法
OopDay04
1.向上造型:
1)超类型的引用指向派生类的对象
2)能点出来什么,看引用的类型-----------------这是规定,记住就可以了
2.方法的重写(override):重新写、覆盖
1)发生在父子类中,方法名相同,参数列表相同
2)重写方法被调用时,看对象的类型--------------这是规定,记住就可以了
3.重写与重载的区别:
重写:发生在父子类中,方法名相同参数列表相同,方法体不同
重写遵循两同两小一大
1)两同:方法名相同,参数列表相同
2)两小:A.子类方法的返回值小于等于父类方法的返回值
a.void/基本类型返回值必须相同
b.引用类型的返回值小于等于父类的返回值
B.子类方法抛出的异常小于或等于超类的方法抛出的异常
3)一大:子类方法的访问权限大于或等于父类方法的
重载: 1. 在同一类中,方法名称相同,参数列表不同,方法体不同,但做的事是一个类型的
2.与方法的返回值类型无关
OopDay05
final关键字
final是最终的意思,可以修饰变量,方法,类
final修饰的变量是不能被改变
fianl修饰的方法不能被重写
final修饰的类不能被继承
static关键字
static是静态的意思,常用于修饰变量和方法,当然也可以修饰代码块和内部类.
静态的特点:
1.随着类的加载而加载
2.优先于对象存在
3.被所有对象所共享
4.可以被类名点直接调用
注意事项:
A为什么静态方法只能访问静态成员?
因为静态的内容是随着类的加载而加载,他是先进入内存中的
B.静态方法中不能使用this,super关键字(静态方法中没有隐式的this)
静态变量和实例变量的区别
1.调用方式:
静态变量也称为类变量,可以直接通过类名点调用,也可以通过对象名点(引用)调用[但是不建议].这个变
量是属于类的.
实例变量只能通过对象名(引用)点调用.这个变量属于对象的.
2.储存方式:
静态变量存储在方法区中
成员变量储存在堆中
3.生命周期
静态变量随着类的加载而存在,随着类的消失而消失,生命周期较长
成员变量随着对象的创建而存在,随着对象的消失而消失
4.与对象的相关性
静态变量是所有对象共享的数据
实例变量是每个对象所特有的数据
OopDay06
static:
1)静态变量:
1.1)由static修饰
1.2)属于类,存储在方法区中,只有一份
1.3)常常通过类名点来访问
1.4)何时用:所有对象所共享的数据(图片、音频、视频等)
2)静态方法:
2.1)由static修饰
2.2)属于类,存储在方法区中,只有一份
2.3)常常通过类名点来访问
2.4)静态方法中没有隐式this传递,所以静态方法中不能直接访问实例成员
2.5)何时用:方法的操作与对象无关
3)静态块:
3.1)由static修饰
3.2)属于类,在类被加载期间自动执行,一个类只被加载一次,所以静态块也只执行一次
3.3)何时用:加载/初始化静态资源(图片、音频、视频等)
static final常量:
1)必须声明同时初始化
2)常常通过类名点来访问,不能被改变
3)建议:常量名所有字母都大写,多个单词用_分隔
4)编译器在编译时会将常量直接替换为具体的值,效率高
5)何时用:数据永远不变,并且经常使用
抽象方法:
1)由abstract修饰
2)只有方法的定义,没有具体的实现(连{}都没有)
抽象类:
1)由abstract修饰
2)包含抽象方法的类必须是抽象类
3)抽象类不能被实例化(new对象)
4)抽象类是需要被继承的,派生类:
4.1)重写所有抽象方法------------常用
4.2)也声明为抽象类--------------不常用
5)抽象类的意义:
5.1)封装共有的属性和行为----------代码复用
5.2)为所有派生类提供统一的类型-----向上造型
5.3)包含抽象方法,为所有派生类提供统一的入口(能点出来)
派生类的具体实现不同,但入口是一致的
1.问:抽象方法是不是多余的呢?(派生类总归还得重写step(),抽象方法达不到让派生类复用的效果)
答:不是的,抽象方法存在的意义在于,当向上造型时通过超类的引用能点出step()来
2.问:既然抽象方法的意义仅仅在于造型后能点出来,那设计为普通方法也能点,为何非要设计为抽象方法?
答:设计为普通方法,则派生类可以重写也可以不重写,而设计为抽象方法,可以强制派生类必须重写
----抽象方法可以达到强制必须重写的目的
OopDay07
- 成员内部类:应用率低类中套类,外面的称为外部类,里面的称为内部类
- 内部类通常只服务于外部类,对外不具备可见性
- 内部类对象通常在外部类中创建
内部类中可以直接访问外部类的成员(包括私有的)
----内部类中有个隐式的引用指向了创建它的外部类对象-------外部类名.this--------必须记住(API时用)
- 匿名内部类:-----------------大大便于访问数据(明天做案例时才能体会)
- 若想创建一个类(派生类)的对象,并且对象只被创建一次,此时该类不必命名,称为匿名内部类
- 匿名内部类中不能修改外面变量的值,因为在此处系统默认该变量为final的---------必须记住(API时用)
面试题:
问: 内部类有独立的.class吗?
- 答: 有
OopDay08
接口:
引用类型,interface定义,常量和抽象方法,不能被实例化,需要被实现,实现类必须重写所有抽象方法
- 一个类可以实现多个接口,用逗号分隔,若又继承又实现时,应先继承后实现,接口继承接口
多态:
1) 意义:行为多态、对象多态
2) 向上造型:可以向上造型为: 超类+所实现的接口
3) 强制类型转换,成功的条件:
3.1) 引用所指向的对象,就是该类型
3.2) 引用所指向的对象,实现了该接口或继承了该类
- 4) 强转若不满足如上条件,则发生ClassCastException类型转换异常,建议强转之前先用instanceof判断
抽象类和接口的区别(面试题)
1.抽象类是由abstract class修饰的,接口是由interface修饰的
2.抽象类中可以有成员变量,也可以有常量,接口中都是常量
3.抽象类中可以有普通方法也可以有抽象方法,接口中都是抽象方法(Java8以后可以有普通方法,但是普 通方法必须由static或default修饰的)
4.抽象类中可以定义构造方法,接口中不能定义构造方法,但是都没有意思,因为不能被实例化 5.抽象类中任何访问控制修饰符都可以,接口中访问控制修饰符只能为public
OopDay09
- 内存管理:由JVM来管理的
堆:
1) 存储new出来的对象(包括实例变量)
2) 垃圾: 没有任何引用所指向的对象
垃圾回收器(GC)不定时到内存中清扫垃圾,回收过程是透明的(看不到的),
并非一看到垃圾就立即回收,通过调用System.gc()建议JVM尽快调度GC来回收
3) 实例变量的生命周期:
创建对象时存储在堆中,对象被回收时一并被回收
4) 内存泄漏:不再使用的对象没有被及时的回收,严重的泄漏会导致系统的崩溃
- 建议:不再使用的对象应及时将引用设置为null
栈:
1) 正在调用的方法中的所有局部变量(包括方法的参数)
2) 调用方法时,会在栈中为该方法分配一块对应的栈帧,栈帧中存储局部变量(包括方法参数),
方法调用结束时,栈帧自动被清除,局部变量一并被清除
3) 局部变量的生命周期:
- 调用方法时存储在栈中,方法调用结束时与栈帧一并被清除
方法区:
1) 存储.class字节码文件(包括静态变量、所有方法)
- 2) 方法只有一份,通过this来区分具体的访问对象
面向对象三大特征:
1.封装:
1)类:封装的是对象的属性和行为
2)方法:封装的是具体的业务逻辑实现
3)访问控制修饰符:封装的是具体的访问权限
2.继承:
1)作用:代码复用
2)超类:所有派生类所共有的属性和行为
接口:部分派生类所共有的属性和行为
派生类:派生类所特有的属性和行为
3)单一继承、多接口实现,传递性
3.多态:
1)行为多态:所有抽象方法都是多态的(通过方法的重写来实现)
对象多态:所有对象都是多态的(通过向上造型来实现)
2)向上造型、强制类型转换、instanceof判断
JavaSE
Git快速入门及使用------->见网址
https://segmentfault.com/a/1190000040229255
面试题:
String
String 和 int 类型之间如何转换
int → String
1)String s = i + "";
2)String s = String.valueOf(i);
3)String s = Integer.toString(i);
String → int
1)int i = Integer.parseInt(s);
2)int i=Integer.valueOf(s).intValue();
String s = new String("abc");创建了几个对象
创建了一个或者两个String类型对象
1.new对象的时候,系统会先检测常量池中是否含有"abc"这个字符串对象,如果有,则不在常量池中创
建,只在堆中开辟一个空间存放String对象.
2.如果字符串常量池中没有这个字符串对象,则在常量池中和堆中各创建一个对象
String s = new String("ab"+"c");创建了几个对象
创建了一个或者两个String类型对象
"ab"+"c" 在编译时 自动 转变成"abc"
String,StringBuilder,StringBuffer的区别
String,StringBuffer,StringBuilder这三者都是操纵字符串的类;
String 底层是由final修饰的char[],声明的对象不可以被改变;如果每次改变相当于重新生成一个
String对象,并将指针指向新的对象;
而StringBuffer,StringBuilder 没有被final修饰,并且提供了一个自动扩容的机制(初始的默认
长度为16),会自动扩容,扩容的长度为原长度的2倍加2,所以在对于拼接字符串效率要比 String 要
高;
StringBuffer,StringBuilder虽然都是可变的字符串,但是StringBuffer是由synchronized修
饰的,线程安全,但是效率较低
你都用过String里的那些方法
1)length():获取字符串的长度,其实也就是字符个数
String str = "adsfaxsdfas沙发上案发地方";
System.out.println(str.length());
1
2
运行结果:
18
2)charAt(int index):获取指定索引处的字符
String str = "adsfaxsdfas沙发上案发地方";
char[] c = {'a','d','s','f','a'};
System.out.println(str.charAt(12));
1
2
3
4
运行结果:
发
3)indexOf(String str):获取str在字符串对象中第一次出现的索引
String str = "adsfaxsdfas沙发上案发地方";
System.out.println(str.indexOf('a',5));
1
2
运行结果:
9
4)substring(int start):从start开始截取字符串
String str = "adsfaxsdfas沙发上案发地方";
System.out.println(str.substring(1));
1
2
运行结果:
dsfaxsdfas沙发上发地方
5)String substring(int start,int end):从start开始,到end结束截取字符串。包括start,不包括end
String str = "adsfaxsdfas沙发上案发地方";
System.out.println(str.substring(1, 12));
1
2
运行结果:
dsfaxsdfas沙
3.String判断功能
1)equals(Object obj):比较字符串的内容是否相同
String str = "adsfaxsdfas沙发上案发地方";
System.out.println(str.equals("adsfaxsdfas沙发上发地方"));
System.out.println(str.equals("adsfaxsdfas"));
1
2
3
运行结果:
true
false
2)equalsIgnoreCase(String anotherString):比较字符串的内容是否相同,忽略大小写
String str = "adsfaxsdfas沙发上案发地方";
System.out.println(str.equalsIgnoreCase("ADsfaxsdfAs沙发上发地方"));
1
2
运行结果:
true
3)startsWith(String prefix):判断字符串对象是否以指定的字符开头(区分大小写)
String str = "adsfaxsdfas沙发上案发地方";
System.out.println(str.startsWith("a"));
System.out.println(str.startsWith("A"));
1
2
3
运行结果:
true
false
4)startsWith(String prefix,int toffset):判断字符串对象是否以指定的字符开头,参数toffset为指定从哪个下标开始
String str = "adsfaxsdfas沙发上案发地方";
System.out.println(str.startsWith("f", 3));
System.out.println(str.startsWith("f", 4));
1
2
3
运行结果:
true
false
5)endsWith(String str):判断字符串对象是否以指定的字符结尾
String str = "adsfaxsdfas沙发上案发地方";
System.out.println(str.endsWith("x"));
System.out.println(str.endsWith("方"));
1
2
3
运行结果:
false
true
6)isEmpty():判断指定字符串是否为空
4.String类中的转化方法:
1)toCharArray():把字符串转换为字符数组
public static void main(String[] args) {
String str = "adsfaxsdfas沙发上发地方";
char arr[] = str.toCharArray();
printArray(arr);
}
public static void printArray(char a[]) {
for(int i=0;i<a.length;i++) {
System.out.print(a[i]+"--");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
运行结果:
a–d--s–f--a–x--s–d--f–a--s–沙--发–上--发–地--方–
2)toLowerCase():把字符串转换为小写字符串
String str1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
System.out.println(str1.toLowerCase());
1
2
运行结果:
abcdefghijklmnopqrstuvwxyz
3)toUpperCase():把字符串转换为大写字符串
String str1 = "abcdefghijklmnopqrstuvwxyz";
System.out.println(str2.toUpperCase());
1
2
运行结果:
ABCDEFGHIJKLMNOPQRSTUVWXYZ
5.其他常用方法
1)trim():去除字符串两端空格
String str3 = " a c e x u a n x u a n ";
System.out.println(str3.trim());
System.out.println(str3);
运行结果:
a c e x u a n x u a n
a c e x u a n x u a n
1
2
3
4
5
6
7
2)split():去除字符串中指定的的字符,然后返回一个新的字符串
public static void main(String[] args) {
String str = "adsfaxsdfas沙发上发地方";
String array[] = str.split("a");
printString(array);
}
public static void printString(String a[]) {
for(int i=0;i<a.length;i++) {
System.out.print(a[i]);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
运行结果:
dsfxsdfs沙发上发地方
3)subSequence(int beginIndex,int endIndex ):截取字符串中指定位置的字符组成一个新的字符串
String str = "adsfaxsdfas沙发上发地方";
System.out.println(str.subSequence(1, 10));
1
2
运行结果:
dsfaxsdfa
4)replace(char oldChar, char newChar):将指定字符替换成另一个指定的字符
String str = "adsfaxsdfas沙发上发地方";
System.out.println(str.replace('a', 's'));
1
2
运行结果:
sdsfsxsdfss沙发上发地方
5)replaceAll(String regex,String replasement):用新的内容替换全部旧内容
String str4 = "Hello,world!";
System.out.println(str4.replaceAll("l", "&"));
1
2
运行结果:
He&&o,wor&d!
6)replaceFirst(String regex,String replacement):替换首个满足条件的内容
String str = "adsfaxsdfas沙发上发地方";
System.out.println(str.replaceFirst("沙", "璇"));
1
2
运行结果:
adsfaxsdfas璇发上发地方
7)lastIndexOf(String str):返回指定字符出现的最后一次的下标
String str4 = "Hello,world!";
System.out.println(str4.lastIndexOf("l"));
1
2
运行结果:
9
8)contains(CharSequence s):查看字符串中是都含有指定字符
String str4 = "Hello,world!";
System.out.println(str4.contains("l"));
1
2
运行结果:
true
9)concat(String str):在原有的字符串的基础上加上指定字符串
String str5 = "dr";
System.out.println(str5.concat("eam"));
1
2
运行结果:
dream
File
你都用过File里的那些方法?
- isDirectory() 是否为文件夹
- isFile() 是否为文件
- getPath() 得到file的路径
- getName() 得到最后一层的名字
- getParent() 得到去掉最后一层的路径
- getParentFile() 得到父类路径的新文件
- renameTo() 改名
- mkdir() 创建新文件夹,只能创建一层
- mkdirs() 创建新文件夹,可以多层
- createNewFile() 创建新文件,只能一层
- exists() 路径是否存在
- delete() 删除文件或者目录(为空的目录)
- list() 返回该路径下文件或者文件夹的名字数组
- listFiles() 返回该路径下文件或者文件夹组成的File数组
- separator 代替文件或文件夹路径的斜线或反斜线,防止跨平台出现错误
FileFilter
boolean accept(File pathname)
测试指定的抽象路径名是否应包含在路径名列表中。
IO
1、IO的分类有哪些?
按流向分为输入流和输出流
按类型分为字节流和字符流
按功能分为节点流和处理流
2、字节流和字符流的区别?
字节流:处理除了文字之外的数据,无缓冲区,每传一次数据就会打开一次文件
字符流:处理文字和字符的数据,有缓存区,使用缓冲区打开文件次数少
3、节点流和处理流的区别?
节点流:可以从某节点读数据或向某节点写数据的流。如 FileInputStream
处理流:对节点流的包装,有比较多的功能
4、谈谈Java IO里面的常见类,字节流,字符流、接口、实现类、方法阻塞
答:输入流就是从外部文件输入到内存,输出流主要是从内存输出到文件。
IO里面常见的类,第一印象就只知道IO流中有很多类,IO流主要分为字符流和字节流。字符流中有抽象类InputStream和OutputStream,它们的子类FileInputStream,FileOutputStream,BufferedOutputStream等。字符流BufferedReader和Writer等。都实现了Closeable, Flushable, Appendable这些接口。程序中的输入输出都是以流的形式保存的,流中保存的实际上全都是字节文件。
java中的阻塞式方法是指在程序调用改方法时,必须等待输入数据可用或者检测到输入结束或者抛出异常,否则程序会一直停留在该语句上,不会执行下面的语句。比如read()和readLine()方法。
5、字符流和字节流有什么区别?
要把一片二进制数据数据逐一输出到某个设备中,或者从某个设备中逐一读取一片二进制数据,不管输入输出设备是什么,我们要用统一的方式来完成这些操作,用一种抽象的方式进行描述,这个抽象描述方式起名为IO流,对应的抽象类为OutputStream和InputStream ,不同的实现类就代表不同的输入和输出设备,它们都是针对字节进行操作的。
在应用中,经常要完全是字符的一段文本输出去或读进来,用字节流可以吗?
计算机中的一切最终都是二进制的字节形式存在。对于“中国”这些字符,首先要得到其对应的字节,然后将字节写入到输出流。读取时,首先读到的是字节,可是我们要把它显示为字符,我们需要将字节转换成字符。由于这样的需求很广泛,人家专门提供了字符流的包装类。
底层设备永远只接受字节数据,有时候要写字符串到底层设备,需要将字符串转成字节再进行写入。字符流是字节流的包装,字符流则是直接接受字符串,它内部将串转成字节,再写入底层设备,这为我们向IO设别写入或读取字符串提供了一点点方便。
6、讲讲NIO
答:看了一些文章,传统的IO流是阻塞式的,会一直监听一个ServerSocket,在调用read等方法时,他会一直等到数据到来或者缓冲区已满时才返回。调用accept也是一直阻塞到有客户端连接才会返回。每个客户端连接过来后,服务端都会启动一个线程去处理该客户端的请求。并且多线程处理多个连接。每个线程拥有自己的栈空间并且占用一些 CPU 时间。每个线程遇到外部未准备好的时候,都会阻塞掉。阻塞的结果就是会带来大量的进程上下文切换。
对于NIO,它是非阻塞式,核心类:
1.Buffer为所有的原始类型提供 (Buffer)缓存支持。
2.Charset字符集编码解码解决方案
3.Channel一个新的原始 I/O抽象,用于读写Buffer类型,通道可以认为是一种连接,可以是到特定设备,程序或者是网络的连接。
序列化
说说对序列化的理解,是怎么实现的,什么场景下需要它 ?
序列化是指将Java对象转换为字节序列的过程
反序列化则是将字节序列转换为Java对象的过程。
Java对象序列化必须实现Serializable接口,使得对象能通过网络传输或者文件储存等方式传输. 本
质上讲,序列化就是把实体对象状态按照一定的格式写入到有序字节流,反序列化就是从有序字节流重建
对象,恢复对象状态。
Java IO Sammary(总结)
(1)明确源和目的。
数据source:就是需要读取,可以使用两个体系:InputStream、Reader;
数据destination:就是需要写入,可以使用两个体系:OutputStream、Writer;
(2)操作的数据是否是纯文本数据?
如果是:
数据source:Reader
数据destination:Writer
如果不是:
数据source:InputStream
数据destination:OutputStream
(3)Java IO体系中有太多的对象,到底用哪个呢?
明确操作的数据设备。
数据source对应的设备:硬盘(File),内存(数组),键盘(System.in)
数据destination对应的设备:硬盘(File),内存(数组),控制台(System.out)。
记住,只要一读取键盘录入,就用这句话。
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
反之
BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(System.out));
异常处理机制
1.运行时异常
定义:RuntimeException及其子类都被称为运行时异常。
特点:Java编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕
获它",还是会编译通过。例如,除数为零时产生的ArithmeticException异常,数组越界时产生的IndexOutOfBoundsException异常,failfast机制产生的ConcurrentModi?cationException异常(java.util包下面的所有的集合类都是快速失败的,“快速失败”也就是fail-fast,它是
Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。
例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的
内容),那么这个时候程序就会抛出ConcurrentModi?cationException 异常,从而产生fail-fast机制,这个错叫并发修改异常。Fail-safe,java.util.concurrent包下面的所有的类都是安全失败的,在遍历过程中,如果已经遍历的数组上的内容变化了,迭代器不会抛出
ConcurrentModi?cationException异常。如果未遍历的数组上的内容发生了变化,则有可能反映到迭代过程中。这就是
ConcurrentHashMap迭代器弱一致的表现。ConcurrentHashMap的弱一致性主要是为了提升效率,是一致性与效率之间的一种权衡。要成为强一致性,就得到处使用锁,甚至是全局锁,这就与Hashtable和同步的HashMap一样了。)等,都属于运行时异常。
常见的五种运行时异常:
ClassCastException(类转换异常)
IndexOutOfBoundsException(数组越界)
NullPointerException(空指针异常)
ArrayStoreException(数据存储异常,操作数组是类型不一致)
Bu?erOver?owException
2.被检查异常
定义:Exception类本身,以及Exception的子类中除了"运行时异常"之外的其它子类都属于被检查异常。特点 : Java编译器会检查它。此类异常,要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。例如,CloneNotSupportedException就属于被
检查异常。当通过clone()接口去克隆一个对象,而该对象对应的类没有实现Cloneable接口,就会抛出CloneNotSupportedException异常。被检查异常通常都是可以恢复的。
如:
IOException
FileNotFoundException
SQLException
被检查的异常适用于那些不是因程序引起的错误情况,比如:读取文件时文件不存在引发的FileNotFoundException 。然而,不被检查的异
常通常都是由于糟糕的编程引起的,比如:在对象引用时没有确保对象非空而引起的 NullPointerException 。
3.错误
定义 : Error类及其子类。
特点 : 和运行时异常一样,编译器也不会对错误进行检查。当资源不足、约束失败、或是其它程序无法继续运行的条件发生时,就产生错误。程序本身无法修复这些错误的。例如,VirtualMachineError就属于错误。出现这种错误会导致程序终止运行。OutOfMemoryError、ThreadDeath。
Java虚拟机规范规定JVM的内存分为了好几块,比如堆,栈,程序计数器,方法区等
1、Java中异常分为哪两种?
编译时异常
运行时异常
2、异常的处理机制有几种?
异常捕捉:try…catch…finally,异常抛出:throws。
3、如何自定义一个异常
继承一个异常类,通常是RumtimeException或者Exception
4、try catch finally,try里有return,finally还执行么?
执行,并且finally的执行早于try里面的return
结论:
1.不管有木有出现异常,finally块中代码都会执行;
2.当try和catch中有return时,finally仍然会执行;
3.finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是在finally执行前确定的;
4.finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。
5、final,fianlly,fianlize的区别?
1)final用于修饰属性,方法,和类,分别表示属性不可变,方法不可覆盖,,类不可被继承(不能再派生出新的子类)
2)finally作为异常处理的一部分,它只能用在try/catch语句中,并且附带一个语句块,表示这段语句最终一定要被执行,经常被用在需要释放资源的情况下。
3)finalize 是Object类的一个方法,在垃圾回收器执行时会调用被回收对象的finalize()方法,可以覆盖此方法来实现对其他资源的回收,例如关闭文件等。需要注意的是,一旦垃圾回收器准备好释放对象占用的空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
6、 Excption与Error包结构
Java可抛出(Throwable)的结构分为三种类型:被检查的异常(CheckedException),运行时异常
(RuntimeException),错误(Error)。
7、Thow与thorws区别
位置不同
throws 用在函数上,后面跟的是异常类,可以跟多个;而 throw 用在函数内,后面跟的
是异常对象。
功能不同
throws 用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方
式;throw 抛出具体的问题对象,执行到 throw,功能就已经结束了,跳转到调用者,并
将具体的问题对象抛给调用者。也就是说 throw 语句独立存在时,下面不要定义其他语
句,因为执行不到。
throws 表示出现异常的一种可能性,并不一定会发生这些异常;throw 则是抛出了异常,
执行 throw 则一定抛出了某种异常对象。
两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异
常,真正的处理异常由函数的上层调用处理。
8、Error与Exception区别?
Error和Exception都是java错误处理机制的一部分,都继承了Throwable类。
Exception表示的异常,异常可以通过程序来捕捉,或者优化程序来避免。
Error表示的是系统错误,不能通过程序来进行错误处理。
线程
一、多线程基础基础知识
1. 并发编程
1.1 并发编程的优缺点
优点:
充分利用多核CPU的计算能力,通过并发编程的形式将多核CPU的计算能力发挥到极致,性能得到提升。
方面进行业务的拆分。提高系统并发能力和性能:高并发系统的开发,并发编程会显得尤为重要,利用好多线程机制可以大大提高系统的并发能力及性能;面对复杂的业务模型,并行程序会比串行程序更适应业务需求,而并发编程更适合这种业务拆分。cai
缺点:
并发编程的目的是为了提高程序的执行效率,提高程序运行速度,但并发编程并不是总能提高性能,有时还会遇到很多问题,例如:内存泄漏,线程安全,死锁等。
1.2 并发编程的三要素
并发编程的三要素:(也是带来线程安全所在)
原子性:原子是不可再分割的最小单元,原子性是指一个或多个操作要么全部执行成功,要么全部执行失败。
可见性:一个线程对共享变量的修改,另一个线程能看到(synchronized,volatile)
有序性:程序的执行顺序按照代码的先后顺序
线程安全的问题原因有:
1. 线程切换带来的原子性问题
2. 缓存导致的可见性问题
3. 编译优化带来的有序性问题
1
2
3
解决方案:
JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
synchronized、volatile、LOCK,可以解决可见性问题
Happens-Before 规则可以解决有序性问题
1.3 并发和并行有和区别
并发:多个任务在同一个CPU上,按照细分的时间片轮流交替执行,由于时间很短,看上去好像是同时进行的。
并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的同时进行。
串行:有n个任务,由一个线程按照顺序执行。
1.4 什么是多线程,多线程的优劣?
定义:多线程是指程序中包含多个流,即在一个程序中可以同时进行多个不同的线程来执行不同的任务
优点:
可以提高CPU的利用率,在多线程中,一个线程必须等待的时候,CPU可以运行其它线程而不是等待,这样就大大提高了程序的效率,也就是说单个程序可以创建多个不同的线程来完成各自的任务。
缺点:
线程也是程序,线程也需要占内存,线程也多内存也占的也多。
多线程需要协调和管理,所以需要CPU跟踪线程。
线程之间共享资源的访问会相互影响,必须解决禁用共享资源的问题。
2. 线程与进程
2.1 什么是线程与进程
进程:内存中运行的运用程序,每个进程都有自己独立的内存空间,一个进程可以由多个线程,例如在Windows系统中,xxx.exe就是一个进程。
线程:进程中的一个控制单元,负责当前进程中的程序执行,一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可以共享数据。
2.2 线程与进程的区别
根本区别:进程是操作系统资源分配的基本单元,而线程是处理器任务调度的和执行的基本单位。
资源开销:每个进程都有自己独立的代码和空间(程序上下文),程序之间的切换会有较大的开销;线程可以看作轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行的过程不是一条线的,而是多条线(多个线程),共同完成;线程是进程的一部分,可以把线程看作是轻量级的进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。
2.3 用户线程与守护线程
用户(User)线程:运行在前台,执行具体任务,如程序的主线程,连接网络的子线程都是用户线程。
守护(Daemon)线程:运行在后台,为其它前台线程服务,也可以说守护线程是JVM非守护线程的”佣人“,一旦所有线程都执行结束,守护线程会随着JVM一起结束运行。
main函数就是一个用户线程,main函数启动时,同时JVM还启动了好多的守护线程,如垃圾回收线程,比较明显的区别时,用户线程结束,JVM退出,不管这个时候有没有守护线程的运行,都不会影响JVM的退出。
2.4 什么是线程死锁
死锁是指两个或两个以上进程(线程)在执行过程中,由于竞争资源或由于彼此通信造成的一种堵塞的现象,若无外力的作用下,都将无法推进,此时的系统处于死锁状态。
如图,线程A拥有的资源2,线程B拥有的资源1,此时线程A和线程B都试图去拥有资源1和资源2,但是它们的🔒还在,因此就出现了死锁。
2.5 形成死锁的四个必要条件
互斥条件:线程(进程)对所分配的资源具有排它性,即一个资源只能被一个进程占用,直到该进程被释放。
请求与保持条件:一个进程(线程)因请求被占有资源而发生堵塞时,对已获取的资源保持不放。
不剥夺条件:线程(进程)已获取的资源在未使用完之前不能被其他线程强行剥夺,只有等自己使用完才释放资源。
循环等待条件:当发生死锁时,所等待的线程(进程)必定形成一个环路,死循环造成永久堵塞。
2.6 如何避免死锁
我们只需破坏形参死锁的四个必要条件之一即可。
破坏互斥条件:无法破坏,我们的🔒本身就是来个线程(进程)来产生互斥
破坏请求与保持条件:一次申请所有资源
破坏不剥夺条件:占有部分资源的线程尝试申请其它资源,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件:按序来申请资源。
2.7 什么是上下文的切换
当前任务执行完,CPU时间片切换到另一个任务之前会保存自己的状态,以便下次再切换会这个任务时可以继续执行下去,任务从保存到再加载执行就是一次上下文切换。
3. 创建线程
3.1 创建线程的四种方式
继承Thread类
实现Runnable接口
实现Callable接口
Executors工具类创建线程池
3.2 Runnable接口和Callable接口有何区别
相同点:
Runnable和Callable都是接口
都可以编写多线程程序
都采用Thread.start()启动线程
不同点:
Runnable接口run方法无返回值,Callable接口call方法有返回值,是个泛型,和Futrue和FutureTask配合用来获取异步执行结果。
Runable接口run方法只能抛出运行时的异常,且无法捕获处理;Callable接口call方法允许抛出异常,可以获取异常信息。
注:Callable接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会堵塞主线程继续往下执行,如果不调用就不会堵塞。
3.2 run()方法和start()方法有和区别
每个线程都是通过某个特定的Thread对象对于的run()方法来完成其操作的,run方法称为线程体,通过调用Thread类的start方法来启动一个线程。
start()方法用于启动线程,run()方法用于执行线程的运行代码,run()可以反复调用,而start()方法只能被调用一次。
start()方法来启动一个线程,真正实现了多线程的运行。调用start()方法无需等待run()方法体代码执行结束,可以直接继续执行其它的代码;调用start()方法线程进入就绪状态,随时等该CPU的调度,然后可以通过Thread调用run()方法来让其进入运行状态,run()方法运行结束,此线程终止,然后CPU再调度其它线程。
3.3 为什么调用start()方法会执行run()方法,为什么不能直接调用run()方法
这是一个常问的面试题,new Thread,线程进入了新建的状态,start方法的作用是使线程进入就绪的状态,当分配到时间片后就可以运行了。start方法会执行线程前的相应准备工作,然后在执行run方法运行线程体,这才是真正的多线程工作。
如果直接执行了run方法,run方法会被当作一个main线程下的普通方法执行,并不会在某个线程中去执行它,所以这并不是多线程工作。
小结:
调用start方法启动线程可使线程进入就绪状态,等待运行;run方法只是thread的一个普通方法调用,还是在主线程里执行。
3.4 什么是Callable和Future
Callable接口也类似于Runnable接口,但是Runnable不会接收返回值,并且无法抛出返回结果的异常,而Callable功能更强大,被线程执行后,可有返回值,这个返回值可以被Future拿到,也就是说Future可以拿到异步执行任务的返回值。
Future接口表示异步任务,是一个可能没有完成的异步任务结果,所以说Callable用于产生结果,Future用于接收结果。
3.5 什么是FutureTask
FutureTask是一个异步运算的任务,FutureTask里面可以可以传入Callable实现类作为参数,可以对异步运算任务的结果进行等待获取,判断是否已经完成,取消任务等操作。只有当结果完成之后才能取出,如果尚未完成get方法将堵塞。一个Future对象可以调用Callable和Runable的对象进行包装,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。
4. 线程状态和基本操作
4.1 线程声明周期的6种状态
很多地方说线程有5种状态,但实际上是6中状态,可以参考Thread类的官方api
新创建:又称初始化状态,这个时候Thread才刚刚被new出来,还没有被启动。
可运行状态:表示已经调用Thread的start方法启动了,随时等待CPU的调度,此状态又被称为就绪状态。
被终止:死亡状态,表示已经正常执行完线程体run()中的方法了或者因为没有捕获的异常而终止run()方法了。
计时状态:调用sleep(参数)或wait(参数)后线程进入计时状态,睡眠时间到了或wait时间到了,再或者其它线程调用notify并获取到锁之后开始进入可运行状态。另一种情况,其它线程调用notify没有获取到锁或者wait时间到没有获取到锁时,进入堵塞状态。
无线等待状态:获取锁对象后,调用wait()方法,释放锁进入无线等待状态
锁堵塞状态:wait(参数)时间到或者其它线程调用notify后没有获取到锁对象都会进入堵塞状态,只要一获取到锁对象就会进入可运行状态。
堵塞状态的详解:
4.2 Java用到的线程调度算法是什么?
计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获取到CPU的使用权才能执行指令,所谓多线程的并发运行,其实从宏观上看,各线程轮流获取CPU的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待,CPU的调度,JVM有一项任务就是负责CPU的调度,线程调度就是按照特定的机制为多个线程分配CPU的使用权。
有两种调度模型:分时调度和抢占式调度
分时调度: 就是让所有的线程轮流获得CPU的使用权,并且平均分配到各个线程占有CPU的时间片。
抢占式调度:Java虚拟机采用抢占式调度模型,是指优先让线程池中优先级高的线程首先占用CPU,如果线程池中优先级相同,那么随机选择一个线程,使其占有CPU,处于这个状态的CPU会一直运行,优先级高的分的CPU的时间片相对会多一点。
4.2 Java线程调度策略
线程调度优先选择优先级高的运行,但是如果出现一下情况,就会终止运行(不是进入死亡状态):
线程调用了yield方法让出CPU的使用权,线程进入就绪状态。
线程调用sleep()方法,使其进入计时状态线程由于IO受阻, 另一个更高的优先级线程出现在支持的时间片系统中,改线程的时间片用完。
4.3 什么是线程调度(Thread Scheduler)和时间分片(Time Slicing )
线程调度是一个操作系统服务,它负责为储在Runnable状态的线程分配CPU时间片,一旦我们创建一个线程并启动它,它的执行便依赖线程调度器的实现。
时间分片是指CPU可用时间分配给Runnable的过程,分配的时间可以根据线程优先级或线程等待时间。
4.4 Java线程同步和线程调度的相关方法
wait():调用后线程进入无限等待状态,并释放所持对象的锁
sleep():使一个线程进入休眠状态(堵塞状态),带有对象锁,是一个静态方法,需要处理InterruptException异常。
notify():唤醒一个处于等待状态的线程(无线等待或计时等待),如果多个线程在等待,并不能确切的唤醒一个线程,与JVM确定唤醒那个线程,与其优先级有关。
notityAll():唤醒所有处于等待状态的线程,但是并不是将对象的锁给所有的线程,而是让它们去竞争,谁先获取到锁,谁先进入就绪状态。
4.5 sleep()和wait()有什么区别
两者都可以使线程进入等待状态
类不同:sleep()是Thread下的静态方法,wait()是Object类下的方法
是否释放锁:sleep()不释放锁,wait()释放锁
用处不同:wait()常用于线程间的通信,sleep()常用于暂停执行。
用法不同:wait()用完后,线程不会自动执行,必须调用notify()或notifyAll()方法才能执行,sleep()方法调用后,线程经过过一定时间会自动苏醒,wait(参数)也可以传参数使其苏醒。它们苏醒后还有所区别,因为wait()会释放锁,所以苏醒后没有获取到锁就进入堵塞状态,获取到锁就进入就绪状态,而sleep苏醒后之间进入就绪状态,但是如果cpu不空闲,则进入的是就绪状态的堵塞队列中。
4.6 你是如何调用wait()方法的,使用if还是循环
等待状态的线程可能会收到错误警告或伪唤醒,如果不在循环中检查等待条件,程序可能会在没有满足条件的时候退出。
synchronized (monitor) {
// 判断条件谓词是否得到满足
while(!locked) {
// 等待唤醒
monitor.wait();
}
// 处理其他的业务逻辑
}
1
2
3
4
5
6
7
8
9
4.7 为什么线程通信方法wait(),notify(),notifyAll()要被定义到Object类中
Java中任何对象都可以被当作锁对象,wait(),notify(),notifyAll()方法用于等待获取唤醒对象去获取锁,Java中没有提供任何对象使用的锁,但是任何对象都继承于Object类,所以定义在Object类中最合适。
有人会说,既然是线程放弃对象锁,那也可以把wait()放到Thread类中,新定义线程继承Thread类,也无需重新定义wait(),
然而,这样做有一个很大的问题,因为一个线程可以持有多把锁,你放弃一个线程时,到底要放弃哪把锁,当然了这种设计不能实现,只是管理起来比较麻烦。
综上:wait(),notify(),notifyAll()应该要被定义到Object类中。
4.8 为什么线程通信方法wait(),notify(),notifyAll()要在同步代码块或同步方法中被调用?
wait(),notify(),notifyAll()方法都有一个特点,就是对象去调用它们的时候必须持有锁对象。
如对象调用wait()方法后持有的锁对象就释放出去,等待下一个线程来获取。
如对象调用notifyAll()要唤醒等待中的线程,也要讲自身用于的锁对象释放,让就绪状态中的线程竞争获取锁。
由于这些方法都需要线程持有锁对象,这样只能通过同步来实现,所以它们只能在同步块或同步方法中被调用。
4.9 Thread的yiele方法有什么作用?
让出CPU的使用权,使当前线程从运行状态进入就绪状态,等待CPU的下次调度。
4.10 为什么Thread的sleep和yield是静态的?
Thread类的sleep()和yield()方法将在当前正在运行的线程上工作,所以其它处于等待状态的线程调用它们是没有意义的,所以设置为静态最合适。
4.11 线程sleep和yield方法有什么区别
线程调用sleep()方法进入堵塞状态,醒来后因为(没有释放锁)后直接进入了就绪状态,运行yield后也没有释放锁,于是进入了就绪状态。
sleep()方法使用时需要处理InterruptException异常,而yield没有。
sleep()执行后进入堵塞状态(计时等待),醒来后进入就绪状态(可能是堵塞队列),而yield是直接进入就绪状态。
4.12 如何停止一个正在运行的线程?
使用stop方法终止,但是这个方法已经过期,不被推荐使用。
使用interrupt方法终止线程
run方法执行结束,正常退出
4.13 如何在两个线程间共享数据?
两个线程之间共享变量即可实现共享数据。
一般来说,共享变量要求变量本身是线程安全的,然后在线程中对变量使用。
4.14 同步代码块和同步方法怎么选?
同步块是更好的选择,因为它不会锁着整个对象,当然你也可以然它锁住整个对象。同步方法会锁住整个对象,哪怕这个类中有不关联的同步块,这通常会导致停止继续执行,并等待获取这个对象锁。
同步块扩展性比较好,只需要锁住代码块里面相应的对象即可,可以避免死锁的产生。
原则:同步范围也小越好。
4.15 什么是线程安全?Servlet是线程安全吗?
线程安全是指某个方法在多线程的环境下被调用时,能够正确处理多线程之间的共享变量,能程序能够正确完成。
Servlet不是线程安全的,它是单实例多线程的,当多个线程同时访问一个方法时,不能保证共享变量是安全的。
Struts2是多实例多线程的,线程安全,每个请求过来都会new一个新的action分配这个请求,请求完成后销毁。
springMVC的controller和Servlet一样,属性单实例多线程的,不能保证共享变量是安全的。
Struts2好处是不用考虑线程安全问题,springMVC和Servlet需要考虑。
如果想既可以提升性能又可以不能管理多个对象的话建议使用ThreadLocal来处理多线程。
4.16 线程的构造方法,静态块是被哪个线程类调用的?
线程的构造方法,静态块是被哪个线程类调用的?
该线程在哪个类中被new出来,就是在哪个被哪个类调用,而run方法是线程类自身调用的。
例子:mian函数中new Thread2,Thread2中new Thread1
thread1线程的构造方法,静态块是thread2线程调用的,run方法是thread1调用的。
thread2线程的构造方法,静态块是main线程调用的,run方法是thread2调用的。
4.17 Java中是如何保证多线程安全的?
使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
使用自动锁,synchronized锁
Lock lock = new ReentrantLock(),使用手动锁lock .lock(),lock.unlock()方法
4.18 线程同步和线程互斥的区别
线程同步:当一个线程对共享数据进行操作的时候,在没有完成相关操作时,不允许其它的线程来打断它,否则就会破坏数据的完整性,必然会引起错误信息,这就是线程同步。
线程互斥:
而线程互斥是站在共享资源的角度上看问题,例如某个共享资源规定,在某个时刻只能一个线程来访问我,其它线程只能等待,知道占有的资源者释放该资源,线程互斥可以看作是一种特殊的线程同步。
实现线程同步的方法:
同步代码块:sychronized(对象){} 块
同步方法:sychronized修饰的方法
使用重入锁实现线程同步:reentrantlock类的锁又互斥功能,Lock lock = new ReentrantLock(); Lock对象的ock和unlock为其加锁
4.19 你对线程优先级有什么理解?
每个线程都具有优先级的,一般来说,高优先级的在线程调度时会具有优先被调用权。我们可以自定义线程的优先级,但这并不能保证高优先级又在低优先级前被调用,只是说概率有点大。
线程优先级是1-10,1代表最低,10代表最高。
Java的线程优先级调度会委托操作系统来完成,所以与具体的操作系统优先级也有关,所以如非特别需要,一般不去修改优先级。
4.20 谈谈你对乐观锁和悲观锁的理解?
乐观锁:每个去拿数据的时候都认为别人不会修改,所以不会都不会上锁,但是在更新的时候会判断一下在此期间有没有去更新这个数据。所以乐观锁使用了多读的场合,这样可以提高吞吐量,像数据库提供的类似write_condition机制,都是用的乐观锁,还有那个原子变量类,在java.util.concurrent.atomic包下
悲观锁:总是假设最坏的情况,每次去拿数据的时候都会认为有人会修改,所以每次在拿数据的时候都会上锁。这样别的对象想拿到数据,那就必须堵塞,直到拿到锁。传统的关系型数据库用到了很多这种锁机制,比如读锁,写锁,在操作之前都会先上锁,再比如Java的同步代码块synchronized/方法用的也是悲观锁。
Socket
什么是socket?
Socket的英文原义是“孔”或“插座”。在网络编程中,网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
Socket套接字是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。
Socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。
Socket的原理
Socket实质上提供了进程通信的端点。进程通信之前,双方首先必须各自创建一个端点,否则是没有办法建立联系并相互通信的。正如打电话之前,双方必须各自拥有一台电话机一样。
套接字之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。
1、服务器监听:是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。
2、客户端请求:是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
3、连接确认:是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
TCP和UDP的区别
UDP:
1、每个数据报中都给出了完整的地址信息,因此无需要建立发送方和接收方的连接。
2、UDP传输数据时是有大小限制的,每个被传输的数据报必须限定在64KB之内。
3、UDP是一个不可靠的协议,发送方所发送的数据报并不一定以相同的次序到达接收方
TCP:
1、面向连接的协议,在socket之间进行数据传输之前必然要建立连接,所以在TCP中需要连接时间。
2、TCP传输数据没有大小限制,一旦连接建立起来,双方的socket就可以按统一的格式传输大的数据。
3、TCP是一个可靠的协议,它确保接收方完全正确地获取发送方所发送的全部数据。
集合框架
什么是集合
集合框架:用于存储数据的容器。Contains{接口+接口的实现+数据结构的算法}
接口:表示集合的抽象数据类型。
实现:集合接口的具体实现,是重用性很高的数据结构。
算法:在一个实现了某个集合框架中的接口的对象身上完成某种有用的计算的方法,例如查找、排序等。
容器都有哪些?他们有什么特点?哪些是线程安全的?哪些是线程不安全的?
Collection
1. List
1) ArrayList(底层是数组,查找快)(线程不安全)
2) LinkedList(底层是双向链表, 在指定位置增删元素快)(线程不安全)
3) Vector(用法跟ArrayList差不多, 效率比ArrayList低)(线程安全)
2. Set
1) HashSet
2) TreeSet
Map
1. HashMap
2. HshTable
3. ConcrruentHashMap(线程安全, 不涉及同步加锁)
集合和数组的区别
-
数组是固定长度的;集合可变长度的。
-
数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
-
数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。
使用集合框架的好处
- 容量自增长;
- 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量;
- 允许不同 API 之间的互操作,API之间可以来回传递集合;
- 可以方便地扩展或改写集合,提高代码复用性和可操作性。
- 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。
常用的集合类有哪些?
Map接口和Collection接口是所有集合框架的父接口:
Collection接口的子接口包括:Set接口和List接口
Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
List接口的实现类主要有:ArrayList、LinkedList、以及Vector等
Collection 和 Collections 有什么区别?
Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如 List、Set 等。
Collections 是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供的排序方法: Collections. sort(list)。
List 和 Set 的区别
List 特点:一个有序容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
Set 特点:一个无序容器,不可以存储重复元素,必须保证元素唯一性 , 只允许存入一个null元素,。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。
另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。
如何实现数组和 List 之间的转换?
- 数组转 List:使用 Arrays. asList(array) 进行转换。
- List 转数组:使用 List 自带的 toArray() 方法。
ArrayList 和 LinkedList 的区别是什么?
ArrayList 底层是数组 , 而 LinkedList 底层是双向链表。
ArrayList 查询快, LinkedList 增删快
LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个地址引用,一个指向前一个元素,一个指向后一个元素。
ArrayList 和 Vector 的区别是什么?
线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
性能:ArrayList 在性能方面要优于 Vector。
扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 是原数组2倍的长度加2
HashMap 和 Hashtable 有什么区别?
存储:HashMap 运行 key 和 value 为 null,而 Hashtable 不允许。
线程安全:Hashtable 是线程安全的,而 HashMap 是非线程安全的。
推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。
HashSet底层封装的是什么?
封装的是HashMap
关于HashMap底层的一些问题
- HashMap的底层存储结构是怎样的啊?
- 线程安全吗?为什么不安全?
- 1.7和1.8版本的HashMap有什么区别?1.7的有什么隐患,什么原因导致的?
- hashcode是唯一的吗?插入元素的时候怎么比较的?
- 跟HashTable,ConcurrentHashMap有什么区别?
- 为什么HashMap链表树化的标准是8个?
- HashMap的基本知识点
HashMap可以说是Java项目里最常用的集合类了,作为一种典型的K-V存储的数据结构,它的底层是由数组 - 链表组成,当添加新元素时,它会根据元素的hash值找到对应的"桶",也就是HashMap源码中Node<K, V> 里的元素,并插入到对应位置的链表中,链表元素个数过长时会转化为红黑树(JDK1.8后的版本),
为什么要转成红黑树呢?
我们都知道,链表取元素是从头结点一直遍历到对应的结点,这个过程的复杂度是O(N) ,而红黑树基于二叉树的结构,查找元素的复杂度为O(logN) ,所以,当元素个数过多时,用红黑树存储可以提高搜索的效率。
既然红黑树的效率高,那怎么不一开始就用红黑树存储呢?JDK的源码里已经对这个问题做了解释:
Because TreeNodes are about twice the size of regular nodes, we
use them only when bins contain enough nodes to warrant use
(see TREEIFY_THRESHOLD). And when they become too small (due to
removal or resizing) they are converted back to plain bins.
看注释里的前面四行就不难理解,单个 TreeNode 需要占用的空间大约是普通 Node 的两倍,所以只有当包含足够多的 Nodes 时才会转成 TreeNodes,这个足够多的标准就是由 TREEIFY_THRESHOLD 的值(默认值8)决定的。而当桶中节点数由于移除或者 resize (扩容) 变少后,红黑树会转变为普通的链表
UNTREEIFY_THRESHOLD(默认值6)。
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
看到这里就不难明白了,红黑树虽然查询效率比链表高,但是结点占用的空间大,只有达到一定的数目才有树化的意义,这是基于时间和空间的平衡考虑。
为什么树化标准是8个
至于为什么树化标准的数量是8个,在源码中,上面那段笔记后面还有一段较长的注释,我们可以从那一段注释中找到答案,原文是这样:
* usages with well-distributed user hashCodes, tree bins are
* rarely used. Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million
大概意思就是:如果 hashCode的分布离散良好的话,那么红黑树是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,注释中给我们展示了1-8长度的具体命中概率,当长度为8的时候,概率概率仅为0.00000006(亿分之六),这么小的概率,HashMap的红黑树转换几乎不会发生,因为我们日常使用不会存储那么多的数据,你会存上千万个数据到HashMap中吗?
当然,这是理想的算法,但不妨某些用户使用HashMap过程导致hashCode分布离散很差的场景,这个时候再转换为红黑树就是一种很好的退让策略。
首先说明一下,在HashMap中,决定某个对象落在哪一个 “桶“,是由该对象的hashCode决定的,JDK无法阻止用户实现自己的哈希算法,如果用户重写了hashCode,并且算法实现比较差的话,就很可能会使HashMap的链表变得很长,就比如这样:
public class HashMapTest {
public static void main(String[] args) {
Map<User, Integer> map = new HashMap<>();
for (int i = 0; i < 1000; i++) {
map.put(new User("闯哥最帅" + i), i);
}
}
static class User{
private String name;
public User(String name) {
this.name = name;
}
@Override
public int hashCode() {
return 1;
}
}
}
我们设计了一个hashCode永远为1的类User,这样一来存储到HashMap的所有User对象都会存放到同一个“桶”里,查询效率无疑会非常的低下,而这也是HashMap设计链表转红黑树的原因之一,可以有效防止用户自己实现了不好的哈希算法时导致链表过长的情况。
hash方法
说到哈希算法,我们再来扩充一个知识点,这也是我觉得HashMap中非常牛逼的设计之一。
在HashMap的源码中,存储对象hashCode的计算是由hash() 方法决定的,hash() 是HashMap 中的核心函数,在存储数据时,将key传入中进行运算,得出key的哈希值,通过这个哈希值运算才能获取key应该放置在 “桶” 的哪个位置,下面是方法的源码:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
从代码中可以看出,传入key之后,hash() 会获取key的hashCode进行无符号右移 16 位,然后进行按位异或,并把运算后的值返回,这个值就是key的哈希值。这样运算是为了减少碰撞冲突,因为大部分元素的hashCode在低位是相同的,不做处理的话很容易造成冲突。
除了做16位位移的处理,在添加元素的方法中,HashMap还把该hash值与table.length - 1,也就是“桶”数组的大小做与运算,得到的结果就是对应的“桶”数组的下标,从而找到该元素所属的链表。源码里这样的:
// n的值是table.length
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
当查找不到对应的索引时,就会新建一个新的结点作为链表的头结点。那么这里为什么要用 i = (n - 1) & hash 作为索引运算呢?
这其实是一种优化手段,由于数组的大小永远是一个2次幂,在扩容之后,一个元素的新索引要么是在原位置,要么就是在原位置加上扩容前的容量。这个方法的巧妙之处全在于&运算,之前提到过&运算只会关注n – 1(n =数组长度)的有效位,当扩容之后,n的有效位相比之前会多增加一位(n会变成之前的二倍,所以确保数组长度永远是2次幂很重要),然后只需要判断hash在新增的有效位的位置是0还是1就可以算出新的索引位置,如果是0,那么索引没有发生变化,如果是1,索引就为原索引加上扩容前的容量。
用一张效果图来表示就是:
通过位运算,在每次扩容时都不用重新计算hash,省去了不少时间,而且新增有效位是0还是1是带有随机性的,之前两个碰撞的Entry又有可能在扩容时再次均匀地散布开,达到较好的分布离散效果,不得不感叹,设计者的功底真是太牛逼了,几句看似简单的代码里面居然包含了这么多的学问。
为什么退化为链表的阈值是6
上面说到,当链表长度达到阈值8的时候会转为红黑树,但是红黑树退化为链表的阈值却是6,为什么不是小于8就退化呢?比如说7的时候就退化,偏偏要小于或等于6?
主要是一个过渡,避免链表和红黑树之间频繁的转换。如果阈值是7的话,删除一个元素红黑树就必须退化为链表,增加一个元素就必须树化,来回不断的转换结构无疑会降低性能,所以阈值才不设置的那么临界。
反射
反射是框架的灵魂
(使用的前提条件: 必须先得到代表的字节码的Class, Class类用于表示.class文件(字节码))
一、反射的概述
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法.所以先要获取到每一个字节码文件对应的Class类型的对象.
以上的总结就是什么是反射
反射就是把java类中的各种成分映射成一个个的Java对象
例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把个个组成部分映射成一个个对象。
(其实:一个类中这些成员方法、构造方法、在加入类中都有一个类来描述)
如图是类的正常加载过程:反射的原理在与class对象。
熟悉一下加载的时候:Class对象的由来是将class文件读入内存,并为之创建一个Class对象。
其中这个Class对象很特殊。我们先了解一下这个C lass类:
Class 类的实例表示正在运行的 Java 应用程序中的类和接口。也就是jvm中有N多的实例每个类都有该Class对象。(包括基本数据类型)
Class 没有公共构造方法。Class 对象是在加载类时由 Java 虚拟机以及通过调用类加载器中的defineClass 方法自动构造的。也就是这不需要我们自己去处理创建,JVM已经帮我们创建好了。没有公共的构造方法.
反射的使用
先写一个Student类。
1、获取Class对象的三种方式
1.1 Object ——> getClass();
1.2 任何数据类型(包括基本数据类型)都有一个“静态”的class属性
1.3 通过Class类的静态方法:forName(String className)(常用)
其中1.1是因为Object类中的getClass方法、因为所有类都继承Object类。从而调用Object类来获取
数据库
常见的关系型数据库
oracle Oracle(甲骨文) 收费, 支持各种操作系统,NBA金州勇士(甲骨文球场)
mysql Oracle(甲骨文) 开源, 支持各种操作系统
mariadb
sql server 微软 .NET 收费, 中型数据库, 只能用windows系统
DB2 IBM 收费, 银行业务用的比较多
Sqlite 迷你型数据库, 嵌入式设备(安卓, 苹果, pad)
概念: 简称DB, 按照数据结构组织, 存储和管理数据库的仓库
SQL中的日期和时间类型
data 日历日期(年月日)
time 时分秒
timestamp 年月日时分秒
Datatime 年月日时分秒
varchar和char的区别
char是固定长度的类型 varchar是可变长度的类型 char的效率要比varchar高
关联查询: 内连接,外连接,自连接
(1) 外连接之笛卡尔积查询 (2)什么时候会发生笛卡尔积查询?
(1)就是两个集合数据条数相乘的结果 (2)关联查询没添加关联关系
分组: group by(单独使用, 聚合函数, HAVING,
HAVING关键字和WHERE关键字的作用相同,都是用于设置条件表达式,对查询结果进行过滤。
两者的区别,HAVING关键字后,可以跟聚合函数,而WHERE关键字不能,通常情况下,HAVING关键字,都是和GROUP BY一起使用,用于对分组后的结果进行过滤
)
数据库聚合函数: (MAX(最大值), min(最小值), sum(和), avg(平均数), count(统计个数))等
分页查询: mysql: limit 0(跳过的数据) 5(显示的数据)
oracle: ROWNUM <= 40和RN >= 21控制分页查询的每页的范围。
索引: 建立索引对于mysql高效运行时很重要, 索引可以提高mysql的检索速度, 创建索引时, 需要确保该索引是应用在SQL查询语句的条件(一般为where条件) 创建索引的字段我们建议是唯一, 不为空, 经常被查询的字段 分普通索引和唯一索引(unique)
索引索然会大大提高了查询速度,同时却会降低更新表的速度,如对表进行Insert,Update,Delete.因
为更新表时,Mysql不仅要保存数据,还要保存一下索引文件.
优点:可以大大加快数据的检索速度,这也是创建索引的最主要的原因.
缺点:1)增加了数据库的存储空间 2)减慢了数据录入的速度
约束:
主键约束(Primay Key Coustraint) 唯一性,非空性(唯一且非空)
唯一约束(Unique Constraint) 唯一性(可以为空,但是只能有一个)
检查约束(Check Counstraint) 对该列格式和范围的限制,自定义约束,自己决定限制条件
外键约束(Foreign Key Counstraint) 需要建立两表间的关系
默认约束(Default Coustraint) 该数据的默认值
非空约束(Not Null Counstraint) 该字段不能为空
有的说数据的5大约束,有的是6大约束
事务: 数据库的事务是指一组sql语句组成的数据库逻辑处理单元,在这组的sql操作中,要么全部执行成功,要么全部执行失败。
事务的ACID: 在Mysql中事务的四大特性主要包含:
原子性(Atomicity)原子性是指事务的原子性操作,对数据的修改要么全部执行成功,要么全部失败,实现事务的原子性
一致性(Consistent)一致性是指执行事务前后的状态要一致,可以理解为数据一致性。
隔离性(Isalotion)隔离性侧重指事务之间相互隔离,不受影响,这个与事务设置的隔离级别有密切的关系。
持久性(Durable)持久性则是指在一个事务提交后,这个事务的状态会被持久化到数据库中,也就是事务提交,对数据的新增、更新将会持久化到书库中。
在我的理解中,原子性、隔离性、持久性都是为了保障一致性而存在的,一致性也是最终的目的。
简称为ACID
。
事务的隔离级别:
在Mysql中事务的隔离级别分为四大等级
读未提交(READ UNCOMMITTED)读未提交会读到另一个事务的未提交的数据,产生脏读问题
读已提交 (READ COMMITTED)读已提交则解决了脏读的,出现了不可重复读,即在一个事务任意时刻读到的数据可能不一样,可能会受到其它事务对数据修改提交后的影响,一般是对于update的操作。
可重复读 (REPEATABLE READ)可重复读解决了之前不可重复读和脏读的问题,但是由带来了幻读的问题,幻读一般是针对inser操作。例如:第一个事务查询一个User表id=100发现不存在该数据行,这时第二个事务又进来了,新增了一条id=100的数据行并且提交了事务。 这时第一个事务新增一条id=100的数据行会报主键冲突,第一个事务再select一下,发现id=100数据行已经存在,这就是幻读。
串行化 (SERIALIZABLE)。可串行化是一个调度,即多个事务之间的执行方式;而多个事务之间的执行有个先后顺序,如果事务之间没有共同的操作对象(读或写操作),则事务之间的执行顺序前后置换是没有关系的;但是如果事物间存在共同的操作对象,则事务间先后执行的顺序则需要区分;对于存在共同操作对象的多个并发执行的事务,如果其执行结果“等价”于某个“串行化调度”,则这个调度才是“可串行化的调度”。满足“可串行化的调度”则具有了可串行化(Serializability)属性。所以,可串行化(Serializability)属性保证的是多个事务并发时的执行顺序要对数据的一致性没有影响。
悲观锁与乐观锁:
悲观锁(Pessimistic Locking):
悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自 外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。
悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能 真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系 统不会修改数据)。
通俗理解:一段数据在被A以悲观锁的形式访问(select * from card_record where status ="03' for update),那么在这个访问没有commit或者事务结束之前,这些 status ="03'的数据是无法被修改的。这条 sql 语句锁定了 card_record 表中所有符合检索条件( status ="03' )的记录。本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。 需要注意的是for update要放到mysql的事务中,即begin和commit中,否则不起作用。
悲观锁有两种:读锁和写锁。上面的操作属于写锁的操作,A上了锁,那么别人就不可以再上锁了,有锁的人可以对数据进行读写操作。而读锁相当于共享锁,开启事务后,A上了共享锁,那么其他人也可以上共享锁。大家都可以读这些数据,但是都不能修改。
悲观的理解,别人在我读写数据的时候会进行更改,那么我就上锁,在我读写数据的时候不允许别人更改我正在操作的数据。
乐观锁(Optimistic Locking):
由于悲观锁在频繁依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销。而乐观锁机制避免了长事务中的数据库加锁开销,大大提升了大并发量下的系统整体性能表现。
乐观锁,大多是基于数据版本(Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来 实现。 读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据 版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
通俗的讲,A和B都在对一个版本为1的数据进行修改,在事务提交之前,这段数据的版本一直是1,那么下次提交的人会把这段数据的版本变为2。如果A在B之前提交了,那么这段数据的版本变为2,之后B提交了数据,由于B拿到数据的时候版本是1,那么加一后,预估版本也是2,但是提交完发现,现在的版本已经是2了,所以B的修改和提交是无效的。
需要注意的是,乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户对数据的操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在 系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途 径,而不是将数据库表直接对外公开)。
乐观的理解,在我读写数据的时候,没人对这段数据进行更改,所以可以随意更改数据,只是commit的时候可能会出现版本问题。
悲观锁,前提是,一定会有并发抢占资源,强行独占资源,在整个数据处理过程中,将数据处于锁定状态。
乐观锁,前提是,不会发生并发抢占资源,只有在提交操作的时候检查是否违反数据完整性。只能防止脏读后数据的提交,不能解决脏读。
视图:
试图是从一个或几个基本表(或视图)导出来的表。它与基本表不同,是一个虚表。数据库中只存放视图的定义,而不存放视图对应的数据,这些数据仍存放在原来的基本表中。所以一旦基本表中的数据发生变化,从视图中查询出来的数据也就随之改变了。从这个意义上说,视图就像是一个窗口,透过它可以看到数据库中自己感兴趣的数据及其变化。
视图一经定义,就可以和基本表一样被查询、删除,也可以在一个视图上再定义新的视图,但对视图的更新(增、删、改)操作则有一定的限制。
视图的作用:
1、视图机制使用户可以将注意力集中放在所关心的数据上。如果这些数据不是直接来自基本表,则可以通过定义视图使数据库看起来结构简单清晰,并且可以简化用户的数据查询操作。例如,那些定义了若干张表连接的视图就将表与表之间的连接操作对用户隐蔽起来,也就是说,用户所做的只是对一个虚表的简单查询,而这个虚表是怎么得来的,用户无需了解。
2、视图使用户能以多种角度看待同一数据。视图机制能使不同的用户以不同的方式看待同一数据,当许多不同种类的用户共享同一个数据库时,这种灵活性是很重要的。
3、视图对重构数据库提供了一定程度的逻辑独立性。具体就不解释了,想了解的去看书或者查资料。
4、视图能够对机密数据提供安全保护。有了视图机制,就可以在设计数据库应用系统时对不同的用户定义不同的视图,使机密数据不出现在不应该看到这些数据的用户视图上。这样视图机制就自动提供了对机密数据的安全保护功能。
CAS算法: 不写了暂时用不到, 再跳槽的的时候弄会就行
Spring Framework
官方网址(快速入门): https://spring.io/quickstart
特征:
-
核心技术:Ioc、依赖注入、数据绑定、类型转换、AOP等。
-
数据访问:事务、DAO 支持、ORM、Marshalling XML等。
-
集成:远程处理、JMS、JCA、JMX、电子邮件、任务、调度、缓存。
Spring是一个分层的轻量级java开发框架,为企业级应用开发提供了全面的编程和配置模型。它为Java开发者提供了近乎于全面的基础架构支持,使得我们只需关注应用的业务逻辑而无需去理会那些与业务逻辑无关的繁琐、重复的编程工作。
Spring的众多功能特性大都依赖于他的两个核心功能模块:依赖注入( dependency Injection )简称DI和面向切面编程(AOP)。
1.1、Spring的优缺点
优点:
1、使用Spring的IOC容器,将对象之间的依赖关系交由Spring管理,降低了组件之间的耦合性,实现了软件各层的解耦。
2、使用容器提供众多服务,如事务管理、消息队列等。
3、AOP技术,利用它可以实现一些拦截,如权限拦截。
4、对主流的框架提供了很好的支持,如Mybatis。
5、低浸入式设计
6、Spring的DI机制降低了业务对象替换的复杂性。
缺点:
Spring依赖反射机制,反射机制占用内存影响性能。
1.2、Spring中有哪些功能模块
1、Spring Core
框架的最基础部分,提供IoC容器,对bean进行管理
2、Spring Context
基于Bean,提供上下文信息,扩展出JNDI、EJB、电子邮件、国际化、校验和调度等功能。
3、Spring Dao
提供了JDBC的抽象层,它可消除冗长的JDBC编码和解析数据库厂商特有的代码错误,还提供了声明式事务管理方法。
4、Spring ORM
提供了常用的对象-关系映射APIs的集成,其中包括Hibernate、Mybatis等。
5、Spring AOP
提供了符合AOP Alliance规范的面向切面的编程实现。
6、Spring Web
提供了基础的Web开发的上下文信息
7、Spring Web MVC
提供了web应用的Model-View-Controller功能实现。
1.3、Spring中用到的设计模式
1、工厂模式
Spring通过BeanFactory或者ApplicationContext创建Bean对象用到了工厂模式。
一.什么是工厂模式
工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。
这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。利用工厂模式,我们可以在创建对象时不对客户端暴露创建逻辑,而是通过使用一个共同的接口来创建新的对象。使“类实例化的操作”与“使用对象的操作”分开。
二.工厂模式具体可以分为3种:
简单工厂模式
实例化对象的时候不再使用 new Object()形式,可以根据用户的选择条件来实例化相关的类。对于客户端来说,去除了具体的类的依赖。只需要给出具体实例的描述给工厂,工厂就会自动返回具体的实例对象。
具体实现如下:
1. 定义一个操作接口:
public interface Operation {
public double getResult(double numberA,double numberB) throws Exception;
}
2. 定义具体的操作类:
public class Add implements Operation{
// 加法计算
public double getResult(double numberA, double numberB) {
return numberA + numberB;
}
}
public class Sub implements Operation{
// 减法计算
public double getResult(double numberA, double numberB) {
return numberA-numberB;
}
}
public class Mul implements Operation{
// 乘法计算
public double getResult(double numberA, double numberB) {
return numberA * numberB;
}
}
public class Div implements Operation {
// 除法计算
public double getResult(double numberA, double numberB) throws Exception {
if (numberB == 0) {
throw new Exception("除数不能为0!");
}
return numberA / numberB;
}
}
3. 定义简单工厂类:
public class EasyFactory {
// 简单工厂,根据字符串创建相应的对象
public static Operation createOperation(String name) {
Operation operationObj = null;
switch (name) {
case "+":
operationObj = new Add();
break;
case "-":
operationObj = new Sub();
break;
case "*":
operationObj = new Mul();
break;
case "/":
operationObj = new Div();
break;
}
return operationObj;
}
}
4. 用户端代码:
public class Client {
public static void main(String[] args) throws Exception {
Operation add = EasyFactory.createOperation("+");
Operation sub = EasyFactory.createOperation("-");
Operation mul = EasyFactory.createOperation("*");
Operation div = EasyFactory.createOperation("/");
System.out.println(add.getResult(1, 1));
System.out.println(sub.getResult(1, 1));
System.out.println(mul.getResult(1, 1));
System.out.println(div.getResult(1, 1));
}
}
Result:
2.0
0.0
1.0
1.0
我们无需提供具体的子类类名,只需要提供一个字符串即可得到相应的实例对象。这样的话,当子类的类名更换或者增加子类时我们都无需修改客户端代码,只需要在简单工厂类上增加一个分支判断代码即可。
使用这种模式,我们在生成工厂的时候可以加一些业务代码,如日志、判断业务等,这时候可以直接在switch case中加上去就行了,如下:
public class EasyFactory {
private static Operation operationObj = null;
private static Operation add(){
System.out.println("加法运算");
return new Add();
}
private static Operation sub(){
System.out.println("减法运算");
return new Sub();
}
private static Operation mul(){
System.out.println("乘法运算");
return new Mul();
}
private static Operation div(){
System.out.println("除法运算");
return new Div();
}
// 简单工厂,根据字符串创建相应的对象
public static Operation createOperation(String name) {
switch (name) {
case "+":
operationObj = add();
break;
case "-":
operationObj = sub();
break;
case "*":
operationObj = mul();
break;
case "/":
operationObj = div();
break;
}
return operationObj;
}
}
这样做的优点:我们 可以对创建的对象进行一些 “加工” ,而且客户端并不知道,因为工厂隐藏了这些细节。如果,没有工厂的话,那我们是不是就得自己在客户端上写这些代码,这就好比本来可以在工厂里生产的东西,拿来自己手工制作,不仅麻烦以后还不好维护。
但是缺点也很明显:如果需要在方法里写很多与对象创建有关的业务代码,而且需要的创建的对象还不少的话,我们要在这个简单工厂类里编写很多个方法,每个方法里都得写很多相应的业务代码,而每次增加子类或者删除子类对象的创建都需要打开这简单工厂类来进行修改。这会导致这个简单工厂类很庞大臃肿、耦合性高,而且增加、删除某个子类对象的创建都需要打开简单工厂类来进行修改代码也违反了开-闭原则。
工厂模式
这时候就需要使用工厂模式了。工厂方法模式是对简单工厂模式进一步的解耦,因为在工厂方法模式中是一个子类对应一个工厂类,而这些工厂类都实现于一个抽象接口。这相当于是把原本会因为业务代码而庞大的简单工厂类,拆分成了一个个的工厂类,这样代码就不会都耦合在同一个类里了。
1. 首先定义一个工厂接口:
import org.zero01.operation.Operation;
public interface Factory {
public Operation createOperation() ;
}
2. 然后是具体的工厂类:
// 加法类工厂
public class AddFactory implements Factory{
public Operation createOperation() {
System.out.println("加法运算");
return new Add();
}
}
// 减法类工厂
public class SubFactory implements Factory{
public Operation createOperation() {
System.out.println("减法运算");
return new Sub();
}
}
........
3. 运算类跟简单工厂一样。
4. 客户端代码:
public class Client {
public static void main(String[] args) throws Exception {
// 使用反射机制实例化工厂对象,因为字符串是可以通过变量改变的
Factory addFactory = (Factory) Class.forName("org.zero01.factory.AddFactory").newInstance();
Factory subFactory=(Factory) Class.forName("org.zero01.factory.SubFactory").newInstance();
// 通过工厂对象创建相应的实例对象
Operation add = addFactory.createOperation();
Operation sub = subFactory.createOperation();
System.out.println(add.getResult(1, 1));
System.out.println(sub.getResult(1, 1));
}
}
比较:
工厂模式中,要增加产品类时也要相应地增加工厂类,客户端的代码也增加了不少。工厂方法把简单工厂的内部逻辑判断转移到了客户端代码来进行。
你想要加功能,本来是改工厂类的,而现在是修改客户端。而且各个不同功能的实例对象的创建代码,也没有耦合在同一个工厂类里,这也是工厂方法模式对简单工厂模式解耦的一个体现。工厂方法模式克服了简单工厂会违背开-闭原则的缺点,又保持了封装对象创建过程的优点。
但工厂方法模式的缺点是每增加一个产品类,就需要增加一个对应的工厂类,增加了额外的开发量。
抽象工厂模式
场景:对数据库中的表进行修改
结构图:
1. 我们现在要对mysql/oracle数据库中的User表进行操作,User表定义如下:
public class User {
private int uid;
private String uname;
public int getUid() {
return uid;
}
public void setUid(int uid) {
this.uid = uid;
}
public String getUname() {
return uname;
}
public void setUname(String uname) {
this.uname = uname;
}
}
2. 接下来我们定义一个对User进行操作的接口:
public interface IUser {
public void insert(User user);
public User getUser(int uid);
}
3. 实现一个对mysql中User进行操作的类:
public class mysqlUser implements IUser{
public void insert(User user){
System.out.println("在mysql中的user表中插入一条元素");
}
public User getUser(int id){
System.out.println("在mysql中的user表得到id为"+id+"的一条数据");
return null;
}
}
实现对oracle中User进行操作的类:
public class oracleUser implements IUser{
@Override
public void insert(User user) {
System.out.println("在oracle中的user表中插入一条元素");
}
@Override
public User getUser(int uid) {
System.out.println("在oracle中的user表得到id为"+uid+"的一条数据");
return null;
}
}
4. 接下来定义一个工厂接口,用于生产访问User表的对象:
public interface sqlFactory {
public IUser createUser(); //用于访问User表的对象
}
5. 生产mysqlUser对象的mysql工厂类:
public class mysqlFactory implements sqlFactory {
@Override
public IUser createUser() {
return new mysqlUser(); //访问mysql中User表的对象
}
}
生成oracleUser对象的oracle工厂类:
public class oracleFactory implements sqlFactory {
@Override
public IUser createUser() {
return new oracleUser(); //访问oracle中User表的对象
}
}
6. 最后用户测试类如下:
public class test_abstractFactory {
public static void main(String[] args) {
sqlFactory factory1 = new mysqlFactory();
IUser userOperator = factory1.createUser();
userOperator.getUser(1);
userOperator.insert(new User());
}
}
结果为:
在mysql中的user表得到id为1的一条数据
在mysql中的user表中插入一条元素
到此为止,工厂模式都可以很好的解决,由于多态的关系,sqlFactory在声明对象之前都不知道在访问哪个数据库,却可以在运行时很好的完成任务,这就是业务逻辑与数据访问的解耦。
但是,当数据库中不止一个表的时候该怎么解决问题呢,此时就可以引入抽象工厂模式了,结构图如下:
1. 比如说现在增加了一个Login类,用于记录登陆信息:
package DesignPattern.abstractFactory;
import java.util.Date;
public class Login {
private int id;
private Date date;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
}
2. 此时就要相应地添加 对login表操作的Ilogin接口,mysqlLogin类,oracleLogin类:
public interface ILogin {
public void insert(Login login);
public Login getLogin(int id);
}
public class MysqlLogin implements ILogin{
public void insert(Login login) {
System.out.println("对 MySQL 里的 Login 表插入了一条数据");
}
public Login getLogin(int id) {
System.out.println("通过 uid 在 MySQL 里的 Login 表得到了一条数据");
return null;
}
}
public class OracleLogin implements ILogin{
public void insert(Login login) {
System.out.println("对 Oracle 里的 Login 表插入了一条数据");
}
public Login getLogin(int id) {
System.out.println("通过 uid 在 Oracle 里的 Login 表得到了一条数据");
return null;
}
}
3. 修改Factory接口及Factory实现类的内容:
IFactory,定义一个抽象的工厂接口,该工厂用于生产访问User表以及Login表的对象:
public interface IFactory {
public IUser createUser();
public ILogin createLogin();
}
public class MysqlFactory implements IFactory{
public IUser createUser() {
return new MysqlUser();
}
public ILogin createLogin() {
return new MysqlLogin();
}
}
public class OracleFactory implements IFactory{
public IUser createUser() {
return new OracleUser();
}
public ILogin createLogin() {
return new OracleLogin();
}
}
4. 客户端代码:
public class Client {
public static void main(String[] args){
User user=new User();
Login login = new Login();
// 只需要确定实例化哪一个数据库访问对象给factory
// IFactory factory=new MysqlFactory();
IFactory factory=new OracleFactory();
// 已与具体的数据库访问解除了耦合
IUser userOperation=factory.createUser();
userOperation.getUser(1);
userOperation.insert(user);
// 已与具体的数据库访问解除了耦合
ILogin loginOperation=factory.createLogin();
loginOperation.insert(login);
loginOperation.getLogin(1);
}
}
结果:
通过 uid 在 Oracle 里的 User 表得到了一条数据
对 Oracle 里的 User 表插入了一条数据
对 Oracle 里的 Login 表插入了一条数据
通过 uid 在 Oracle 里的 Login 表得到了一条数据
所以抽象工厂与工厂方法模式的区别在于:抽象工厂是可以生产多个产品的,例如 MysqlFactory 里可以生产 MysqlUser 以及 MysqlLogin 两个产品,而这两个产品又是属于一个系列的,因为它们都是属于MySQL数据库的表。而工厂方法模式则只能生产一个产品,例如之前的 MysqlFactory 里就只可以生产一个 MysqlUser 产品
抽象工厂模式的优缺点:
优点:
1. 抽象工厂模式最大的好处是易于交换产品系列,由于具体工厂类,例如 IFactory factory=new OracleFactory(); 在一个应用中只需要在初始化的时候出现一次,这就使得改变一个应用的具体工厂变得非常容易,它只需要改变具体工厂即可使用不同的产品配置。不管是任何人的设计都无法去完全防止需求的更改,或者项目的维护,那么我们的理想便是让改动变得最小、最容易,例如我现在要更改以上代码的数据库访问时,只需要更改具体的工厂即可。
2. 抽象工厂模式的另一个好处就是它让具体的创建实例过程与客户端分离,客户端是通过它们的抽象接口操作实例,产品实现类的具体类名也被具体的工厂实现类分离,不会出现在客户端代码中。就像我们上面的例子,客户端只认识IUser和ILogin,至于它是MySQl里的表还是Oracle里的表就不知道了。
缺点:
1. 如果你的需求来自增加功能,比如增加Login表,就有点太烦了。首先需要增加 ILogin,mysqlLogin,oracleLogin。 然后我们还要去修改工厂类: sqlFactory, mysqlFactory, oracleFactory 才可以实现,需要修改三个类,实在是有点麻烦。
2. 还有就是,客户端程序肯定不止一个,每次都需要声明sqlFactory factory=new MysqlFactory(), 如果有100个调用数据库的类,就需要更改100次sqlFactory factory=new oracleFactory()。
抽象工厂的改进(反射+配置文件+简单工厂) 有三种只重点说这一种(灵活 , 符合开-闭原则)
原始EasyFacrory
public class EasyFactory {
// 数据库名称
private static String db="MySQL";
// private static String db="Oracle";
public static IUser createUser(){
IUser user=null;
switch (db){
case "MySQL":
user=new MysqlUser();
break;
case "Oracle":
user=new OracleUser();
break;
}
return user;
}
public static ILogin createLogin(){
ILogin login=null;
switch (db){
case "MySQL":
login=new MysqlLogin();
break;
case "Oracle":
login=new OracleLogin();
break;
}
return login;
}
}
客户端
public class Client {
public static void main(String[] args){
User user=new User();
Login login = new Login();
// 直接得到实际的数据库访问实例,而不存在任何依赖
IUser userOperation= EasyFactory.createUser();
userOperation.getUser(1);
userOperation.insert(user);
// 直接得到实际的数据库访问实例,而不存在任何依赖
ILogin loginOperation=EasyFactory.createLogin();
loginOperation.insert(login);
loginOperation.getLogin(1);
}
}
使用反射的话,我们就可以不需要使用switch,因为使用switch的话,我添加一个sql server数据库的话,又要switch的话又需要添加case条件。
改进后EasyFactory
EasyFactory类,简单工厂:
public class easyFactory1 {
private static String packName = "DesignPattern.abstractFactory";
private static String sqlName = "mysql";
public static IUser createUser() throws Exception{
String className = packName+"."+sqlName+"User";
return (IUser)Class.forName(className).newInstance();
}
public static ILogin createLogin() throws Exception{
String className = packName+"."+sqlName+"Login";
return (ILogin)Class.forName(className).newInstance();
}
}
以上我们使用简单工厂模式设计的代码中,是用一个字符串类型的db变量来存储数据库名称的,所以变量的值到底是 MySQL 还是 Oracle ,完全可以由事先设置的那个db变量来决定,而我们又可以通过反射来去获取实例,这样就可以去除switch语句了。
在使用反射之后,我们还是需要进easyFactory中修改数据库类型,还不是完全符合开-闭原则。
我们可以通过配置文件来达到目的,每次通过读取配置文件来知道我们应该使用哪种数据库。
如下是一个json类型的配置文件,也可以使用xml类型的配置文件:
{
"packName": "DesignPattern.abstractFactory",
"DB": "Mysql"
}
之后就可以通过这个配置文件去找需要加载的类是哪一个。
我们通过反射机制+配置文件+简单工厂模式解决了数据库访问时的可维护、可扩展的问题。
三.为什么要用工厂模式
1. 将对象的使用的对象的创建分离开来
2. 如果创建一个类的步骤很复杂,很多地方都用得到这个类,我们可以使用工厂模式做统一创建。3. 数据库访问时的可维护、可扩展的问题。
四.工厂模式的优点
增加了代码的重用性
增加了代码的可维护性
五.工厂模式的缺点
使用了工厂模式,就会引入工厂类,会增加系统的复杂度
2、单例模式
Spring中bean的作用域默认为单例模式(singleton)。
3、代理模式
Spring AOP(面向切面编程)基于动态代理,如果要代理的对象实现了某个接口,SpringAOP会使用JDK Proxy创建代理对象;如果对象没有实现接口,Spring AOP会使用CGLib生成一个被代理对象的子类作为代理。
4、模板方法模式
Spring中的jdbcTemplate等以Template结尾的对数据库操作的类就使用了模板模式。
5、适配器模式
Spring AOP的实现基于代理模式,但是SpringAOP的增强(Advice)使用了适配器模式。SpringMVC的HandlerAdapter也是适配器模式。
6、观察者模式
定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,Spring事件驱动模型就是观察者模式一个很经典的应用。
1.4、Spring中的事件有哪几种?
1、上下文更新事件(ContextRefreshedEvent):在调用ConfigurableApplicationContext 接口中的refresh()方法时被触发。
2、上下文开始事件(ContextStartedEvent):当容器调用ConfigurableApplicationContext的Start()方法开始/重新开始容器时触发该事件。
3、上下文停止事件(ContextStoppedEvent):当容器调用ConfigurableApplicationContext的Stop()方法停止容器时触发该事件。
4、上下文关闭事件(ContextClosedEvent):当ApplicationContext被关闭时触发该事件。容器被关闭时,其管理的所有单例Bean都被销毁。
5、请求处理事件(RequestHandledEvent):在Web应用中,当一个http请求(request)结束触发该事件。如果一个bean实现了ApplicationListener接口,当一个ApplicationEvent 被发布以后,bean会自动被通知。
2、什么是IoC?
IOC(Inverse of Control)即控制反转——某一接口具体实现类的选择控制权从调用类中移除,转交由第三方Spring容器借由bean配置来进行控制。SPring IoC负责创建对象、管理对象、装配对象、配置对象,并且管理对象的整个生命周期。
(注:依赖注入(DI)其实指的就是IoC,其解释为:让调用类对某一接口实现类的依赖关系由第三方(Spring 容器)注入,以移除调用类对某一接口实现类的依赖。)
2.1、IoC的作用
管理对象的创建和依赖关系的维护。对象的创建并不是一件简单的事,在对象关系比较复杂时,如果依赖关系需要程序员来维护的话成本是很高的。
解耦,由容器去维护具体的对象。
托管了类的生产过程,比如我们需要在类的生产过程中做一些处理,最直接的例子就是代理,如有容器程序可以把这部分处理交给容器,应用程序则无需去关心类是如何完成代理的。
2.2、Spring IoC的实现机制
Spring 中的IoC的实现原理就是工厂模式加反射机制。
2.3、Spring的IoC支持哪些功能?
1、依赖注入
2、依赖检查
3、自动装配
4、支持集合
5、指定初始化方法和销毁方法
2.4、Ioc的类型有哪几种?(依赖注入的类型有哪几种)
1、构造函数注入
通过调用类的构造函数,将接口实现类通过构造函数变量传入。
2、属性注入
有选择的通过Setter方法完成调用类所需依赖的注入,更加灵活方便。
3、接口注入
将调用类所有依赖注入的方法抽取到一个接口中,调用类通过实现改接口提供相应的注入方法(必须先声明一个接口)。
(注:Spring可以通过容器来完成依赖关系的注入)
3、Spring AOP
OOP(Object-Oriented Programming)面向对象编程,允许开发者定义纵向的关系,但并不适用于定义横向的关系,导致了大量代码的重复,而不利于各个模块的重用。
AOP(Aspect-Oriented Programming),一般称为面向切面编程,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。可用于权限认证、日志、事务处理等。
特点
1、降低模块之间的耦合度
2、使系统容易扩展
3、更好的代码复用。
一、对AOP的初印象
首先先给出一段比较专业的术语(来自百度):
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方
式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个
热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑
的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高
了开发的效率。
要理解切面编程,就需要先理解什么是切面。用刀把一个西瓜分成两瓣,切开的切口就是切面;炒菜,锅与炉子共同来完成炒菜,锅与炉子就是切面。web层级设计中,web层->网关层->服务层->数据层,每一层之间也是一个切面。编程中,对象与对象之间,方法与方法之间,模块与模块之间都是一个个切面。
我们一般做活动的时候,一般对每一个接口都会做活动的有效性校验(是否开始、是否结束等等)、以及这个接口是不是需要用户登录。
按照正常的逻辑,我们可以这么做。
这有个问题就是,有多少接口,就要多少次代码copy。对于一个“懒人”,这是不可容忍的。好,提出一个公共方法,每个接口都来调用这个接口。这里有点切面的味道了。
同样有个问题,我虽然不用每次都copy代码了,但是,每个接口总得要调用这个方法吧。于是就有了切面的概念,我将方法注入到接口调用的某个地方(切点)。
这样接口只需要关心具体的业务,而不需要关注其他非该接口关注的逻辑或处理。
红框处,就是面向切面编程。
二、AOP中的相关概念
看过了上面的例子,我想大家脑中对AOP已经有了一个大致的雏形,但是又对上面提到的切面之类的术语有一些模糊的地方,接下来就来讲解一下AOP中的相关概念,了解了AOP中的概念,才能真正的掌握AOP的精髓。
这里还是先给出一个比较专业的概念定义:
- Aspect(切面): Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。
- Joint point(连接点):表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。
- Pointcut(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
- Advice(增强):Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。
- Target(目标对象):织入 Advice 的目标对象.。
- Weaving(织入):将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程
- 然后举一个csdn最火最容易理解的例子:
- 看完了上面的理论部分知识, 我相信还是会有不少朋友感觉到 AOP 的概念还是很模糊, 对 AOP 中的各种概念理解的还不是很透彻. 其实这很正常, 因为 AOP 中的概念是在是太多了, 我当时也是花了老大劲才梳理清楚的.
- 下面我以一个简单的例子来比喻一下 AOP 中 Aspect, Joint point, Pointcut 与 Advice之间的关系.
- 让我们来假设一下, 从前有一个叫爪哇的小县城, 在一个月黑风高的晚上, 这个县城中发生了命案. 作案的凶手十分狡猾, 现场没有留下什么有价值的线索. 不过万幸的是, 刚从隔壁回来的老王恰好在这时候无意中发现了凶手行凶的过程, 但是由于天色已晚, 加上凶手蒙着面, 老王并没有看清凶手的面目, 只知道凶手是个男性, 身高约七尺五寸. 爪哇县的县令根据老王的描述, 对守门的士兵下命令说: 凡是发现有身高七尺五寸的男性, 都要抓过来审问. 士兵当然不敢违背县令的命令, 只好把进出城的所有符合条件的人都抓了起来.
- 来让我们看一下上面的一个小故事和 AOP 到底有什么对应关系.
- 首先我们知道, 在 Spring AOP 中 Joint point 指代的是所有方法的执行点, 而 point cut 是一个描述信息, 它修饰的是 Joint point, 通过 point cut, 我们就可以确定哪些 Joint point 可以被织入 Advice. 对应到我们在上面举的例子, 我们可以做一个简单的类比, Joint point 就相当于 爪哇的小县城里的百姓,pointcut 就相当于 老王所做的指控, 即凶手是个男性, 身高约七尺五寸, 而 Advice 则是施加在符合老王所描述的嫌疑人的动作: 抓过来审问.
- 为什么可以这样类比呢?
- Joint point : 爪哇的小县城里的百姓: 因为根据定义, Joint point 是所有可能被织入 Advice 的候选的点, 在 Spring AOP中, 则可以认为所有方法执行点都是 Joint point. 而在我们上面的例子中, 命案发生在小县城中, 按理说在此县城中的所有人都有可能是嫌疑人.
- Pointcut :男性, 身高约七尺五寸: 我们知道, 所有的方法(joint point) 都可以织入 Advice, 但是我们并不希望在所有方法上都织入 Advice, 而 Pointcut 的作用就是提供一组规则来匹配joinpoint, 给满足规则的 joinpoint 添加 Advice. 同理, 对于县令来说, 他再昏庸, 也知道不能把县城中的所有百姓都抓起来审问, 而是根据凶手是个男性, 身高约七尺五寸, 把符合条件的人抓起来. 在这里 凶手是个男性, 身高约七尺五寸 就是一个修饰谓语, 它限定了凶手的范围, 满足此修饰规则的百姓都是嫌疑人, 都需要抓起来审问.
- Advice :抓过来审问, Advice 是一个动作, 即一段 Java 代码, 这段 Java 代码是作用于 point cut 所限定的那些 Joint point 上的. 同理, 对比到我们的例子中, 抓过来审问 这个动作就是对作用于那些满足 男性, 身高约七尺五寸 的爪哇的小县城里的百姓.
- Aspect::Aspect 是 point cut 与 Advice 的组合, 因此在这里我们就可以类比: “根据老王的线索, 凡是发现有身高七尺五寸的男性, 都要抓过来审问” 这一整个动作可以被认为是一个 Aspect.
AOP中的Joinpoint可以有多种类型:构造方法调用,字段的设置和获取,方法的调用,方法的执行,异常的处理执行,类的初始化。也就是说在AOP的概念中我们可以在上面的这些Joinpoint上织入我们自定义的Advice,但是在Spring中却没有实现上面所有的joinpoint,确切的说,Spring只支持方法执行类型的Joinpoint。
Advice 的类型
- before advice, 在 join point 前被执行的 advice. 虽然 before advice 是在 join point 前被执行, 但是它并不能够阻止 join point 的执行, 除非发生了异常(即我们在 before advice 代码中, 不能人为地决定是否继续执行 join point 中的代码)
- after return advice, 在一个 join point 正常返回后执行的 advice
- after throwing advice, 当一个 join point 抛出异常后执行的 advice
- after(final) advice, 无论一个 join point 是正常退出还是发生了异常, 都会被执行的 advice.
- around advice, 在 join point 前和 joint point 退出后都执行的 advice. 这个是最常用的 advice.
- introduction,introduction可以为原有的对象增加新的属性和方法。
Aop代理之什么是代理?
指为一个目标对象提供一个代理对象, 并由代理对象控制对目标对象的引用. 使用代理对象, 是为了在不修改目标对象的基础上, 增强目标对象的业务逻辑.
静态代理
静态代理的特点是, 为每一个业务增强都提供一个代理类, 由代理类来创建代理对象. 下面我们通过静态代理来实现对转账业务进行身份验证.
(1) 转账业务
public interface IAccountService {
//主业务逻辑: 转账
void transfer();
}
public class AccountServiceImpl implements IAccountService {
@Override
public void transfer() {
System.out.println("调用dao层,完成转账主业务.");
}
}
(2) 代理类
public class AccountProxy implements IAccountService {
//目标对象
private IAccountService target;
public AccountProxy(IAccountService target) {
this.target = target;
}
/**
* 代理方法,实现对目标方法的功能增强
*/
@Override
public void transfer() {
before();
target.transfer();
}
/**
* 前置增强
*/
private void before() {
System.out.println("对转账人身份进行验证.");
}
}
(3) 测试
public class Client {
public static void main(String[] args) {
//创建目标对象
IAccountService target = new AccountServiceImpl();
//创建代理对象
AccountProxy proxy = new AccountProxy(target);
proxy.transfer();
}
}
结果:
对转账人身份进行验证.
调用dao层,完成转账主业务.
动态代理
静态代理会为每一个业务增强都提供一个代理类, 由代理类来创建代理对象, 而动态代理并不存在代理类, 代理对象直接由代理生成工具动态生成.
JDK动态代理
JDK动态代理是使用 java.lang.reflect 包下的代理类来实现. JDK动态代理动态代理必须要有接口.
(1) 转账业务
public interface IAccountService {
//主业务逻辑: 转账
void transfer();
}
public class AccountServiceImpl implements IAccountService {
@Override
public void transfer() {
System.out.println("调用dao层,完成转账主业务.");
}
}
(2) 增强
因为这里没有配置切入点, 称为切面会有点奇怪, 所以称为增强.
public class AccountAdvice implements InvocationHandler {
//目标对象
private IAccountService target;
public AccountAdvice(IAccountService target) {
this.target = target;
}
/**
* 代理方法, 每次调用目标方法时都会进到这里
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
return method.invoke(target, args);
}
/**
* 前置增强
*/
private void before() {
System.out.println("对转账人身份进行验证.");
}
}
(3) 测试
public class Client {
public static void main(String[] args) {
//创建目标对象
IAccountService target = new AccountServiceImpl();
//创建代理对象
IAccountService proxy = (IAccountService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new AccountAdvice(target)
);
proxy.transfer();
}
}
结果:
对转账人身份进行验证.
调用dao层,完成转账主业务.
CGLIB动态代理
JDK动态代理必须要有接口, 但如果要代理一个没有接口的类该怎么办呢? 这时我们可以使用CGLIB动态代理. CGLIB动态代理的原理是生成目标类的子类, 这个子类对象就是代理对象, 代理对象是被增强过的.
注意: 不管有没有接口都可以使用CGLIB动态代理, 而不是只有在无接口的情况下才能使用.
(1) 转账业务
public class AccountService {
public void transfer() {
System.out.println("调用dao层,完成转账主业务.");
}
}
(2) 增强
因为这里没有配置切入点, 称为切面会有点奇怪, 所以称为增强.
public class AccountAdvice implements MethodInterceptor {
/**
* 代理方法, 每次调用目标方法时都会进到这里
*/
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
before();
return methodProxy.invokeSuper(obj, args);
// return method.invoke(obj, args); 这种也行
}
/**
* 前置增强
*/
private void before() {
System.out.println("对转账人身份进行验证.");
}
}
(3) 测试
public class Client {
public static void main(String[] args) {
//创建目标对象
AccountService target = new AccountService();
//
//创建代理对象
AccountService proxy = (AccountService) Enhancer.create(target.getClass(),
new AccountAdvice());
proxy.transfer();
}
}
结果:
对转账人身份进行验证.
调用dao层,完成转账主业务.
模拟Spring AOP场景
了解了动态代理后, 我们就可以自己来实现Spring AOP功能了, 所以下面我们来模拟下Spring AOP场景.
(1) 转账业务
public interface IAccountService {
//主业务逻辑: 转账
void transfer();
}
public class AccountServiceImpl implements IAccountService {
@Override
public void transfer() {
System.out.println("调用dao层,完成转账主业务.");
}
}
(2) 切面抽象类
定义一个切面抽象类, 该类使用了模板方法的设计模式, 为开始, 结束, 异常, 前置增强, 后置增强提供了默认实现, 当我们定义切面类时只需要按需重写它们就行. isIntercept() 方法用来判断切入点是否正确, 切面类需要重写这个方法.
public abstract class BaseAspect implements MethodInterceptor {
private static final Logger logger = LoggerFactory.getLogger(BaseAspect.class);
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object result = null;
begin();
try {
if (isIntercept(method, args)) {
before();
result = methodProxy.invokeSuper(obj, args);
after();
} else {
result = methodProxy.invokeSuper(obj,args);
}
} catch (Exception e) {
logger.error("proxy failure", e);
error(e);
throw e;
} finally {
end();
}
return result;
}
/**
* 开始增强
*/
public void begin() {
}
/**
* 切入点判断
*/
public boolean isIntercept(Method method, Object[] args) throws Throwable {
return true;
}
/**
* 前置增强
*/
public void before() throws Throwable {
}
/**
* 后置增强
*/
public void after() throws Throwable {
}
/**
* 异常增强
*/
public void error(Throwable e) {
}
/**
* 最终增强
*/
public void end() {
}
}
(3) 切面类
创建一个切面类, 类中配置切入点和增强.
public class AccountAspect extends BaseAspect {
/**
* 切入点
*/
public boolean isIntercept(Method method, Object[] args) throws Throwable {
return method.getName().equals("transfer");
}
/**
* 前置增强
*/
public void before() throws Throwable {
System.out.println("对转账人身份进行验证.");
}
}
(4) 代理工厂类
定义一个工厂类来创建代理, 其实不创建这个类也行, 但为了模仿Spring还是创建了. @SuppressWarnings是为了抑制警告, 就是编译器上面的黄线.
public class ProxyFactory {
@SuppressWarnings("unchecked")
public static <T> T createProxy(final Class<?> targetClass, final MethodInterceptor methodInterceptor) {
return (T) Enhancer.create(targetClass,methodInterceptor);
}
}
(5) 测试
public class Client {
public static void main(String[] args) {
//创建目标对象
IAccountService target = new AccountServiceImpl();
//切面
BaseAspect accountAspect = new AccountAspect();
//创建代理对象
IAccountService proxy = (IAccountService) ProxyFactory.createProxy(target.getClass(), accountAspect);
proxy.transfer();
}
}
结果:
对转账人身份进行验证.
调用dao层,完成转账主业务.
4、Spring的通知有哪些类型?
AOP 术语中,切面的工作被称为通知,实际上是程序执行时要通过Spring AOP框架触发的代码段。Spring切面可以应用5种类型的通知:
注: 说中文不严谨, 说英文
1、前置通知(@Before):在目标方法被调用之前调用此功能
2、后置通知(@After):在目标方法完成之后执行
3、返回通知(@AfterReturning):在目标方法成功执行之后调用此功能
4、异常通知(@AfterThrowing):在目标方法抛出异常后调用此通知
5、环绕通知(@Around):在目标方法调用之前和调用之后执行
5、Spring中bean的生命周期?
1、Spring启动,查找并加载需要被Spring管理的bean,进行Bean的实例化
2、Bean实例化后对将Bean的引入和值注入到Bean的属性中
3、如果Bean实现了BeanNameAware接口的话,Spring将Bean的Id传递给setBeanName()方法
4、如果Bean实现了BeanFactoryAware接口的话,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入
5、如果Bean实现了ApplicationContextAware接口的话,Spring将调用Bean的setApplicationContext()方法,将bean所在应用上下文引用传入进来。
6、如果Bean实现了BeanPostProcessor接口,Spring就将调用他们的postProcessBeforeInitialization()方法。
7、如果Bean 实现了InitializingBean接口,Spring将调用他们的afterPropertiesSet()方法。类似的,如果bean使用init-method声明了初始化方法,该方法也会被调用
8、如果Bean 实现了BeanPostProcessor接口,Spring就将调用他们postProcessAfterInitialization()方法。
9、此时,Bean已经准备就绪,可以被应用程序使用了。他们将一直驻留在应用上下文中,直到应用上下文被销毁。
10、如果bean实现了DisposableBean接口,Spring将调用它的destory()接口方法,同样,如果bean使用了destory-method 声明销毁方法,该方法也会被调用。
解释Spring支持的几种bean的作用域。
Spring容器中的bean可以分为5个范围:
(1)singleton:默认,每个容器中只有一个bean的实例,单例的模式由BeanFactory自身来维护。
(2)prototype:为每一个bean请求提供一个实例。
(3)request:为每一个网络请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收。
(4)session:与request范围类似,确保每个session中有一个bean的实例,在session过期后,bean会随之失效。
(5)global-session:全局作用域,global-session和Portlet应用相关。当你的应用部署在Portlet容器中工作时,它包含很多portlet。如果你想要声明让所有的portlet共用全局的存储变量的话,那么这全局变量需要存储在global-session中。全局作用域与Servlet中的session作用域效果相同。
8、Spring框架中的单例Beans是线程安全的么?
Spring框架并没有对单例bean进行任何多线程的封装处理。关于单例bean的线程安全和并发问题需要开发者自行去搞定。但实际上,大部分的Spring bean并没有可变的状态(比如Serview类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。如果你的bean有多种状态的话(比如 View Model 对象),就需要自行保证线程安全。最浅显的解决办法就是将多态bean的作用域由“singleton”变更为“prototype”。
9、Spring如何处理线程并发问题?
在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域,因为Spring对一些Bean中非线程安全状态采用ThreadLocal进行处理,解决线程安全问题。
ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式,仅提供一份变量,不同的线程在访问前需要获取锁,没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。
ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
10-1、Spring基于xml注入bean的几种方式:
(1)Set方法注入;
(2)构造器注入:①通过index设置参数的位置;②通过type设置参数类型;
(3)静态工厂注入;
(4)实例工厂;
beanFactory和applicationContext的区别?
ApplicationContext由BeanFactory派生而来,提供了更多面向实际应用的功能。beanFactory是Spring里面最低层的接口,提供了最简单的容器的功能,只提供了实例化对象和拿对象的功能。而ApplicationContext应用上下文,继承BeanFactory接口,它是Spring的一各更高级的容器,提供了更多的有用的功能;1.国际化(MessageSource)、2.访问资源(ResourceLoader)、3.载入多个(有继承关系的上下文),使得每一个上下文都专注于一个特定的层次。4.消息发送、响应机制(ApplicationEventPublisher)5.AOP拦截器
beanFactory和applicationContext的主要区别体现在装载bean的方面:
beanfactory在容器启动的时候不会去实例化bean,而是在从容器中取bean的时候才实例化一个。
applicationContext在启动的时候就把所有的bean全部实例化了。
SpringBoot
1.谈谈你对Spring Boot的理解?
SpringBoot主要用来简化使用Spring的难度和繁重的XML配置,它是Spring组件的一站式解决方案,采取了习惯优于配置的方法。通过.properties或者.yml文件替代了Spring繁杂的XML配置文件,同时支持@ImportResource注解加载XML配置。Spring Boot还提供了嵌入式HTTP服务器、命令行接口工具、多种插件等等,使得应用程序的测试和开发简单起来。
2. 为什么需要Spring Boot?
Spring Boot 优点非常多,如:独立运行、简化配置、自动配置和无需部署war文件等等
3. 说出Spring Boot 的优点
简化开发,提高整体生产力
Spring Boot 使用 JavaConfig 有助于避免使用 XML,同时避免大量的Maven导入和各种版本冲突
Spring Boot 引导的应用程序可以很容易地与 Spring 生态系统集成,如Spring JDBC、Spring ORM、Spring Data、Spring Security等等
Spring Boot 应用程序提供嵌入式HTTP服务器,如Tomcat和Jetty,可以轻松地开发和测试web应用程序。
Spring Boot 提供命令行接口工具,用于开发和测试应用程序
Spring Boot 提供了多种插件,可以使用内置Maven工具开发和测试 应用程序
Spring Boot 没有单独的 Web 服务器需要,这意味着不再需要启动 Tomcat或其他任何东西
4. Spring Boot 的核心配置文件有哪几个?它们的区别是什么?
Spring Boot 的核心配置文件是 application 和 bootstrap 配置文件。
application 配置文件主要用于 Spring Boot 项目的自动化配置。
bootstrap 配置文件有三个应用场景。
使用Spring Cloud Config配置中心时,需要在 bootstrap 配置文件中添加连接到配置中心的配置属性,来加载外部配置中心的配置信息;
一些固定的不能被覆盖的属性;
一些加密或解密的场景;
5. Spring Boot 的配置文件有哪几种格式?它们有什么区别?
主要有.properties 和 .yml格式,它们的区别主要是书写格式不同。另外,.yml 格式不支持 @PropertySource 注解导入配置。
6. 开启SpringBoot特性有哪几种方式?
继承spring-boot-starter-parent项目
导入spring-boot-dependencies项目依赖
7. 什么是Spring Boot Starter?
Starters可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,可以一站式集成 Spring 和其他技术,而不需要到处找示例代码和依赖包。Spring Boot Starter的工作原理是:Spring Boot 在启动时扫描项目所依赖的JAR包,寻找包含spring.factories文件的JAR包,根据spring.factories配置加载AutoConfigure类,根据 @Conditional注解的条件,进行自动配置并将Bean注入Spring Context
8. Spring Boot 有哪几种读取配置的方式?
使用@Value注解加载单个属性值
使用@ConfigurationProperties注解可以加载一组属性的值,针对于要加载的属性过多的情况,比@Value注解更加简洁
9. Spring Boot 支持哪些日志框架?推荐和默认的日志框架是哪个?
Spring Boot 支持 Java Util Logging, Log4j2, Logback 作为日志框架,如果使用 Starters 启动器,Spring Boot 将使用 Logback 作为默认日志框架,推荐的日志框架是Log4j2。
10. Spring Boot 可以兼容老 Spring 项目吗?
可以兼容,使用 @ImportResource 注解导入老 Spring 项目配置文件。
11. 保护 Spring Boot 应用有哪些方法?
在生产中使用HTTPS
使用Snyk检查依赖关系
升级到最新版本
启用CSRF保护
使用内容安全策略防止XSS攻击
12. 什么是 JavaConfig?
JavaConfig 是 Spring 社区的产品,它提供了配置 Spring IoC 容器的纯 Java 方法,有助于避免使用 XML 配置。
13. (Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的)介绍一下 @SpringBootApplication 注解
Spring Boot 的核心注解是@SpringBootApplication,它也是启动类使用的注解,主要包含了 3 个注解:
@SpringBootConfiguration:它组合了 @Configuration 注解,实现配置文件的功能。
@EnableAutoConfiguration:具有打开自动配置的功能,也可以关闭某个自动配置的选项。
@ComponentScan:用于Spring组件扫描。
14. Spring Boot 自动配置原理是什么?
@EnableAutoConfiguration注解、 @Configuration注解和 @ConditionalOnClass注解组成了Spring Boot自动配置的核心,首先它得是一个配置文件,其次根据类路径下是否有这个类去自动配置。具体是通过maven读取每个starter中的spring.factories文件,该文件配置了所有需要被创建在spring容器中的bean。
15. 你如何理解 Spring Boot 配置加载顺序?
Spring Boot配置加载顺序优先级是:propertiese文件、YAML文件、系统环境变量、命令行参数。
16. Spring Boot支持哪些嵌入式Web容器?
Spring Boot支持的嵌入式servlet容器有: Tomcat、Jetty、Undertow。
17. 什么是YAML?
YAML 是一种可读的数据序列化语言,它通常用于配置文件。
18. YAML 配置的优势在哪里 ?
配置有序
支持数组,数组中的元素可以是基本数据类型或者对象
简洁方便
19. Spring Boot 是否可以使用 XML 配置 ?
Spring Boot 推荐使用 Java 配置同时支持 XML 配置,通过 @ImportResource 注解加载 XML 配置。
20. application.properties和bootstrap.properties有何区别 ?
bootstrap比 applicaton 优先加载,配置在应用程序上下文的引导阶段生效, 而且boostrap 里面的属性不能被覆盖;
application用于 spring boot 项目的自动化配置。
21. 什么是 Spring Profiles?
Spring Profiles 允许用户根据配置文件(dev,prod,test等等)来注册 bean。当应用程序在开发环境中运行时,只有某些 bean 可以加载,而在生产环境中,某些其他 bean 也可以加载。比如要求 Swagger 文档仅适用于测试环境,并且禁用所有其他文档,可以使用配置文件来完成。
22. 如何在自定义端口上运行 Spring Boot 应用程序
可以在 application.properties 配置文件中指定端口,比如server.port = 8090
23. 如何实现 Spring Boot 应用程序的安全性?
为了实现 Spring Boot 的安全性,可以使用 spring-boot-starter-security 依赖,添加安全配置和重写WebSecurityConfigurerAdapter 配置类的方法。
24. 什么是 WebSocket?
WebSocket 是一种计算机通信协议,通过单个 TCP 连接提供全双工通信信道。
WebSocket 是双向的 ,使用 WebSocket 客户端或服务器可以实现消息发送。
WebSocket 是全双工的 ,客户端和服务器通信是相互独立的。
WebScoket 使用单个 TCP 连接 ,与http 相比,WebSocket 消息数据交换要轻得多。
25. Spring Boot 中的监视器是什么?(什么是Spring Boot Actuator)?
Spring boot actuator 是 spring 启动框架中的重要功能之一,Spring boot 监视器可以访问生产环境中正在运行的应用程序的当前状态。监视器模块公开了一组可直接作为 HTTP URL 访问的 REST 端点来检查状态。
26. 如何在 Spring Boot 中禁用 Actuator 端点安全性?
默认情况下,所有敏感的 HTTP 端点都是安全的,只有具有 ACTUATOR 角色的用户才能访问它们。
安全性是使用标准的 HttpServletRequest.isUserInRole 方法实施的,可以用来禁用安全性。
只有在执行机构端点在防火墙后访问时,才建议禁用安全性。
27. 什么是 CSRF 攻击?
CSRF 代表跨站请求伪造,这是一种攻击,迫使最终用户在当前通过身份验证的Web 应用程序上执行不需要的操作。CSRF 攻击专门针对状态改变请求,而不是数据窃取,因为攻击者无法查看对伪造请求的响应。
28. 如何使用 Spring Boot 实现异常处理?
Spring 通过使用 @ControllerAdvice 注解处理异常,实现一个ControllerAdvice 类来处理控制器类抛出的所有异常。
29. 如何监视所有 Spring Boot 微服务?
Spring Boot 提供监视器端点监控各个微服务,这些端点对于获取有关应用程序的信息(如它们是否已启动)以及它们的组件(如数据库等)是否正常运行很有帮助。但是用监视器的一个主要缺点是,必须单独打开应用程序的知识点以了解其状态或健康状况。
30. 运行 Spring Boot 有哪几种方式?
用命令打包或者放到容器中运行
用 Maven 插件运行
直接执行 main 方法运行
MyBatis
MyBatis 是一款优秀的持久层框架
中文官网:https://mybatis.org/mybatis-3/zh/getting-started.html
1.1、什么是MyBatis
它支持定制化 SQL、存储过程以及高级映射。
MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
1.2持久化
持久化就是将程序的数据在持久状态和瞬时状态转化的过程
内存:断电即失
数据库(JDBC),io文件持久化。
生活:冷藏、罐头。
为什么需要持久化?------------------------有一些对象,不能让他丢掉。
1.3、持久层
Dao层、Service层、Controller层等
完成持久化工作的代码块 层界限十分明显
1.4、为什么需要Mybatis
帮助程序员将数据存入到数据库中。
方便
传统的JDBC代码太复杂了。简化、框架、自动化。
不用Mybatis也可以。更容易上手。技术没有高低之分
优点:
简单易学
灵活
sql和代码的分离,提高了可维护性。
提供映射标签,支持对象与数据库的orm字段关系映射
提供对象关系映射标签,支持对象关系组建维护
提供xml标签,支持编写动态sql。
可能会遇到的问题:
- 配置文件没有注册
- 绑定接口错误
- 方法名不对
- 返回类型不对
- Maven导出资源问题
3、CRUD
3.1、namespace
namespace中的包名要和Dao/mapper接口的包名保持一致
3.2、select
id:就是对应的namespace中的方法名;resultType:Sql语句执行的返回值!
parameterType:参数类型!
package com.rui.dao;
import com.rui.pojo.User;
import java.util.List;
public interface UserMapper {
//根据id查询用户
User getUserById(int id);
}
编写对应的mapper中的sql语句
<select id="getUserById" resultType="com.rui.pojo.User" parameterType="int">
/*定义sql*/
select * from mybatis.user where id = #{id};
</select>
测试
@Test
public void getUserById(){
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.getUserById(1);
System.out.println(user);
sqlSession.close();
}
3.3、Insert
3.4、Update
3.5、Delete
<?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核心配置文件-->
<configuration>
<!--environments配置环境组-->
<!--default默认环境-->
<environments default="development">
<!--environment单个环境-->
<environment id="development">
<!--transactionManager配置事务管理器-->
<transactionManager type="JDBC"/>
<!--配置连接池-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useSSL=true&useUnicode=true"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<!--每一个Mapper.xml需要在Mybatis核心配置文件中注册-->
<mappers>
<mapper resource="com/newer/dao/UserMapper.xml"/>
</mappers>
</configuration>
package com.newer.utils;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class MyBatisUtils {
private static SqlSessionFactory sqlSessionFactory;
static {
try {
//使用mybatis第一步、获取sqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
//既然有了 SqlSessionFactory,顾名思义,我们就可以从中获得 SqlSession 的实例了。
// SqlSession 完全包含了面向数据库执行 SQL 命令所需的所有方法。
public static SqlSession getSqlSession(){
return sqlSessionFactory.openSession();
}
}
package com.newer.pojo;
//实体类
public class User {
private int id;
private String name;
private String pwd;
public User() {
}
public User(int id, String name, String pwd) {
this.id = id;
this.name = name;
this.pwd = pwd;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
@Override
public String toString() {
return "user{" +
"id=" + id +
", name='" + name + '\'' +
", pwd='" + pwd + '\'' +
'}';
}
}
package com.newer.dao;
import com.newer.pojo.User;
import java.util.List;
public interface UserMapper {
//查询全部用户
List<User> getUserList();
//根据ID查询用户
User getUserById(int id);
//insert一个用户
int addUser(User user);
//修改用户
int updateUser(User user);
//删除用户
int deleteUser(int id);
}
<?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">
<!--namespace绑定一个对应的Mapper接口-->
<mapper namespace="com.newer.dao.UserMapper">
<select id="getUserList" resultType="com.newer.pojo.User">
select * from mybatis.user
</select>
<select id="getUserById" parameterType="int" resultType="com.newer.pojo.User">
select * from mybatis.user where id=#{id}
</select>
<insert id="addUser" parameterType="com.newer.pojo.User">
insert into mybatis.user (id,name,pwd) values (#{id},#{name },#{pwd})
</insert>
<update id="updateUser" parameterType="com.newer.pojo.User">
update mybatis.user set name=#{name},pwd=#{pwd} where id=#{id}
</update>
<delete id="deleteUser" parameterType="int">
delete from mybatis.user where id=#{id}
</delete>
</mapper>
package com.newer.dao;
import com.newer.pojo.User;
import com.newer.utils.MyBatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
import java.util.List;
public class UserDaoTest {
@Test
public void test(){
//第一步:获得SqlSession对象
SqlSession sqlSession = MyBatisUtils.getSqlSession();
//方式一:getMapper
UserMapper userDao = sqlSession.getMapper(UserMapper.class);
List<User> userList = userDao.getUserList();
//方式二
//List<User> userList = sqlSession.selectList("com.newer.dao.UserMapper.getUserList");
for (User user : userList) {
System.out.println(user);
}
//关闭sqlSession
sqlSession.close();
}
@Test
public void getUserById(){
//第一步:获得SqlSession对象
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.getUserById(1);
System.out.println(user);
//关闭sqlSession
sqlSession.close();
}
//增删改需要提交事务
@Test
public void addUser(){
//第一步:获得SqlSession对象
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
int res = mapper.addUser(new User(4, "哈哈", "12364"));
if(res>0){
System.out.println("插入成功!");
}
//提交事务
sqlSession.commit();
//关闭sqlSession
sqlSession.close();
}
@Test
public void updateUser(){
//第一步:获得SqlSession对象
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
int res = mapper.updateUser(new User(4,"呵呵","88888"));
if(res>0){
System.out.println("修改成功!");
}
//提交事务
sqlSession.commit();
//关闭sqlSession
sqlSession.close();
}
@Test
public void deleteUser(){
//第一步:获得SqlSession对象
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
int res = mapper.deleteUser(4);
if(res>0){
System.out.println("删除成功!");
}
//提交事务
sqlSession.commit();//这里就是提交事务!!!!!!!!!!!!!!!!!!
//关闭sqlSession
sqlSession.close();
}
}
注意点:增删改需要提交事务!!!!!!!!!!!!!!!!!!
3.6、分析错误
- 标签不要匹配错误
- resource绑定mapper,需要使用路径!
- 程序配置文件必须符合规范
- NullPointerException,没有注册到资源
- 输出的xml文件中存在中文乱码问题
- maven资源没有导出问题
3.7、万能Map
我们的实体类,或者数据库中的表,字段或者参数过多,我们应当考虑使用Map!
int addUser2(Map<String,Object> map);
<!--对象中的属性,可以直接取出来 parameterType=传递map中的key-->
<insert id="addUser2" parameterType="map">
insert into mybatis.user (id, name, pwd) values (#{userId},#{userName},#{password});
</insert>
@Test
public void addUser2(){
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
HashMap<String, Object> map = new HashMap<>();
map.put(“userId”,4);
map.put(“userName”,“闯哥”);
map.put(“password”,“yuzhouzuishuai”);
mapper.addUser2(map);
//提交事务
sqlSession.commit();
sqlSession.close();
}
- Map传递参数,直接在sql中取出key即可!【parameterType=“map”】
- 对象传递参数,直接在sql中取对象的属性即可!【parameterType=“Object”】
- 只有一个基本类型参数的情况下,可以直接在sql中取到!
- 多个参数用Map,或者注解!
3.8、思考模糊查询
Java代码执行的时候传递通配符%%
List<User> userList=mapper.getUserLike("%李%");
在sql拼接中使用通配符!
select * from mybatis.user where name like "%"#{value}"%" ---------可能存在SQL注入问题
什么是SQL 注入?
SQL 注入是一种非常常见的数据库攻击手段,SQL 注入漏洞也是网络世界中最普遍的漏洞 之一。大家也许都听过某某学长通过攻击学校数据库修改自己成绩的事情,这些学长们一 般用的就是 SQL 注入方法。SQL 注入其实就是恶意用户通过在表单中填写包含 SQL 关键字的数据来使数据库执行非常 规代码的过程。简单来说,就是数据「越俎代庖」(yuè zǔ dài páo)做了代码才能干的 事情。这个问题的来源是,SQL 数据库的操作是通过 SQL 语句来执行的,而无论是执行代 码还是数据项都必须写在 SQL 语句之中,这就导致如果我们在数据项中加入了某些 SQL 语 句关键字(比如说 SELECT、DROP 等等),这些关键字就很可能在数据库写入或读取数据 时得到执行。
如何解决:【1】在JDBC中,使用Statement的子类PreparedStatement,这也是我们最先想到的方法,事先将sql语句传入PreparedStatement中,等会要传入的参数使用?代替,那么该sql语句会进行预编译,之后将前台获取的参数通过set方式传入编译后的sql语句中,那么此时被注入的sql语句无法得到编译,从而避免了sql注入的问题。而且使用PreparedStatement在一定程度上有助于数据库执行性能的提升。
【2】Mybatis中尽量使用#{}而不是${}
#{}就像是JDBC中的?,mybatis通过使用#{}的方式,代表该语句在执行之前会经过预编译的过程,后来传入的参数通过占位符的方式填入到已经编译完的语句中。但使用${}的参数会直接参与编译,不能避免sql注入。如果需求中非要使用到${}的话,只能手动过滤了。
【3】手动过滤参数
声明一个危险参数数组danger,将前台传入的参数用“ ”分隔后,产生的参数数组与danger有交集时,终止该sql的执行,并反馈前台,提示禁止输入非法字符。
【4】Mybatis之bind标签使用,可解决模糊查询sql 注入,参数名不一致等 见如下代码块
<if test="titledescription != null and titledescription != ''">
<bind name="titledescription" value="'%'+titledescription+'%'"/>
and titleDescription like #{titledescription}
或者
like concat('%', #{titledescription }, '%')
</if>
4、配置解析
4.1、核心配置文件
mybatis-config.xml
MyBatis的配置文件包含了会深深影响MyBatis行为的设置和属性信息
configuration(配置)
properties(属性)
settings(设置)
typeAliases(类型别名)
typeHandlers(类型处理器)
objectFactory(对象工厂)
plugins(插件)
environments(环境配置)
environment(环境变量)
transactionManager(事务管理器)
dataSource(数据源)
databaseIdProvider(数据库厂商标识)
mappers(映射器)
4.2、环境配置(environments)
MyBatis 可以配置成适应多种环境
不过要记住:尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。
学会使用配置多套运行环境!
MyBatis默认的事务管理器就是JDBC,连接池:POOLED
4.3、属性(properties)
可以通过properties属性来实现引用配置文件
这些属性都是可外部配置且可动态替换的,既可以在典型的 Java 属性文件中配置,亦可通过 properties 元素的子元素来传递。
db.properties
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?
useSSL=true&useUnicode=true&characterEncoding=utf8
username=root
password=Cc105481
在核心配置文件中引入
<properties resource="db.properties">
<property name="username" value="root"/>
<property name="password" value="Cc105481"/>
</properties>
可以直接引入外部文件 可以在其中增加一些属性配置 如果两个文件有同一字段,优先使用外部配置文件的!
4.4、类型别名(typeAliases)
类型别名是为 Java 类型设置一个短的名字。 存在的意义仅在于用来减少类完全限定名的冗余。
<!--可以给实体类起别名-->
<typeAliases>
<typeAlias type="com.rui.pojo.User" alias="User"/>
</typeAliases>
可以指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean,比如: 扫描实体类的包,他的默认别名就为这个类的类名,首字母小写!
<!--可以给实体类起别名-->
<typeAliases>
<package name="com.rui.pojo"/>
</typeAliases>
- 在实体类比较少的时候,使用第一种方式。
- 如果实体类十分多,建议使用第二种方式。
第一种可以DIY别名,第二种则不行,如果非要改,需要在实体类(pojo)上增加@Alias注解
@Alias(“author”)
public class Author {
…
}
4.5、MyBatis设置
这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为----------------Settings
4.6、映射器(mappers)
<!--每一个Mapper.xml需要在Mybatis核心配置文件中注册-->
<mappers>
<!-- <mapper resource="com/newer/dao/UserMapper.xml"/>-->
<!-- <mapper class="com.newer.dao.UserMapper"></mapper>-->
<package name="com.newer.dao"/>
</mappers>
除了第一种没有限制条件,其他两种有限制条件
- 接口和它的Mapper配置文件必须同名
- 接口和它的Mapper配置文件必须在同一个包下
4.7、MyBatis生命周期和作用域
生命周期,和作用域是至关重要的,因为错误的使用会导致非常严重的并发问题 !!!!!!!!
SqlSessionFactoryBuilder:
- 一旦创建了SqlSessionFactory,就不再需要它了
- 局部变量
mybatis运行流程SqlSessionFactory:
可以想象为:数据库连接池
SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。
因此SqlSessionFactory的最佳作用域是应用作用域。
最简单的就是使用 单例模式 或者静态单例模式
SqlSession:
- 连接到连接池的一个请求!
- SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。
- 用完之后需要赶紧关闭,否则会占用资源
- SqlSessionFactory
这里的每一个Mapper,就代表一个具体的业务!
5、解决属性名和字段名不一致的问题
5.1、问题
数据库中的字段
新建一个项目,拷贝之前的,测试实体类字段不一致的情况。
public class User { private int id; private String name; private String password; }
测试错误
解决方法:起别名
select id,name,pwd as password from mybatis.user where id = #{id}
5.2、resultMap
结果集映射
- id name pwd
- id name password
<resultMap id="UserMap" type="User">
<!--column数据库中的字段,property实体类中的属性-->
<result column=" id" property="id"/>
<result column="name" property="name"/>
<result column="pwd" property="password"/>
</resultMap>
<select id="getUserById" resultMap="UserMap" parameterType="int">
/*定义sql*/
select * from mybatis.user where id = #{id};
resultMap 元素是 MyBatis 中最重要最强大的元素
ResultMap 的设计思想是,对于简单的语句根本不需要配置显式的结果映射,而对于复杂一点的语句只需要描述它们的关系就行了。
ResultMap 最优秀的地方在于,虽然你已经对它相当了解了,但是根本就不需要显式地用到他们。
6、日志
6.1、日志工厂
如果一个数据库操作,出现了异常,我们需要排错。日志就是最好的助手!
- 曾经:sout、debug
- 现在:日志工厂
- SLF4J
- LOG4J【掌握】
- LOG4J2
- JDK_LOGGING
- COMMONS_LOGGING
- STDOUT_LOGGING【掌握】
- NO_LOGGING
STDOUT_LOGGING标准日志输出
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
在mybatis核心配置文件中,配置我们的日志!
6.2、Log4j
Log4j是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件
我们也可以控制每一条日志的输出格式通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。
通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。有一些公司也用sl4j 大同小异
导入log4j的包
<dependencies>
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
log4j.properties
#将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代码
log4j.rootLogger=DEBUG,console,file
#控制台输出的相关设置
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.Threshold=DEBUG
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=[%c]-%m%n
#文件输出的相关设置
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/kuang.log
log4j.appender.file.MaxFileSize=10mb
log4j.appender.file.Threshold=DEBUG
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n
#日志输出级别
log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG
配置log4j为日志实现
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>
log4j的使用!直接测试运行刚才的查询
DEBUG [main] (LogFactory.java:105) - Logging initialized using ‘class org.apache.ibatis.logging.log4j.Log4jImpl’ adapter.
DEBUG [main] (LogFactory.java:105) - Logging initialized using ‘class org.apache.ibatis.logging.log4j.Log4jImpl’ adapter.
DEBUG [main] (PooledDataSource.java:353) - PooledDataSource forcefully closed/removed all connections.
DEBUG [main] (PooledDataSource.java:353) - PooledDataSource forcefully closed/removed all connections.
DEBUG [main] (PooledDataSource.java:353) - PooledDataSource forcefully closed/removed all connections.
DEBUG [main] (PooledDataSource.java:353) - PooledDataSource forcefully closed/removed all connections.
DEBUG [main] (JdbcTransaction.java:136) - Opening JDBC Connection
Loading class com.mysql.jdbc.Driver'. This is deprecated. The new driver class iscom.mysql.cj.jdbc.Driver’. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
DEBUG [main] (PooledDataSource.java:424) - Created connection 2049051802.
DEBUG [main] (JdbcTransaction.java:100) - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7a220c9a]
DEBUG [main] (BaseJdbcLogger.java:143) - ==> Preparing: /定义sql/ select * from mybatis.user where id = ?;
DEBUG [main] (BaseJdbcLogger.java:143) - > Parameters: 1(Integer)
DEBUG [main] (BaseJdbcLogger.java:143) - < Total: 1
User{id=1, name=‘狂神’, password=‘123456’}
DEBUG [main] (JdbcTransaction.java:122) - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7a220c9a]
DEBUG [main] (JdbcTransaction.java:90) - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7a220c9a]
DEBUG [main] (PooledDataSource.java:381) - Returned connection 2049051802 to pool.
Disconnected from the target VM, address: ‘127.0.0.1:58296’, transport: ‘socket’
Process finished with exit code 0
简单使用
//在要使用Log4j 的类中,导入org.apache.log4j.Logger;
//日志对象,加载参数为当前类的class
static Logger logger = Logger.getLogger(UserDaoTest.class);
//日志级别
logger.info("info:进入了testLog4j方法");
logger.debug("debug:进入了testLog4j");
logger.error("error:进入了testLog4j");
7、分页
为什么要分页?------------减少数据的处理量
7.1、使用Limit分页
select * from user limit startIndex,pageSize
使用Mybatis实现分页,核心SQL
//分页
List<User> getUserByLimit(Map<String,Integer> map);
Mapper.xml
<!--分页-->
<select id="getUserByLimit" parameterType="map" resultMap="UserMap">
select * from mybatis.user limit #{startIndex},#{pageSize}
</select>
测试
@Test
public void getUserByLimit(){
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
HashMap<String, Integer> map = new HashMap<>();
map.put("startIndex",0);
map.put("pageSize",2);
List<User> userList = mapper.getUserByLimit(map);
for (User user : userList) {
System.out.println(user);
}
sqlSession.close();
}
7.2、RowBounds分页
不再使用SQL实现分页
List<User> getUserByRowBounds();
mapper.xml
<!--分页2-->
<select id="getUserByRowBounds" resultMap="UserMap">
select * from mybatis.user
</select>
测试
@Test
public void getUserByRowBounds(){
SqlSession sqlSession = MyBatisUtils.getSqlSession();
//RowBounds实现
RowBounds rowBounds = new RowBounds(1, 2);
//通过java代码层面实现分页
List<User> userList = sqlSession.selectList("com.rui.dao.UserMapper.getUserByRowBounds",null,rowBounds);
for (User user : userList) {
System.out.println(user);
}
sqlSession.close();
}
7.3、分页插件
8、使用注解开发
8.1、面向接口编程
面向接口编程的根本原因:解耦,可拓展,提高复用,分层开发中、上层不用管具体的实现,大家都遵守共同的标准,使得开发变得容易,规范性好
8.2、使用注解开发
注解在接口上实现
@Select(value = "select * from user")
List<User> getUsers();
需要在核心配置文件中绑定接口!
<!--绑定接口-->
<mappers>
<mapper class="rui.dao.UserMapper"/>
</mappers>
测试
public class UserMapperTest {
@Test
public void test(){
SqlSession sqlSession = MyBatisUtils.getSqlSession();
//底层主要应用反射
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List users = mapper.getUsers();
for (User user : users) {
System.out.println(user);
}
sqlSession.close();
}
}
- 本质:反射机制实现
- 底层:动态代理!
8.3、CRUD
在工具类创建的时候实现自动提交事务!
public static SqlSession getSqlSession(){
return sqlSessionFactory.openSession(true);
}
编写接口,增加注解
public interface UserMapper {
@Select(value = “select * from user”)
List getUsers();
//方法存在多个参数,所有的参数前面必须加上@Param注解
@Select("select * from user where id = #{id} or name = #{name}")
User getUserByID(@Param("id")int id,@Param("name")String name);
@Insert("insert into user(id,name,pwd) values (#{id},#{name},#{password})")
int addUser(User user);
@Update("update user set name = #{name},pwd = #{password} where id = #{id}")
int updateUser(User user);
@Delete("delete from user where id = #{uid}")
int deleteUser(@Param("uid") int id);
}
测试类
【注意:我们必须要将接口注册绑定到我们的核心配置文件中!】
关于@Param()注解
- 基本类型的参数或者String类型,需要加上
- 引用类型不需要加
- 如果只有一个基本类型的话,可以忽略,但是建议大家都加上
- 我们在SQL中引用的就是我们这里的@Param()中设定的属性名
9、Lombok
Project Lombok is a java library that automatically plugs into your editor and build tools, spicing up your java.
Project Lombok是一个java库,它可以自动插入你的编辑器和构建工具中,为你的java添彩。
Never write another getter or equals method again, with one annotation your class has a
再也不用写另一个getter或equals方法了,只需一个注释,你的类就有了一个
fully featured builder, Automate your logging variables, and much more.
功能齐全的构建器,自动记录变量,还有更多。
使用步骤:
//在IDEA中安装Lombok插件
//在项目中导入lombok的jar包
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
//在实体类上加注解即可
@Getter and @Setter
@ToString
@EqualsAndHashCode
@AllArgsConstructor, @RequiredArgsConstructor and @NoArgsConstructor
@Log, @Log4j, @Log4j2, @Slf4j
@Data
10、多对一处理
多对一:
- 多个学生,对应一个老师
- 对于学生这边而言,关联...多个学生,关联一个老师【多对一】
- 对于老师而言,集合,一个老师又很多学生【一对多】
CREATE TABLE teacher(
id int(10) Not null,
name VARCHAR(30) DEFAULT NULL,
PRIMARY KEY (id)
)ENGINE=INNODB DEFAULT CHARSET=utf8
INSERT INTO teacher(id,name) VALUES (1,‘赵老师’);
CREATE TABLE student(
id int(10) Not null,
name VARCHAR(30) DEFAULT NULL,
tid INT(10) DEFAULT NULL,
PRIMARY KEY (id),
KEY fktid(tid),
CONSTRAINT fktid FOREIGN KEY (tid) REFERENCES teacher (id)
)ENGINE=INNODB DEFAULT CHARSET=utf8
INSERT INTO student(id,name,tid) VALUES (1,‘张子豪’,1);
INSERT INTO student(id,name,tid) VALUES (2,‘郭全’,1);
INSERT INTO student(id,name,tid) VALUES (3,‘金旭辉’,1);
INSERT INTO student(id,name,tid) VALUES (4,‘丁鹏’,1);
INSERT INTO student(id,name,tid) VALUES (5,‘吴签’,1);
测试环境:
- 导入lombok 新建实体类Teacher,Student
- 新建Mapper接口
- 建立Mapper.XML文件
- 在核心配置文件中绑定注册我们的MApper接口或者文件!【方式很多,随意选】
- 测试查询是否成功!
按照查询嵌套处理
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.newer.dao.StudentMapper">
<!--1.查询所有学生信息
2.根据查询出来的学生的tid,寻找对应的老师
-->
<select id="getStudent" resultMap="StudentTeacher">
select * from student
</select>
<resultMap id="StudentTeacher" type="Student">
<result property="id" column="id"></result>
<result property="name" column="name"></result>
<!--复杂的属性,需要单独处理
对象:association
集合:collection
-->
<association property="teacher" column="tid" javaType="Teacher" select="getTeacher"></association>
</resultMap>
<select id="getTeacher" resultType="Teacher">
select * from teacher where id=#{id}
</select>
</mapper>
按照结果嵌套处理
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.newer.dao.StudentMapper">
<!--按照结果嵌套处理-->
<select id="getStudent2" resultMap="StudentTeacher2">
select s.id sid,s.name sname,t.name tname,t.id tid
from student s,teacher t
where s.tid=t.id
</select>
<resultMap id="StudentTeacher2" type="Student">
<result property="id" column="sid"></result>
<result property="name" column="sname"></result>
<association property="teacher" javaType="Teacher">
<result property="id" column="tid"></result>
<result property="name" column="tname"></result>
</association>
</resultMap>
</mapper>
回顾Mysql多对一查询方式:
- 子查询
- 多表联查
11、一对多处理
比如:一个老师拥有多个学生!
对于老师而言,就是一对多的关系!
@Data
public class Teacher {
private int id;
private String name;
//一个老师拥有多个学生
private List<Student> students;
}
@Data
public class Student {
private int id;
private String name;
private int tid;
}
//按照结果嵌套处理
select s.id sid,s.name sname,t.name tname,t.id tid from student s,teacher t where s.tid=t.id and t.id = #{tid}
//按照查询嵌套处理
select * from mybatis.teacher where id = #{tid} select * from mybatis.student where tid = #{tid}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.newer.dao.TeacherMapper">
<select id="getTeacher" resultMap="TeacherStudent">
select s.id sid ,s.name sname,t.name tname,t.id tid
from student s,teacher t
where s.tid=t.id and t.id=#{tid}
</select>
<resultMap id="TeacherStudent" type="Teacher">
<result property="id" column="tid"></result>
<result property="name" column="tname"></result>
<collection property="students" ofType="Student">
<result property="id" column="sid"></result>
<result property="name" column="sname"></result>
<result property="tid" column="tid"></result>
</collection>
</resultMap>
<!--=====================================================-->
<select id="getTeacher2" resultMap="TeacherStudent2">
select * from teacher where id=#{tid}
</select>
<resultMap id="TeacherStudent2" type="Teacher">
<collection property="students" column="id" javaType="ArrayList" ofType="Student" select="getStudentByTeacherId"></collection>
</resultMap>
<select id="getStudentByTeacherId" resultType="Student">
select * from student where tid=#{tid}
</select>
</mapper>
关联-association【多对一】
集合-collection 【一对多】
javaType & ofType
- JavaType用来指定实体类中属性的类型
- ofType用来指定映射到List或者集合中的pojo类型,泛型中的约束类型!
注意点:
- 保证SQL的可读性,尽量保证通俗易懂
- 注意一对多和多对一中,属性名和字段的问题!
- 如果问题不好排查错误,可以使用日志,建议使用Log4j
面试高频:
- Mysql引擎
- InnoDB底层原理
- 索引
- 索引优化!
- 关于数据库到公司慢慢去消化
12、动态SQL
动态SQL:动态SQL就是指根据不同的条件生成不同的SQL语句
动态 SQL 元素和 JSTL 或基于类似 XML 的文本处理器相似。在 MyBatis 之前的版本中,有很多元素需要花时间了解。MyBatis 3 大大精简了元素种类,现在只需学习原来一半的元素便可。MyBatis 采用功能强大的基于 OGNL 的表达式来淘汰其它大部分元素。
动态SQL:动态SQL就是指根据不同的条件生成不同的SQL语句
动态 SQL 元素和 JSTL 或基于类似 XML 的文本处理器相似。在 MyBatis 之前的版本中,有很多元素需要花时间了解。MyBatis 3 大大精简了元素种类,现在只需学习原来一半的元素便可。MyBatis 采用功能强大的基于 OGNL 的表达式来淘汰其它大部分元素。
搭建环境
CREATE TABLE blog(
id VARCHAR(50) NOT NULL COMMENT ‘博客id’,
title VARCHAR(100) not null comment ‘博客标题’,
author VARCHAR(30) not null comment ‘博客作者’,
creat_time datetime not null comment ‘创建时间’,
views int(30) not null comment ‘浏览量’
)ENGINE=InnoDB DEFAULT CHARSET=utf8
创建一个基础工程
- 导包
- 编写配置文件
- 编写实体类
- 编写实体类对应的Mapper接口和Mapper.xml
package com.newer.pojo;
import lombok.Data;
import java.util.Date;
@Data
public class Blog {
private String id;
private String title;
private String author;
private Date creatTime;
private int views;
}
<?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核心配置文件-->
<configuration>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<typeAliases>
<package name="com.newer.pojo"/>
</typeAliases>
<!--environments配置环境组-->
<!--default默认环境-->
<environments default="development">
<!--environment单个环境-->
<environment id="development">
<!--transactionManager配置事务管理器-->
<transactionManager type="JDBC"/>
<!--配置连接池-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useSSL=true&useUnicode=true"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<!--每一个Mapper.xml需要在Mybatis核心配置文件中注册-->
<mappers>
<mapper class="com.newer.dao.BlogMapper"/>
</mappers>
</configuration>
package com.newer.utils;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class MyBatisUtils {
private static SqlSessionFactory sqlSessionFactory;
static {
try {
//使用mybatis第一步、获取sqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
//既然有了 SqlSessionFactory,顾名思义,我们就可以从中获得 SqlSession 的实例了。
// SqlSession 完全包含了面向数据库执行 SQL 命令所需的所有方法。
public static SqlSession getSqlSession(){
return sqlSessionFactory.openSession(true);
}
}
package com.newer.utils;
import org.junit.Test;
import java.util.UUID;
@SuppressWarnings("all")//抑制警告
public class IDutils {
public static String getId(){
return UUID.randomUUID().toString().replace("-","");
}
@Test
public void test(){
System.out.println(IDutils.getId());
}
}
package com.newer.dao;
import com.newer.pojo.Blog;
import java.util.List;
import java.util.Map;
public interface BlogMapper {
//插入数据
int addBlog(Blog blog);
//查询博客
List<Blog> queryBlogIF(Map map);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.newer.dao.BlogMapper">
<insert id="addBlog" parameterType="Blog" >
insert into mybatis.blog(id,title,author,creat_time,views)
values (#{id},#{title},#{author},#{creatTime},#{views})
</insert>
<select id="queryBlogIF" parameterType="map" resultType="Blog">
select * from mybatis.blog where 1=1
<if test="title !=null">
and title=#{title}
</if>
<if test="author !=null">
and author=#{author}
</if>
</select>
</mapper>
IF
select * from mybatis.bolg where 1=1 and title = #{title} and author = #{author}
import com.newer.dao.BlogMapper;
import com.newer.pojo.Blog;
import com.newer.utils.IDutils;
import com.newer.utils.MyBatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
public class MyTest {
@Test
public void addInitBlog(){
SqlSession sqlSession = MyBatisUtils.getSqlSession();
BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
Blog blog = new Blog();
blog.setId(IDutils.getId());
blog.setTitle("Mybatis如此简单");
blog.setAuthor("闯哥说");
blog.setCreatTime(new Date());
blog.setViews(9999);
mapper.addBlog(blog);
blog.setId(IDutils.getId());
blog.setTitle("Java如此简单");
mapper.addBlog(blog);
blog.setId(IDutils.getId());
blog.setTitle("Spring如此简单");
mapper.addBlog(blog);
blog.setId(IDutils.getId());
blog.setTitle("微服务如此简单");
mapper.addBlog(blog);
sqlSession.close();
}
@Test
public void queryBlogIF(){
SqlSession sqlSession = MyBatisUtils.getSqlSession();
BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
HashMap map = new HashMap();
map.put("author","闯哥说");
List<Blog> blogs = mapper.queryBlogIF(map);
for (Blog blog : blogs) {
System.out.println(blog);
}
sqlSession.close();
}
}
所谓的动态SQL,本质还是SQL语句,只是我们可以在SQL层面,去执行一些逻辑代码
if
Where,set,choose,when
SQL片段-------有的时候,我们可能会将一些公共的部分抽取出来,方便复用!
使用SQL标签抽取公共的部分
<sql id="if-title-author">
<if test="title != null">
title = #{title}
</if>
<if test="author != null">
and author = #{author}
</if>
</sql>
在需要使用的地方使用Include标签引用即可
<select id="queryBlogIF" parameterType="map" resultType="com.rui.pojo.Blog">
select * from mybatis.bolg
<where>
<include refid="if-title-author"></include>
</where>
</select>
注意事项:
- 最好基于单表来定义SQL片段!
- 不要存在where或者set标签,片段里尽量只有if就好了
foreach
select * from user where 1=1 and (id=1 or id=2 or id=3)
select * from mybatis.bolg
<where>
<foreach collection="ids" item="id" open="(" close=")" separator="or">
id = #{id}
</foreach>
</where>
动态SQL就是在拼接SQL语句,我们只要保证SQL的正确性,按照SQL的格式,去排列组合就可以了建议: 先在Mysql中写出完整的SQL,在对应的去修改称为我们的动态SQL
13、缓存(了解)
13.1、简介
查询 : 连接数据库,耗资源!
一次查询的结果,给他暂存在一个可以直接取到的地方!—>内存 : 缓存我们再次查询相同数据的时候,直接走缓存,就不用走数据库了
缓存[Cache]:
存在内存中的临时数据。
将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询,
从缓存中查询,从而提高查询效率,解决了高并发系统的性能问题。
为什么使用缓存?减少和数据库的交互次数,减少系统开销,提高系统效率。
什么样的数据能使用缓存?经常查询并且不经常改变的数据
MyBatis包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率。
MyBatis系统中默认定义了两级缓存:一级缓存和二级缓存
默认情况下,只有一级缓存开启。(SqlSession级别的缓存,也称为本地缓存)
二级缓存需要手动开启和配置,他是基于namespace级别的缓存。
为了提扩展性,MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存
13.3、一级缓存
一级缓存也叫本地缓存:SqlSession
- 与数据库同义词会话期间查询到的数据会放在本地缓存中。
- 以后如果需要获取相同的数据,直接从缓存中拿,没有必要再去查询数据;
测试步骤:
- 开启日志!
- 测试在一个Session中查询两次相同的记录
- 查看日志输出
缓存失效的情况:
- 查询不同的东西
- 增删改操作,可能会改变原来的数据,所以必定会刷新缓存!
- 查询不同的Mapper.xml 手动清理缓存!
- sqlsession.clearCache(); //手动清理缓存
import com.newer.dao.UserMapper;
import com.newer.pojo.User;
import com.newer.utils.MyBatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
public class MyTest {
@Test
public void test(){
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.queryUser(1);
System.out.println(user);
//调用修改方法
//mapper.updateUser(new User(2,"aaa","bbbb"));
//手动清理缓存
sqlSession.clearCache();
System.out.println("============================");
User user2 = mapper.queryUser(1);
System.out.println(user2);
System.out.println(user==user2);
sqlSession.close();
}
}
一级缓存默认是开启的,只在一次SqlSession中有效,也就是拿到连接到关闭连接这个区间段!
一级缓存就是一个Map。
13.4、二级缓存
二级缓存也叫全局缓存,一级缓存作用域太低了,所以诞生了二级缓存
基于namespace级别的缓存,一个名称空间,对应一个二级缓存;
工作机制 :
一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中;
如果当前会话关闭了,这个会话对应的一级缓存就没了;但是我们想要的是,会话关闭了,一级缓存中的数据会被保存到二级缓存中;
新的会话查询信息,就可以从二级缓存中获取内容;
不同的mapper查出的数据会放在自己对应的缓存(map)中;
开启全局缓存
<!--显式的开启全局缓存-->
<setting name="cacheEnabled" value="true"/>
在要使用二级缓存的Mapper中开启
<!--在当前Mapper.xml中使用二级缓存-->
<cache/>
测试
问题:我们需要将实体类序列化!否则就会报错 java.io.NotSerializableException: com.rui.pojo.User
<--也可以自定义参数-->
<cache eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.newer.dao.UserMapper">
<cache eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
<select id="queryUser" resultType="User" useCache="true">
select * from mybatis.user where id=#{id}
</select>
<update id="updateUser" parameterType="User">
update mybatis.user set name =#{name},pwd=#{pwd} where id=#{id}
</update>
</mapper>
import com.newer.dao.UserMapper;
import com.newer.pojo.User;
import com.newer.utils.MyBatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
public class MyTest {
@Test
public void test(){
SqlSession sqlSession = MyBatisUtils.getSqlSession();
SqlSession sqlSession2= MyBatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.queryUser(1);
System.out.println(user);
sqlSession.close();
//调用修改方法
//mapper.updateUser(new User(2,"aaa","bbbb"));
//手动清理缓存
//sqlSession.clearCache();
System.out.println("============================");
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = mapper2.queryUser(1);
System.out.println(user2);
System.out.println(user==user2);
sqlSession2.close();
}
}
- 只要开启了二级缓存,在同一个Mapper下就有效
- 所有的数据都会先放在一级缓存中;
- 只有当会话提交,或者关闭的时候,才会提交到二级缓存中!
13.5、自定义缓存-EhCache
EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider。
在程序中使用ehcache,先要导包!
https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache
在mapper中指定使用ehcache缓存实现
<在当前Mapper.xml中使用二级缓存>
<?xml version="1.0" encoding="UTF-8"?>
diskStore:为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置。参数解释如下:
user.home – 用户主目录
user.dir – 用户当前工作目录
java.io.tmpdir – 默认临时文件路径
<diskStore path="java.io.tmpdir/Tmp_EhCache"/>
defaultCache:默认缓存策略,当ehcache找不到定义的缓存时,则使用这个缓存策略。只能定义一个。
name:缓存名称。
maxElementsInMemory:缓存最大数目
maxElementsOnDisk:硬盘最大缓存个数。
eternal:对象是否永久有效,一但设置了,timeout将不起作用。
overflowToDisk:是否保存到磁盘,当系统当机时
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
clearOnFlush:内存数量最大时是否清除。
memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
FIFO,first in first out,这个是大家最熟的,先进先出。
LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
<defaultCache
eternal="false"
maxElementsInMemory="10000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="259200"
memoryStoreEvictionPolicy="LRU"/
<cache
name="cloud_user"
eternal="false"
maxElementsInMemory="5000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="1800"
memoryStoreEvictionPolicy="LRU"/>
SpringMVC框架
SpringMVC框架介绍:
Spring MVC属于SpringFrameWork的后续产品,已经融合在Spring Web Flow里面。Spring 框架提供了构建 Web 应用程序的全功能 MVC 模块。使用 Spring 可插入的 MVC 架构,从而在使用Spring进行WEB开发时,可以选择使用Spring的Spring MVC框架或集成其他MVC开发框架,如Struts1(现在一般不用),Struts 2(一般老项目使用)等等。
---------------------------------上述内容来自百度百科--------------------------------------------------------
- springmvc是spring框架的一个模块,springmvc和spring无需通过中间整合层进行整合。
- springmvc是一个基于mvc的web框架。
- springmvc 表现层:方便前后端数据的传输
- Spring MVC 拥有控制器,作用跟Struts类似,接收外部请求,解析参数传给服务层
- MVC是指,C控制层,M模块层,V显示层这样的设计理念,而SSM框架里面SPRING MVC本身就是MVC框架,作用是帮助(某种意义上也可以 理解为约束)我们要按照MVC这样的设计来开发WEB项目,而另外两个框架spring主要是用作IOC,AOP等其他的一些设计原则,至于mybatis是用来方便操作数据库的,所以他们都在MV里面
Springmvc架构原理解析(结合上图去看)
发起请求到前端控制器(DispatcherServlet)
前端控制器请求HandlerMapping查找 Handler,可以根据xml配置、注解进行查找
处理器映射器HandlerMapping向前端控制器返回Handler
前端控制器调用处理器适配器去执行Handler
处理器适配器去执行Handler
Handler执行完成给适配器返回ModelAndView
处理器适配器向前端控制器返回ModelAndView,ModelAndView是springmvc框架的一个底层对象,包括 Model和view
前端控制器请求视图解析器去进行视图解析,根据逻辑视图名解析成真正的视图(jsp)
视图解析器向前端控制器返回View
前端控制器进行视图渲染,视图渲染将模型数据(在ModelAndView对象中)填充到request域
前端控制器向用户响应结果
组件:
1、前端控制器DispatcherServlet(不需要程序员开发)
作用接收请求,响应结果,相当于转发器,中央处理器。
有了DispatcherServlet减少了其它组件之间的耦合度。
2、处理器映射器HandlerMapping(不需要程序员开发)
作用:根据请求的url查找Handler
3、处理器适配器HandlerAdapter
作用:按照特定规则(HandlerAdapter要求的规则)去执行Handler
4、处理器Handler(需要程序员开发)
注意:编写Handler时按照HandlerAdapter的要求去做,这样适配器才可以去正确执行Handler
5、视图解析器View resolver(不需要程序员开发)
作用:进行视图解析,根据逻辑视图名解析成真正的视图(view)
6、视图View(需要程序员开发jsp)
View是一个接口,实现类支持不同的View类型(jsp、freemarker、pdf…)
SpringMVC常用注解
@Controller
负责注册一个bean 到spring 上下文中
@RequestMapping
注解为控制器指定可以处理哪些 URL 请求
@RequestBody
该注解用于读取Request请求的body部分数据,使用系统默认配置的HttpMessageConverter进行解析,然后把相应的数据绑定到要返回的对象上 ,再把HttpMessageConverter返回的对象数据绑定到 controller中方法的参数上
@ResponseBody
该注解用于将Controller的方法返回的对象,通过适当的HttpMessageConverter转换为指定格式后,写入到Response对象的body数据区
@ModelAttribute
在方法定义上使用 @ModelAttribute 注解:Spring MVC 在调用目标处理方法前,会先逐个调用在方法级上标注了@ModelAttribute 的方法
在方法的入参前使用 @ModelAttribute 注解:可以从隐含对象中获取隐含的模型数据中获取对象,再将请求参数 –绑定到对象中,再传入入参将方法入参对象添加到模型中
@RequestParam
在处理方法入参处使用 @RequestParam 可以把请求参 数传递给请求方法
@PathVariable
绑定 URL 占位符到入参
@ExceptionHandler
注解到方法上,出现异常时会执行该方法
@ControllerAdvice
使一个Contoller成为全局的异常处理类,类中用@ExceptionHandler方法注解的方法可以处理所有Controller发生的异常
支持自动匹配参数与自动装箱
Spring MVC 快速入门
准备工作
第一步:创建项目module,基本信息如图所示:
第二步:添加项目依赖(可以在module创建时,也可以创建后),代码如下:
Spring Web 依赖(提供了spring mvc支持并且会嵌入一个tomcat)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Thymeleaf 依赖(提供了以html作为页面模板进行解析和操作的相关对象)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
第三步:启动项目检测控制台启动状态是否OK
static 目录分析及应用
static 目录为springboot工程创建时添加了web依赖以后自动创建的目录,此目录中可以存储html、css、js、image,这些资源可以在启动服务器以后,直接在浏览器进行访问。例如:
第一步:在static目录下创建一个index.html页面,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>The First Html Page</h1>
</body>
</html>
第二步:启动服务器并直接进行访问测试,如图所示
templates 目录分析及应用
templates 目录为springboot工程创建时添加了thymeleaf依赖以后自动创建的目录,此目录中要存储一些html模板,这个模板页面不能直接通过浏览器url进行访问,需要基于后端控制器,在方法中定义页面响应,例如:
第一步:定义TemplateController及方法,代码如下:
package com.cy.pj.health.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class TemplateController {
@RequestMapping("doTemplateUI")
public String doTemplateUI(){
return "default";
}
}
第二步:在templates目录中定义模板页面default.html,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>The Default Template page</h1>
</body>
</html>
其中,假如default.html要在放在templates子目录中,则还需要在配置文件中配置thymeleaf的前缀,例如:
spring.thymeleaf.prefix=classpath:/templates/module/
第三步:启动服务进行访问测试,如图所示:
第四步:访问过程中的BUG分析
第五步:thymeleaf页面缓存设置
假如我们希望在修改了页面内容以后,不重新启动服务,直接刷新即可看页面改动,可以执行如下操作:
修改配置文件内容,添加如下语句:
spring.thymeleaf.cache=false
修改idea默认配置,允许自动编译,如图所示:
SpringMVC 响应数据处理
ModelAndView 应用
我们有一业务,现在需要将响应数据封装到ModelAndView对象,然后响应到客户端,如何实现呢?
第一步:定义ModelViewController以及方法,代码如下:
package com.cy.pj.module.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class ModelViewController {
@RequestMapping("/doModelAndView")
public String doModelAndView(Model model) {
model.addAttribute("username", "jason");
model.addAttribute("state", true);
return "view";
}
}
第二步:在templates相关目录下定义view.html,并在页面中添加呈现数据的代码,例如:
<div>
<ul>
<li>username:[[${username}]]</li>
<li>state:[[${state}]]</li>
</ul>
</div>
第三步:启动项目进行访问测试,并检测输出结果,例如:
第四步:运行过程中的结果分析
JSON数据响应
我们有一业务不需要页面,只需要将响应数据转换为json,然后响应到客户端,如何实现呢?
第一步:定义ResponseResult对象用于封装响应数据,例如:
package com.cy.pj.module.pojo;
public class ResponseResult {
private Integer code;
private String message;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
第二步:定义JsonObjectController以及方法,代码如下:
package com.cy.pj.module.controller;
@RestController
public class JsonObjectController {
@RequestMapping("/doConvertResponseToJson")
public ResponseResult doConvertResponseToJson(){
ResponseResult rs=new ResponseResult();
rs.setCode(200);
rs.setMessage("OK");
return rs;
}
@RequestMapping("/doConvertMapToJson")
public Map<String,Object> doConvertMapToJson(){
Map<String,Object> map=new HashMap<>();
map.put("username","刘德华");
map.put("state",true);
return map;
}
@RequestMapping("/doPrintJSON")
public void doPrint(HttpServletResponse response)throws Exception{
Map<String,Object> map=new HashMap<>();
map.put("username","刘德华");
map.put("state",true);
//将map中的数据转换为json格式字符串
ObjectMapper om=new ObjectMapper();
String jsonStr=om.writeValueAsString(map);
System.out.println("jsonStr="+jsonStr);
//将字符串响应到客户端
//设置响应数据的编码
response.setCharacterEncoding("utf-8");
//告诉客户端,要向它响应的数据类型为text/html,编码为utf-8.请以这种编码进行数据呈现
response.setContentType("text/html;charset=utf-8");
PrintWriter pw=response.getWriter();
pw.println(jsonStr);
}
}
第三步:启动服务器分别进行访问测试,代码如下:
第四步:启动或访问过程中的问题分析
SpingMVC 请求参数数据处理
我们在执行业务的过程中通常会将一些请求参数传递到服务端,服务端如何获取参数并注入给我们的方法参数的呢?
准备工作
定义一个controller对象,用户处理客户端请求,例如:
package com.cy.pj.module.controller;
import com.cy.pj.module.pojo.RequestParameter;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
public class ParamObjectController {}
直接量方式
在ParamObjectController中添加方法,基于直接量方式接受客户端请求参数,例如:
@GetMapping("/doParam01")
public String doMethodParam(String name){
return "request params "+name;
}
访问时,可以这样传参,例如:
http://localhost/doParam01?name=beijing
POJO对象方式
定义pojo对象,用于接受客户端请求参数,例如:
package com.cy.pj.module.pojo;
public class RequestParameter {
private String name;
//......
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "RequestParam{" +
"name='" + name + ''' +
'}';
}
}
定义Controller方法,方法中使用pojo对象接收方法参数,例如:
@RequestMapping("/doParam02")
public String doMethodParam(RequestParameter param){//pojo对象接收请求参数,pojo对象中需提供与参数名相匹配的set方法
return "request params "+param.toString();
}
启动服务进行访问测试,可以这样传参,例如:
http://localhost/doParam02?name=beijing
Map对象方式
有时候我们不想使用pojo对象接收请求参数,我们可以使用map对象来接收,又该如何实现呢?
定义Controller方法,基于map对象接收请求参数,例如:
@GetMapping("/doParam03")
public String doMethodParam(@RequestParam Map<String,Object> param){
return "request params "+param.toString();
}
其中,map接收请求参数,必须使用@RequestParam对参数进行描述.
启动服务进行访问测试,可以这样传参,例如:
http://localhost/doParam03?name=beijing
总结(Summary)
上述内容对springboot工程下spring mvc技术的应用做了一个入门实现,并结合实际项目中的业务应用,讲解了MVC中请求数据的获取和响应数据处理的一个基本过程.
SpringSecurity(安全管理框架)
对于一个Web项目来说,最重要的不是功能酷不酷炫,而是这个项目安不安全。做过项目的人都知道,一个项目在上线前一定会经过安全漏扫,只有通过安全漏扫后这个项目才能正式上线。
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架,类似的安全框架还有Shiro。
Spring Security主要做两个事情,认证、授权。
一般我们常做的四个功能 : 认证 、授权、注销、记住密码
以下为入门案例:
方便方便展示 引入thymeleaf依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
新建一个登陆页,一个首页,然后几个不同等级的展示页面:
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆页</title>
</head>
<body>
<div>
<form>
<h2>登陆页</h2>
<input type="text" id="username" placeholder="username">
<input type="password" id="password" placeholder="password">
<button type="button">登陆</button>
</form>
</div>
</body>
</html>
效果图:
首页index.html,主要就展示不同等级的按钮,为之后授权做准备
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<div>
<h2>首页</h2>
<a href="/login">登陆</a>
<div style="overflow: hidden">
<div style="float: left;margin-left: 20px">
<h3>level1</h3>
<a href="/level1/1">level-1-1</a>
<hr>
<a href="/level1/2">level-1-2</a>
</div>
<div style="float: left;margin-left: 20px">
<h3>level2</h3>
<a href="/level2/1">level-2-1</a>
<hr>
<a href="/level2/2">level-2-2</a>
</div>
<div style="float: left;margin-left: 20px">
<h3>level3</h3>
<a href="/level3/1">level-3-1</a>
<hr>
<a href="/level3/2">level-3-2</a>
</div>
</div>
</div>
</body>
</html>
效果图:
另外还有几个不同等级的页面
分别在body中写上自己对应的编号。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
level-1-1
</body>
</html>
最后编写一个controller来接收请求:
@Controller
public class RouteController {
@RequestMapping({"/","/index"})
public String index(){
return "index";
}
@RequestMapping("/login")
public String toLogin(){
return "login";
}
@RequestMapping("/level1/{id}")
public String level1(@PathVariable("id")String id){
return "level1/"+id;
}
@RequestMapping("/level2/{id}")
public String level2(@PathVariable("id")String id){
return "level2/"+id;
}
@RequestMapping("/level3/{id}")
public String level3(@PathVariable("id")String id){
return "level3/"+id;
}
}
做好准备工作后我们开始着手学习security的四项功能
1&2. 认证与授权
在上面的这个页面中有两个问题,第一未做登陆认证的处理,第二三个level页面现在都能被所有人访问,未做授权。而上面的这两个问题都可以通过SpringSecurity来解决。
实现SpringSecurity只需要引入spring-boot-starter-security依赖,进行少量的配置,就可以实现强大的安全管理功能。
在正式接触前我们需要记住几个类:
1.WebSecurityConfigurerAdapter :自定义Security策略
2.AuthenticationManagerBuilder:自定义认证策略
3.@EnableWebSecurity :开启WebSecurity模式
我们新建一个config包,创建一个配置类SecurityConfig,继承WebSecurityConfigurerAdapter接口
@EnableWebSecurity//开启WebSecurity模式
public class SecurityConfig extends WebSecurityConfigurerAdapter /*继承自定义security策略*/{
//授权
@Override
protected void configure(HttpSecurity http) throws Exception {
//首页所有人都能访问,level页面只有有权限的人才能访问
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
//没有权限默认跳到登陆页,默认会重定向到/login
http.formLogin();
}
//认证
@Override
protected void configure(AuthenticationManagerBuilder auth/*自定义认证策略*/) throws Exception {
auth.inMemoryAuthentication()
.passwordEncoder(new BCryptPasswordEncoder())
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
.and()
.withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}
}
通过上面的这段代码,实现了授权和认证的功能,其中认证方法在内存中存入的用户信息。正常情况下用户的数据是从数据库中获取,授权功能给不同的页面设置不同的角色。
我们再次打开首页http://localhost:8080/,所有人都有权限看到,当我们点击里面的level1 2 3里的链接时,自动跳转到了http://localhost:8080/login
你会发现这个登陆页面明明不是自己写的,怎么就出现了?其实这就是SpringSecurity所提供的,http.formLogin();这段代码做了对登陆页的处理,没有权限默认跳到登陆页,默认会重定向到/login。
当我们用不同权限的账号登陆时,就能点击不同权限的链接,如果点击权限外的链接时,会报403错误
403错误,可以简单理解为没有权限访问此网站,该状态表示服务器理解了此次请求,但是拒绝执行此次任务。
导致403错误的主要原因
- 你的IP被列入黑名单。
- 你在一定时间内过多地访问此网站(一般是用采集程序),被防火墙拒绝访问了。
- 网站域名解析到了空间,但空间未绑定此域名。
- 你的网页脚本文件在当前目录下没有执行权限。
- 在不允许写/创建文件的目录中执行了创建/写文件操作。
- 以http方式访问需要ssl连接的网址。
- 浏览器不支持SSL 128时访问SSL 128的连接。
- 在身份验证的过程中输入了错误的密码。
- DNS解析错误,手动更改DNS服务器地址。
- 连接的用户过多,可以过后再试。
- 服务器繁忙,同一IP地址发送请求过多,遭到服务器智能屏蔽。
3. 注销的操作
既然有认证和授权,那么一定免不了注销的功能,SpringSecurity实现注销很简单,你只需要在SecurityConfig类的授权代码里增加一条代码。
//登出
http.logout();
然后在前端index.html代码中增加一个注销的标签
<a href="/logout">注销</a>
点击首页的注销按钮后就会跳转到SpringSecurity的注销提示界面
点击logout按钮后自动退出到登陆页面,如果你希望注销后还是回到首页的话,可以将注销代码修改成下面的方式:
http.logout().logoutSuccessUrl("/");
4. 记住密码功能
一般情况下登陆页面肯定会有一个记住密码的功能,这个功能其实就是在本地生成一个cookie,用原生的java实现虽然不难,但是也需要一定的代码。而使用SpringSecurity只需要一行:
http.rememberMe();
再次进入登陆页面后,就可以看到增加了一行记住密码的选项
5. 解决页面英文问题(其实就是换一个登陆页面)
技术要点
- 编写登录界面(略)
- 配置页面和登录路径等 写在security的config类里
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
http.formLogin()
.loginPage("/login.html")// 自定义登录页面路径
.loginProcessingUrl("/authentication/form")// 自定义页面的登录路径,
//注意要与登录页面的action值一致,
//<form action="/authentication/form" method="post">
.and()
.authorizeRequests() // 定义哪些URL需要被保护、哪些不需要被保护
.antMatchers("/login.html").permitAll()// 设置所有人都可以访问登录页面
.anyRequest().authenticated() // 除了以上的请求外都需要身份验证
;
http.csrf().disable();// 禁用跨站攻击
}