面试官好:
很荣幸有机会参加xxx公司面试。我此次应聘的职位是*****。下面我请允许我自我介绍一下。
我叫***,今年***,来自******,目前是********大四学生,对软件开发怀有浓烈的兴趣,对Java语言尤其熟悉,有良好的Java编程知识和面向对象编程思想,熟悉常用的数据结构和算法,熟悉 MVC 开发模式和前后端分离框架,熟练使用 Maven、Git 管理工具,熟悉 Web 框架和 J2EE 开发技术,熟悉 Linux 操作系统和常用的操作指令、熟悉 JVM 内存模型、类加载 。目前已经编写了几个项目,拥有一定的项目经验,获奖情况 。
我觉得自己为人踏实、勤奋、好学,严格要求自己,工作积极,一丝不苟,对新事物有较强的接受能力。对自己已掌握的技术敢于创新,不畏难题,勇于迎接新挑战!在平时生活中喜欢将自己学到的一些新技术写成博客与他人分享.也会经常阅读他人的技术性文章.来提升自己的能力,丰富自己的想法。能够妥善的处理周围的人际关系,团结同事,并具有极强的团队合作精神。我能够通过自己的努力在为企业服务的过程中实现自身价值。
专业书籍:深入理解JVM、鸟哥的Linux私房菜、大话设计模式
操作系统
计算机网络
数据结构算法
java语言(javaSE,javaEE,框架,JVM)
数据库
项目
linux
javaSE内容作为java开发方向的基础知识,一定要扎实,对知识点要有自己深入的理解.一旦有磕绊对面试官的印象将大打折扣.
Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
Lambda表达式
本质上是一段匿名内部类,也可以是一段可以传递的代码。
函数式接口
定义了一个抽象方法的接口,就是函数式接口,并且还提供了注解:@Functionallnterface
*方法引用和构造器调用
Stream API
接口中的默认方法和静态方法
在接口中可以使用default和static关键字来修饰接口中定义的普通方法
在JDK1.8中很多接口会新增方法,为了保证1.8向下兼容,1.7版本中的接口实现类不用每个都重新实现新添加的接口方法,引入了default默认实现,static的用法是直接用接口名去调方法即可。当一个类继承父类又实现接口时,若后两者方法名相同,则优先继承父类中的同名方法,即“类优先”,如果实现两个同名方法的接口,则要求实现类必须手动声明默认实现哪个接口中的方法。
新时间日期API
新的日期API都是不可变的,更使用于多线程的使用环境中。
简单性、面向对象、分布式、健壮性、安全性、体系结构中立、可移植性、跨平台性、解释型、高性能、多线程、动态性。
不要小看此问题,他会引出一连串问题:
例如:如何实现跨平台,JVM作用,面向对象的理解,多线程…
为不同的平台的提供不同的JVM(虚拟机),将字节码翻译/编译成不同平台支持的指令.
JDK:Java开发工具包 包含jre
JRE:Java运行环境(核心工具类) 包含JVM
JVM:Java虚拟机
面向过程分析出解决问题所需的步骤,然后将步骤一步步实现。
比如将大象装进冰箱,分为三步,打开冰箱,放入大象,关上冰箱。
面向过程以分类的方法进行思考和解决问题,面向对象的思维方式适合于处理复杂的问题。面向对象无法取代面向过程,两者是相辅相成的,面向对象关注于宏观上把握事物之间的关系,在具体到如何实现某个细节时,任然采用面向过程的思维方式。
比如将大象装进冰箱,首先设计三个类冰箱类(门类(包含开门,关门两个方法)),人类(包含操作方法),大象类(进入冰箱功能)
接着按照面向过程的思想调用三个类,解决问题。
类的构成{
成员变量
成员方法
构造方法
代码块
内部类
}
1、易维护
采用面向对象思想设计的结构,可读性高,由于继承的存在,即使改变需求,那么维护也只是在局部模块,所以维护起来是非常方便和较低成本的。
2、质量高
在设计时,可重用现有的,在以前的项目的领域中已被测试过的类使系统满足业务需求并具有较高的质量。
3、效率高
在软件开发时,根据设计的需要对现实世界的事物进行抽象,产生类。使用这样的方法解决问题,接近于日常生活和自然的思考方式,势必提高软件开发的效率和质量。
4、易扩展
由于继承、封装、多态的特性,自然设计出高内聚、低耦合的系统结构,使得系统更灵活、更容易扩展,而且成本较低
封装
隐藏类的信息,不向外界暴露,隐藏实现细节,向外提供特定的方法访问.
成员变量私有化,单例模式是封装的一种体现
继承
在原有类的基础上派生出新的类,新的类具有原有类的所有非私有属性和非私有方法,并扩展新的能力。
优点:可扩展性,代码可重用性
多态
实现多态的三个必要条件:继承,重写,父类的引用指向子类对象
同一事物不同时刻的不同状态
程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定。
多态
同一事物,在不同时候表现不同状态
前提: 继承,方法重写,父类引用指向子类对象
优点:提升程序可维护性,可扩展性 集合 异常 String.valueOf(Object obj) jdbc 接口 mysql servlet Httpetservletrequest
缺点:不能调用子类特有的方法 解决方法:向下转型
多态满足面向对象设计原则中的里氏替换原则.
包: 避免类重名
管理类
访问权限控制
public 修饰类,变量,方法 在任何地方都可以访问
private 修饰变量,方法,内部类 只能在本类中访问
proected 修饰变量,方法 在同包中,不同包的子类中访问
默认 修饰类,变量,方法 在同包类中可以访问
继承
优点:
代码复用,易于扩展,维护
缺点:
打破了父类的封装性
父类和子类联系紧密,耦合度高 , 父类一旦发生了变化,子类可能会受影响
结合面向对象设计7大原则来讲. 单一职责原则,组合 聚合复用原则.
应用场景:
多种相同类型有共性属性行为时使用继承
new Car(int price);
new Car();
基类静态代码块,基类静态成员字段(并列优先级,按照代码中出现的先后顺序执行,且只有第一次加载时执行)——>派生类静态代码块,派生类静态成员字段(并列优先级,按照代码中出现的先后顺序执行,且只有第一次加载时执行)——>基类普通代码块,基类普通成员字段(并列优点级,按代码中出现先后顺序执行)——>基类构造函数——>派生类普通代码块,派生类普通成员字段(并列优点级,按代码中出现先后顺序执行)——>派生类构造函数.
静态变量->静态代码块->成员变量->构造代码块->构造方法->普通代码块
new
反射(Class类的newInstance方法、使用java.lang.relect.Constructor类的newInstance方法) Class.forName(“com.ff.Car”)
Class类的newInstance方法
Student student2 = (Student)Class.forName(className).newInstance();
// 或者:
Student stu = Student.class.newInstance();
Constructor类的newInstance方法
Constructor constructor = Student.class.getInstance();
Student stu = constructor.newInstance();
区别:
两种newInstance方法区别:
对象反序列化 IO
对象clone()
什么是对象克隆?
创建一个新的对象,将原对象中的数据复制到新对象中
什么时候用?
将请求中接收到数据的对象信息, 拷贝到向dao层传入的参数对象中.
浅克隆?
基本类属于浅克隆,
只将对象中关联的对象的引用地址复制属于浅克隆
深克隆?
只将对象中关联的对象也进行了克隆,多级克隆.
解决多级克隆: 在关联的对象中 继续重写克隆方法
通过对象序列化,反序列化.
特征
方法名与类名相同,没有返回值,不可以被void修饰
一个类中可以有多个构造方法, 默认有一个无参的构造(隐式),一旦显示的定义有参的构造方法,默认的就失效,
一般情况,显示的定义无参的构造方法
作用: 初始化创建对象的成员变量
new Car(int age,int a) Car() age=0; String name=null
方法重载,方法重写
在一个类中,有多个名称相同的方法,参数不同.
在继承关系中,父类实现不能满足子类的需求,在子类中重写(覆盖,覆写)父类方法, 功能的扩展,super.父类
为什么构造方法不能重写
构造方法名与当前类名相同, 重写要求与父类方法结构一致.
在子类的构造方法中调用父类的构造方法, 必须放在子类构造方法的第一行.
基本类型
何为引用类型
针对类类型(复合类型), 变量 指向堆中一个对象
值传递和引用传递
基本类型,传值 10
int a = 10;
int b = a; //b=10
引用类型
引用传递, 传递的对象的地址值.
本质都是传值:
Pass By Value 和 Pass By Reference
细化为:值传递(基本类型),引用传递(引用类型 不是对象本身,只是传递对象引用地址)
表示静态(指在内存只有一份的)
修饰内部类,成员变量,成员方法,代码块
特点:
**注意:**在static方法内部只能访问类的static属性,不能访问非static属性
作用:抽象,功能定义 开闭原则,里氏替换原则都依赖接口,抽象类.
相同点:
都可以包含抽象方法, 不能被实例化.抽象类构造方法和静态方法不可以修饰为abstract
不同点:
抽象类中可以包含成员变量 接口中只能包含静态常量
抽象类中可以包含成员方法 接口中可以包含抽象方法,默认(子类调用),静态(接口调用静态方法jdk8后添加)
抽象类可以包含构造方法,初始化父类成员 接口中不能有构造方法
抽象类中的抽象方法的访问类型可以是public ,protected和默认类型 接口中每一个方法也是隐式抽象的,默认为public abstract 。接口中声明的属性默认为 public static final 的;
从jdk1.8开始,接口中的方法不再是只能有抽象方法(普通方法会被隐式地指定为public abstract方法),他还可以有静态方法和default方法。并且静态方法与default方法可以有方法体!
Java语言不同于C++语言,是一种单根继承结构语言,也就是说,Java中所有的类都有一个共同的祖先。这个祖先就是Object类。
equals() hashCode wait notify finalize() clone()
如图可知,Object类有12个成员方法,按照用途可以分为以下几种
1,构造函数
2,hashCode和equale函数用来判断对象是否相同,
3,wait(),wait(long),wait(long,int),notify(),notifyAll()
4,toString()和getClass
5,clone()函数的用途是用来另存一个当前存在的对象。
6,finalize()用于在垃圾回收
基本原理
java中的基本数据类型判断是否相等,直接使用"=="就行了,相等返回true,否则,返回false。
java中的引用类型的对象比较变态,假设有两个引用对象obj1,obj2,obj1==obj2 判断是obj1,obj2这两个引用变量是否相等,即它们所指向的对象是否为同一个对象。言外之意就是要求两个变量所指内存地址相等的时候,才能返回true,每个对象都有自己的一块内存,因此必须指向同一个对象才返回ture。
遇到的问题
如果想要自定义两个对象(不是一个对象,即这两个对象分别有自己的一块内存)是否相等的规则,那么必须在对象的类定义中重写equals()方法,如果不重写equals()方法的话,默认的比较方式是比较两个对象是否为同一个对象。
在Java API中,有些类重写了equals()方法,它们的比较规则是:当且仅当该equals方法参数不是 null,两个变量的类型、内容都相同,则比较结果为true。这些类包括:String、Double、Float、Long、Integer、Short、Byte、Boolean、BigDecimal、BigInteger等等,太多太多了,但是常见的就这些了,具体可以查看API中类的equals()方法,就知道了。
重写equals的原则
一旦创建时赋值后,值不可变.不可以被继承.一旦修改会重新创建应该新的对象
final class String {
final char[] value;
}
创建对象时,先在常量池检查有没有相同的,如果没有就在堆中创建一个字符串对象,值在常量池中,第二次创建时,常量池已经存在,直接让引用变量指向已有的对象地址
不能改变指的是对象内的成员变量值
private final char[] value;
1.便于实现字符串常量池
在Java中,由于会大量的使用String常量,如果每一次声明一个String都创建一个String对象,那将会造成极大的空间资源的浪费。Java提出了String pool的概念,在堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。
2.使多线程安全
看下面这个场景,一个函数appendStr()在不可变的String参数后面加上一段“bbb”后返回。appendSb()负责在可变的StringBuilder后面加"bbb"。
public class test {
// 不可变的String
public static String appendStr(String s) {
s += “bbb”;
return s;
}
// 可变的StringBuilder
public static StringBuilder appendSb(StringBuilder sb) {
return sb.append(“bbb”);
}
public static void main(String[] args) {
String s = new String(“aaa”);
String ns = test.appendStr(s);//aaabbb
System.out.println(“String aaa>>>” + s.toString());//aaa
// StringBuilder做参数
StringBuilder sb = new StringBuilder(“aaa”);
StringBuilder nsb = test.appendSb(sb);
System.out.println(“StringBuilder aaa >>>” + sb.toString());
}
}
如果程序员不小心像上面例子里,直接在传进来的参数上加上“bbb”.因为Java对象参数传的是引用,所有可变的StringBuffer参数就被改变了。可以看到变量sb在Test.appendSb(sb)操作之后,就变成了“aaabbb”。
有的时候这可能不是程序员的本意。所以String不可变的安全性就体现在这里。
在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全。
3.避免安全问题
在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。
4.加快字符串处理速度
由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。
在String类的定义中有如下代码:
private int hash;//用来缓存HashCode
总体来说,String不可变的原因要包括 设计考虑,效率优化,以及安全性这三大方面。
//通过反射改变String底层数组值 public static void main(String[] args) throws Exception { String s = "Hello World"; System.out.println("s = " + s); //Hello World //获取String类中的value字段 Field valueFieldOfString = String.class.getDeclaredField("value"); //改变value属性的访问权限 valueFieldOfString.setAccessible(true); //获取s对象上的value属性的值 char[] value = (char[]) valueFieldOfString.get(s); //改变value所引用的数组中的第5个字符 value[5] = '_'; System.out.println("s = " + s); //Hello_World System.out.println(s);//Hello_World }
String对象创建方式
Java中创建String的两种方式:
String s1 = "abc"; String s2 = new String("abc");
方法1中,先在常量池中查找有没有"abc"这个字符串对象存在,
如果存在就把常量池中的指向这个字符串对象;
方法2中,不论常量池中中是否已经存在"abc"这个字符串对象,都会新建一个对象。String strA = " abc " ; String strB = " abc " ; String strAA = new String( " abc " ); String strBB = new String( " abc " ); System.out.println(strA == strB);//true System.out.println(strAA == strBB);// false
这句代码的字面量在哪里? String str = new (“hello”);
常量池
1.
String s = new String("abc")创建了几个对象
情况1: 创建两个对象
String s = new String("abc");
直接new对象,此前在字符串常量池中不存在"abc",此种情况在堆中创建一个对象,然后在字符串常量池中创建一个对象
情况2: 创建一个对象
String s = "abc";
String s1 = new String("abc");
因为字符串常量池中已存在"abc",只需要在堆中创建即可.
2.
创建了一个对象,底层优化创建了一个StringBuilder对象,append()追多个字符串对象,只产生一个对象.
String st1 = "a" + "b" + "c";//只创建了一个字符串对象
String st2 = "abc";
System.out.println(st1 == st2);//true
3.
String st1 = "ab";
String st2 = "abc";
String st3 = st1 + "c";//一旦发生拼接中有变量,会重新创建一个字符串对象
System.out.println(st2 == st3);//false
构造方法
String()
String("")
String(byte[] b ,0,length,chatset)
String(char[] b ,0,length,chatset)
判断功能
equals, compareTo, contains, isempty startWith
获取功能
length indexOf charAt substring
转换
split valueOf() tocharArray
正则表达式
String 值不可以改变
StringBuffer 值可变 线程安全的
StringBuilder 值可变 线程不安全
StringBuilder和StringBuffer的初始容量都是16,程序猿尽量手动设置初始值。以避免多次扩容所带来的性能问题,默认数组容量扩充为原数组容量的2倍+2。假设这个新容量仍然小于预定的最小值(minimumCapacity),那么就将新容量定为(minimumCapacity),最后推断是否溢出,若溢出,则将容量定为整型的最大值0x7fffffff。
因为基本类型不是对象,所以每个基本类提供了一个类包装基本类.使用面向对象方式操作。基本数据类型放在堆栈中,对象放在堆中
最大值,最小值,转换
Integer
int value
自动装箱 Integer a = 10 valueOf() 注意 -128 +127 之间会从缓存池中直接获取
==
自动拆箱 int b = a; intvalue()
Object
Arrays 排序(冒泡,选择,插入 ,快排 , 堆排) ,二分搜索
日期
运行时异常,可以使用异常处理机制处理 不是错误(OOM)
Throwable
Error Exception
RunTimeException
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HB0SnuyY-1634353769845)(D:\桌面\笔记\1632106539174.png)]
运行时异常
编译期(检查)异常 编写代码时,需要处理
NullPointerException 空指针异常
ClassNotFoundException 指定类不存在
NumberFormatException 字符串转换为数字异常
IndexOutOfBoundsException 数组下标越界异常
ClassCastException 数据类型转换异常
FileNotFoundException 文件未找到异常
NoSuchMethodException 方法不存在异常
IOException IO 异常
SocketException Socket 异常
try{ 可能出现异常的代码
return
}
catch(异常类型 ){
处理异常
return
}
finally{
无论是否出现异常,都会执行
return
}
try{ 可能出现异常的代码
}finally{
}
throws,定义一个方法的时候可以使用throws关键字声明,表示此方法不处理异常,而交给方法调用处进行处理.
基本语法
public void test() throws 异常1,异常2,异常3{
}
注意:
throw关键字用于显示抛出异常,抛出的时候是一个异常类的实例化对象。
在异常处理中,try语句要捕获的是一个异常对象,那么此异常对象也可以自己抛出.
语法:throw new 异常类构造方法
public static void someMethod() {
if (1==1) {
throw new RuntimeException("错误原因");
}
}
根据业务需要,自定义不同的异常类.
public static void main(String[] args) { try { test(101); } catch (ScoreException e) { e.printStackTrace(); System.out.println(e.getMessage()); }
} public static int test(int s)throws ScoreException{ if(s<0||s>100){ //throw new RuntimeException("分数不合法"); //在程序中主动抛出异常对象,在构造方法中可以传入异常原因 throw new ScoreException("分数不合法"); } return s; }
}
/*当分数不满足条件时,抛出此对象.*/ public class ScoreException extends Exception{ public ScoreException() { super(); } public ScoreException(String message) { super(message); } }
集合
为什么需要集合
程序中的数据长度可变.
数据结构不同.
许多集合类
单列集合 双列集合
集合体系
各集合类的底层结构实现
List: 可以重复,有序的
ArrayList: 底层Object数组实现,查询快,(数组中间)增删慢, 浪费空间
第一次添加时,初始化一个默认容量为10,jdk8的时候初始化为{},只有在add元素时才创建10个容量
创建时,通过构造方法指定初始容量
扩容: 添加满了之后,会发生扩容 grow() 扩容为原来的1.5倍
LinkedList: 底层是双向链表, 查询慢,增删快 实现队列,栈
没有扩容机制,就是一直在前面或者后面新增就好。
什么时候用ArrayList,LinkedList
Vector: 底层是数组 ,线程安全的.第一次添加时,初始化一个默认容量为10,添加满了之后,会发生扩容 grow() ,扩容为原来的2倍
集合遍历
for 支持增删(注意索引变化) 增强for(只能删除一个,报错 使用break)
迭代器(里面有一个计数器)
iterator();从前向后 listIterator(arrayList.size()); 从指定的位置开始遍历 从后向前listIterator.hasPrevious() previous()
stream 流式
Set 不能重复,元素无序
HashSet:底层是hash表(无序,唯一)。继承hashmap双列,value值默认object
如何保证元素唯一性?
根据key计算hash值,确定在hash表中的索引,若该位置未存在元素,则插入,反之,通过equals方法对key的真实内容进行比较,如果比较结果是key值已经存在于表中,那么就不对其进行插入,而是返回原本的key值对应的value值。如果这个表中的元素不存在key值相同的元素,那么就将(hash, key, value, i)插入链表的下一个位置。
LinkedHashSet:底层链表和哈希表。(FIFO插入有序,唯一)
链表保证元素有序
哈希表保证元素唯一
TreeSet:底层红黑树。(唯一,有序)
HashSet -->HashMap, TreeSet—>TreeMap
hashCode() equals() “”.hashCode()
解决hash冲突:
保证效率,又保证安全
线程不安全
https://blog.csdn.net/qq_26542493/article/details/105482732
哈希表+链表+红黑树
允许null(key和value都允许)
无序
哈希值的使用,HashMap的父类是AbstractMap。
而HashMap重新计算hash值
创HashMap是默认哈希表容量是16,加载因子是0.75(当元素个数超过容量长度的0.75倍时扩容),扩容为原来的2倍 也可以指定长度
put(k,value)
添加时首先是通过k的哈希值,再通过哈希函数计算位置,
位置上如果没有元素添加在链表的头结点,如果有插入到链表的下一个节点.
链表的长度==8,转为红黑树
扩展 负载因子 默认为0.75
1 效率低
0.5 浪费空间
扩容为原来的2倍
效率高
减少哈希重冲突
源码:
常用变量
//默认的初始化容量为16,必须是2的n次幂 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //最大容量为 2^30 static final int MAXIMUM_CAPACITY = 1 << 30; //默认的加载因子0.75,乘以数组容量得到的值,用来表示元素个数达到多少时,需要扩容。 //为什么设置 0.75 这个值呢,简单来说就是时间和空间的权衡。 //若小于0.75如0.5,则数组长度达到一半大小就需要扩容,空间使用率大大降低, //若大于0.75如0.8,则会增大hash冲突的概率,影响查询效率。 static final float DEFAULT_LOAD_FACTOR = 0.75f; //刚才提到了当链表长度过长时,会有一个阈值,超过这个阈值8就会转化为红黑树 static final int TREEIFY_THRESHOLD = 8; //当红黑树上的元素个数,减少到6个时,就退化为链表 static final int UNTREEIFY_THRESHOLD = 6; //链表转化为红黑树,除了有阈值的限制,还有另外一个限制,需要数组容量至少达到64,才会树化。 //这是为了避免,数组扩容和树化阈值之间的冲突。 static final int MIN_TREEIFY_CAPACITY = 64; //存放所有Node节点的数组 transient Node<K,V>[] table; //存放所有的键值对 transient Set<Map.Entry<K,V>> entrySet; //map中的实际键值对个数,即数组中元素个数 transient int size; //每次结构改变时,都会自增,fail-fast机制,这是一种错误检测机制。 //当迭代集合的时候,如果结构发生改变,则会发生 fail-fast,抛出异常。 transient int modCount; //数组扩容阈值 int threshold; //加载因子 final float loadFactor; //普通单向链表节点类 static class Node<K,V> implements Map.Entry<K,V> { //key的hash值,put和get的时候都需要用到它来确定元素在数组中的位置 final int hash; final K key; V value; //指向单链表的下一个节点 Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } } //转化为红黑树的节点类 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { //当前节点的父节点 TreeNode<K,V> parent; //左孩子节点 TreeNode<K,V> left; //右孩子节点 TreeNode<K,V> right; //指向前一个节点 TreeNode<K,V> prev; // needed to unlink next upon deletion //当前节点是红色或者黑色的标识 boolean red; TreeNode(int hash, K key, V val, Node<K,V> next) { super(hash, key, val, next); } }
构造函数
//默认无参构造,指定一个默认的加载因子 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; } //可指定容量的有参构造,但是需要注意当前我们指定的容量并不一定就是实际的容量,下面会说 public HashMap(int initialCapacity) { //同样使用默认加载因子 this(initialCapacity, DEFAULT_LOAD_FACTOR); } //可指定容量和加载因子,但是笔者不建议自己手动指定非0.75的加载因子 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; //这里就是把我们指定的容量改为一个大于它的的最小的2次幂值,如传过来的容量是14,则返回16 //注意这里,按理说返回的值应该赋值给 capacity,即保证数组容量总是2的n次幂,为什么这里赋值给了 threshold 呢? //先卖个关子,等到 resize 的时候再说 this.threshold = tableSizeFor(initialCapacity); } //可传入一个已有的map public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); } //把传入的map里边的元素都加载到当前map final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { if (table == null) { // pre-size float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) threshold = tableSizeFor(t); } else if (s > threshold) resize(); for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); //put方法的具体实现,后边讲 putVal(hash(key), key, value, false, evict); } } }
hash()计算
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
//h原来的值
0110 1101 0110 1111 0000 0011 0100 0111
高16位值和当前h的低16位进行了混合,这样可以尽量保留高16位的特征,从而降低哈希碰撞的概率。
为什么高低位异或运算可以减少哈希碰撞
//例如我有另外一个h2,和原来的 h相比较,高16位有很大的不同,但是低16位相似度很高,甚至相同的话。
//原h值
0110 1101 0110 1111 0110 1110 0010 1000
//另外一个h2值
0100 0101 1110 1011 0110 0110 0010 1000
// n -1 ,即 15 的二进制
0000 0000 0000 0000 0000 0000 0000 1111
//可以发现 h2 和 h 的高位不相同,但是低位相似度非常高。
//他们分别和 n -1 进行与运算时,得到的结果却是相同的。(此处n假设为16)
//因为 n-1 的高16位都是0,不管 h 的高 16 位是什么,与运算之后,都不影响最终结果,高位一定全是 0
//因此,哈希碰撞的概率就大大增加了,并且 h 的高16 位特征全都丢失了。
与运算,结果会趋向于0;或运算,结果会趋向于1;而只有异或运算,0和1的比例可以达到1:1的平衡状态。
HashTable结构
线程安全(所有public方法声明中都有synchronized)
不允许null(key和value都不允许)
无序
Hashtable的父类是Dictionary
扩容机制:默认初始容量11,扩容加载因子0.75,当超出默认长度(int)(11*0.75)=8时,扩容为oldx2+1,新容量为原容量的2倍+1
哈希值的使用,HashTable直接使用对象的hashCode。
一个二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因此,红黑树是一种弱平衡二叉树(所以在相同节点情况下,AVL树的高度低于红黑树),相对与要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,就用红黑树。
(3)应用
1,广泛用于C ++的STL中,地图和集都是用红黑树实现的;
2,着名的Linux的的进程调度完全公平调度程序,用红黑树管理进程控制块,进程的虚拟内存区域都存储在一颗红黑树上,每个虚拟地址区域都对应红黑树的一个节点,左指针指向相邻的地址虚拟存储区域,右指针指向相邻的高地址虚拟地址空间;
3,IO多路复用的epoll的的的实现采用红黑树组织管理的的的sockfd,以支持快速的增删改查;
4,Nginx的的的中用红黑树管理定时器,因为红黑树是有序的,可以很快的得到距离当前最小的定时器;
5,Java的的的中TreeMap中的中的实现;
AVL树是带有平衡条件的二叉查找树,一般是用平衡因子差值判断是否平衡并通过旋转来实现平衡,左右字数树高不超过1,和红黑相比,AVL树是严格的平衡二叉树,平衡条件必须满足(所有的节点的左右字数高度差的绝对值不超过1)。不管我们是执行插入还是删除操作,只要不满足条件,就要通过旋转来保持平衡,而旋转是非常耗时的,所以AVL树适合用于插入与删除次数比较少,但查找多的情况。
二叉平衡树有以下规则:
相同点:
hashmap和Hashtable都实现了map、Cloneable(可克隆)、Serializable(可序列化)这三个接口
不同点:
concurrenthashmap:在进行读操作时不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。只能保证单个方法是同步的,不能保证先读后写的原子性。其余类似hashmap
jdk5使用分段锁,JDK8放弃了分段锁采用了Node锁,减低锁的粒度,提高性能,并使用CAS操作来确保Node的一些操作的原子性,取代了锁
对于多线程的操作,介于HashMap与HashTable之间
弃用分段锁原因
**操作:**put时首先通过hash找到对应链表后,查看是否是第一个Node,如果是,直接用CAS原则插入,无需加锁。如果不是链表的第一个Node,则直接用链表第一个Node加锁,这里加的锁是synchronized。
TreeSet TreeMap 底层是红黑树结构 根据内容的自然顺序排序
set遍历
不能用for循环遍历,
map遍历
keySet
EntrySet
Collections类 :collection工具类
类型参数化
一个File的对象表示硬盘上的一个文件/目录
createNewFile()
mkdir()
mkdirs()
delete()
判断
字节流
输入流,输出流
InputStream OutputStream
字符流
输入流,输出流
FIleInputStream FileOutputStream 直接来操作文件 read() read(byte[] b)
BufferedInputStream 缓存功能 提示效率
对象的寿命通常随着生成该对象的程序的终止而终止。
有时候,可能需要将对象的状态保存下来,在需要时再将对象恢复。
对象的输出流将指定的对象写入到文件的过程,就是将对象序列化的过程,对象的输入流将指定序列化好的文件读出来的过程,就是对象反序列化的过程。既然对象的输出流将对象写入到文件中称之为对象的序列化,所以必须要实现Serializable接口。
Serializable接口中没有任何方法。当一个类声明实现Serializable接口后,表明该类可被序列化。
在类中可以生成一个编号
private static final long serialVersionUID = -5974713180104013488L; 随机生成 唯一的
可以显示的声明创建序列号id,如果实现接口不显式的生成序列化版本号,类的信息一旦改变,序列化id也会随之改变。
transient 所修饰的属性不被序列化到文件中。
serialVersionUID 用来表明实现序列化类的不同版本间的兼容性。某个类在与之对应的对象已经序列化出去后做了修改,该对象依然可以被正确反序列化
ObjectInputStream ObjectOutputStream 反序列化 一种创建对象方式 深克隆
tomcat 缓存session对象 sessions.ser
如果一个数据在最近一段时间没有被访问到, 那么在讲台它被访问到的概率也会比较小, 所以当内存空间有限时, 应该把这部分数据淘汰掉。
程序:是一段静态的代码。
进程:是正在执行的程序,是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间。一个进程中可以启动多个线程。
线程:进程中的一个执行流程,是进程内部最小执行单元,没有主机的虚拟地址空间,与进程内的其他线程一起共享分配给该进程的所有资源。
线程包含:
继承Thread类的方式
线程代码存放Thread子类run方法中(没有返回值)
实现Runnable接口方式
线程代码存在接口的子类run方法中(重写run方法)(没有返回值)
**好处:**避免了单继承的局限,允许多继承。适合多个相同的线程来处理同一份资源
实现Callable接口方式
与Runnable相比:
优点:
缺点:
并发:多个任务在同一个CPU核上,按细分时间片轮流执行,从逻辑上来看是同时执行的。在单核CPU下,线程实际还是串行执行的。
并行:单位时间内,多个处理器或多核处理器同时处理多个任务。
可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。
为了避免处理器停顿下来等待向内存写入数据而产生的延迟,处理器使用写缓冲区来临时保存向内存写入的数据。写缓冲区合并对同一内存地址的多次写,并以批处理的方式刷新,也就是说写缓冲区不会即使将数据刷新到主内存中。缓存不能及时刷新导致了可见性问题。
原子性:一个或多个操作在cpu执行的过程中不被中断的特性,称为原子性。
原子性是拒绝多线程交叉操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。
线程切换导致了原子性问题
有序性:程序按照代码的先后顺序执行。
编译器为了优化性能,有时候会改变程序中语句的先后顺序。
总结: 缓存导致的可见性问题,线程切换带来的原子性问题,编译优化带来的有序性问题。
锁
synchronized 是独占锁/排他锁(就是有你没我的意思),但是注意!synchronized 并不能改变 CPU 时间片切换的特点,只是当其他线程要访问这个 资源时,发现锁还未释放,所以只能在外面等待。
synchronized 一定能保证原子性,因为被 synchronized 修饰某段代码后,无论是单核 CPU 还是多核 CPU,只有一个线程能够执行该代码,所以一定能保证原子操作.
synchronized 也能够保证可见性和有序性。
JUC-原子变量
属于java.util.concurrent.atomic
原子类的原子性是通过volatile+CAS实现原子操作的。
AtomicInteger类中的value是又volatile关键字修饰的,保证了value的内存可见性。
低并发情况下:使用AtomicInteger
cas:比较并交换,该算法是硬件对并发操作的支持。
cas是乐观锁的一种实现方式.·,是一种轻量级的锁机制。
过程:每次判断我的预期值和内存中的值是否相同,如果不相同则说明该内存值已经被其他线程更新过了,因此需要拿到最新值作为预期值,重新判断。而该线程不断的循环判断是否内存值已经被其他线程更新过了,这就是自旋的思想。
优点:效率高于加锁
缺点:
不断的自旋,会导致cpu消耗。
ABA问题:某个线程将内存值由A改为了B,再由B改为了A。当另一个线程使用预估值去判断时,预期值于内存值相同,误以为该变量没有被修改过而导致的问题。
解决方法:通过使用类似于添加版本号的方式。如原先的内存值为(A,1),线程将(A,1)修改为了(B,2),再由(B,2)修改为(A,3)。此时另一个线程使用预期值(A,1)与内存值(A,3)进行比较,只需要比较版本号 1 和 3,即可发现该内存中的数据被更新过了。
死锁根本原因:
是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环;
一个线程T1持有锁L1,并且申请获得锁L2;而另一个线程T2持有锁L2,并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞了。导致了死锁。
java 死锁产生的四个必要条件:
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
既然我们知道了产生死锁可能性的原因,那么就可以在编码时进行规避。
如何避免
1、避免嵌套锁
2、只锁需要的部分
3、避免无限期等待
java中的线程分为两类:用户线程和守护线程
只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作。只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
守护线程的作用是为其他线程的运行是提供便利服务,守护线程最典型的应用就是GC(垃圾回收器)
注意:设置线程为守护线程必须在启动线程之前,否则会跑出一个IllegalThreadStateException异常。
线程与线程之间不是相互独立的个体,它们彼此之间需要相互通信和协作,最典型的例子就是生产者-消费者问题.
不同线程之间无法直接访问对方工作内存中的变量,线程间通信有两种方式:共享内存、消息传递、IPC通信、线程上下文、socket。java采用的是共享内存。
使用volatile关键字
一旦一个共享变量被volatile修饰后:
使用Object类的wait() 和 notify() 方法
使用JUC工具类 CountDownLatch
使用 ReentrantLock 结合 Condition
基本LockSupport实现线程间的阻塞和唤醒
线程间的通信方式: wait/notify机制
join()
方法是Thread
类中的一个方法,该方法的定义是等待该线程终止。其实就是join()
方法将挂起调用线程的执行,直到被调用的对象完成它的执行。
/** *等待该线程终止的时间最长为 millis 毫秒。超时为 0 意味着要一直等下去。 *millis - 以毫秒为单位的等待时间。 */ public final synchronized void join(long millis) throws InterruptedException { //获取启动时的时间戳,用于计算当前时间 long base = System.currentTimeMillis(); //当前时间 long now = 0;
if (millis < 0) {//等待时间不能小于0则抛出IllegalArgumentException throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) {//等待时间为0,则无限等待 //需要注意,如果当前线程未被启动或者终止,则isAlive方法返回false //即意味着join方法不会生效 while (isAlive()) { wait(0); } } else { //需要注意,如果当前线程未被启动或者终止,则isAlive方法返回false //即意味着join方法不会生效 while (isAlive()) { //计算剩余的等待时间 long delay = millis - now; if (delay <= 0) {//如果剩余的等待时间小于等于0,则终止等待 break; } //等待指定时间 wait(delay); //获取当前时间 now = System.currentTimeMillis() - base; } }
}
从源码中可以得知,如果要join
正常生效,调用join
方法的对象必须已经调用了start()
方法且并未进入终止状态。
继承Thread类的方式
线程代码存放Thread子类run方法中(没有返回值)
实现Runnable接口方式
线程代码存在接口的子类run方法中(重写run方法)(没有返回值)
**好处:**避免了单继承的局限,允许多继承。适合多个相同的线程来处理同一份资源
jdk1.6对锁的实现引入了大量的优化。 锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。 注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
锁的状态:锁的状态是通过对象监视器在对象头中的字段来表明的。
自旋锁:当线程抢锁失败后,重试几次
锁的分类并不是全指锁的状态,有的指锁的特性,有的指锁的设计。
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
**公平锁:**将锁分配给排队时间最长的线程;在多核的情况下要维护一个线程等待队列。效率远低于 非公平锁。
非公平锁:不考虑排队时间,抢占式的;(synchronized) ReentrantLock默认是非公平锁,但是底层通过****AQS来实现线程调度,可以使其变成公平锁**。
**可重入锁(递归锁):**同一个线程在外层方法获取锁时,在进入内层方法时会自动获取锁。
**分段锁:**是一种思想。将数据分段,在每个分段上加锁,把锁细粒度化,以提高并发效率。
自旋锁**(SpinLock):**是一种思想。指自己重试,当抢锁失败后,重试几次,抢到了就继续,抢不 到就阻塞线程。**目的还是为了尽量不阻塞线程。
**共享锁:**该锁可被多个线程所持有,并访问共享资源。(读锁)
**独占锁(互斥锁):**一次只能被一个线程持有
ReentrantLock,synchronized都是独占锁;对于ReadWriteLock来说,其读锁是共享锁,其写锁是独占锁。
共同点:
不同点:
AtomicInteger是对int类型的一个封装,提供原子性的访问和更新操作,其原子性操作的实现是基于CAS技术。
CAS+volatile 可见性,有序性
比较并交换 无锁实现 乐观锁 自旋锁
对象封闭在一个线程里,即使这个对象不是线程安全的,也不会出现并发安全问题。
ThreadLocal线程封闭:简单易用
使用ThreadLocal来实现线程封闭,线程封闭的指导思想是封闭,而不是共享。所以说ThreadLocal是用来解决变量共享的并发安全问题,是不精确的。
ThreadLocal叫做线程变量,在ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
ThreadLocal是一个泛型类,保证可以接受任何类型的对象。
因为一个线程内可以存在多个ThreadLocal对象,所以其实是ThreadLocal内部维护了一个Map,这个Map不是直接使用的HashMap,而是ThreadLocal实现的一个叫做ThreadLocalMap的静态内部类。而我们使用的get()、set()方法其实都是调用了这个ThreadLocalMap类对应的get()、set()方法。
createMap方法
ThreadLocalMap是个静态的内部类
set方法
get方法
最终的变量是放在了当前线程的ThreadLocalMap中,并不是存在ThreadLocal上,ThreadLocal可以理解为只是ThreadLocalMap的封装,传递了变量值。
强引用(默认):是指在程序代码之中普遍存在的引用赋值,即类似“Object obj = new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。造成Java内存泄漏的主要原因之一。
软引用:内存不足即回收。在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
弱引用:发现即回收。被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。
虚引用:对象回收跟踪。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null,而value还存在着强引用,只有thread线程退出以后,value的强引用链条才会断掉。
但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref->Thread->ThreadLocalMap->Entry->value
永远无法回收,造成内存泄漏。
key使用强引用
当ThreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
key使用弱引用
当ThreadLocalMap的key为弱引用回收ThredaLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。
ThreadLocal正确的使用方法
每次使用完ThreadLocal都调用它的remove()方法清除数据。
保证了不同线程对这个变量进行操作时的可见性,及一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
为什么?
比如,线程A修改了自己的共享变量副本,这时如果该共享变量没有被volatile修饰,那么本次修改不一定会马上将修改结果刷新到主存中,如果此时B去主存中读取共享变量的值,那么这个值就是没有被A修改之前的值。如果该共享变量被volatile修饰了,那么本次修改结果会强制立刻刷新到主存中,如果此时B去主存中读取共享变量的值,那么这个值就是被A修改之后的值了。
禁止进行指令重排序(有序性)
volatile不能保证对变量操作的原子性
synchronized 有三种方式来加锁,分别是
synchronized提供了同步锁的概念,被synchronized修饰的代码段可以防止多个线程同时执行,必须一个线程把synchronized修饰的代码段都执行完毕了,其他的线程才能开始执行这段代码。
因为synchronized保证了同一时刻,只能有一个线程执行同步代码块,所以执行同步代码块的时候相当于单线程操作了,那么线程的可见性、原子性、有序性它都能保证了。
Java对象头和monitor是实现synchronized的基础。
线程在获取锁的时候,实际上就是获得一个监视器对象(monitor) ,monitor 可以认为是一个同步对象,所有的Java 对象是天生携带 monitor。而monitor是添加Synchronized关键字之后独有的。synchronized同步块使用了monitorenter和monitorexit指令实现同步,这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。
线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁,而执行monitorexit,就是释放monitor的所有权。
系统通过调用线程类的start()方法来启动一个线程、此时该线程处于就绪状态,等待调度,即就是这个线程可以被JVM来调度执行。在调度过程中,JVM底层通过调用线程类的run()方法来完成实际的操作,当run()方法结束后,此线程就会终止。
如果直接调用线程类的run()方法,此时run()方法仅仅被当作一个普通的函数调用,程序中任然只有主线程这一个线程
start()方法能够异步的调用run()方法,但是直接调用run()方法却是同步的。
start()方法实现了多线程,无需等待run()方法体中的代码执行完毕而直接继续执行后续的代码。
实现Runnable接口方式
线程代码存在接口的子类run方法中(重写run方法)(没有返回值)
**好处:**避免了单继承的局限,允许多继承。适合多个相同的线程来处理同一份资源
实现Callable接口方式
与Runnable相比:
cas:比较并交换,该算法是硬件对并发操作的支持。
cas是乐观锁的一种实现方式,是一种轻量级的锁机制。
过程:每次判断我的预期值和内存中的值是否相同,如果不相同则说明该内存值已经被其他线程更新过了,因此需要拿到最新值作为预期值,重新判断。而该线程不断的循环判断是否内存值已经被其他线程更新过了,这就是自旋的思想。
优点:效率高于加锁
缺点:
不断的自旋,会导致cpu消耗。
ABA问题:某个线程将内存值由A改为了B,再由B改为了A。当另一个线程使用预估值去判断时,预期值于内存值相同,误以为该变量没有被修改过而导致的问题。
解决方法:通过使用类似于添加版本号的方式。如原先的内存值为(A,1),线程将(A,1)修改为了(B,2),再由(B,2)修改为(A,3)。此时另一个线程使用预期值(A,1)与内存值(A,3)进行比较,只需要比较版本号 1 和 3,即可发现该内存中的数据被更新过了。
抽象的队列式的同步器。
**核心思想:**如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时所分配的机制,这个机制AQS是用CLH队列锁(CLH同步队列是一个FIFO双向队列,AQS依赖它来完成同步状态的管理)实现的,即将暂时获取不到锁的线程加入到队列中。
由来:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
解决方法:在Java中可以通过线程池来达到这样的效果。线程池里的每个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
java.util.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类。
ThreadPoolExecutor继承了AbstarctExecutorService类,并提供了四个构造器,事实上,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调用第四个构造器进行的初始化工作。
执行流程:
创建完成ThreadPoolExecutor之后,当向线程池提交任务时,通常使用execute方法。
execute方法的执行流程图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zvZPqlxY-1634353769848)(D:\桌面\tu\1629514662034.png)]
如果线程池中存活的核心线程数小于线程数corePoolSize时,线程池会创建一个核心线程去处理提交的任务。
如果线程池核心线程数已满,一个新提交的任务,会被放进任务队列workQueue排队等待执行。
当线程池里面存活的线程数已经等于corePoolSize且任务队列workQueue也满,判断线程数是否达到maximumPoolSize,如果没到达,创建一个非核心线程执行提交的任务。
如果当前的线程数达到了maximumPoolSize,还有新的任务过来的话,直接采用拒绝策略处理。
execute与submit的区别
执行任务除了可以使用execute方法还可以使用submit方法。
区别:
阿里编程规约建议使用ThreadPoolExecutor类,是最原始的线程池创建.
4种拒绝策略
关闭线程池
两个方法实现:
shutdownNow:对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表。
shutdown:当我们调用shutdown后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务。
N代表CPU的个数
反射之中包含了一个「反」字,所以想要解释反射就必须先从「正」开始解释。
一般情况下,我们使用某个类时必定知道它是什么类,是用来做什么的。于是我们直接对这个类进行实例化,之后使用这个类对象进行操作。
Apple apple = new Apple(); //直接初始化,「正」
apple.setPrice(4);
上面这样子进行类对象的初始化,我们可以理解为「正」。
而反射则是一开始并不知道我要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了。
反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。
API:
Class类
Class clz = Class.forName(“com.ff.api.Apple”);
Constructor appleConstructor = clz.getConstructor();
Object appleObj = appleConstructor.newInstance();
Method setPriceMethod = clz.getMethod(“setPrice”, int.class);
setPriceMethod.invoke(appleObj, 14);
Field field = clz.getField(“price”);
boolean isPrimitive = class1.isPrimitive();//判断是否是基础类型
boolean isArray = class1.isArray();//判断是否是集合类
boolean isAnnotation = class1.isAnnotation();//判断是否是注解类
boolean isInterface = class1.isInterface();//判断是否是接口类
boolean isEnum = class1.isEnum();//判断是否是枚举类
String simpleName = class1.getSimpleName();//获取class类名
int modifiers = class1.getModifiers();//获取class访问权限
Class<?>[] declaredClasses = class1.getDeclaredClasses();//内部类
Class<?> declaringClass = class1.getDeclaringClass();//外部类
Annotation[] annotations = class1.getAnnotations();//获取class对象的所有注解
Class parentClass = class1.getSuperclass();//获取class对象的父类
Class<?>[] interfaceClasses = class1.getInterfaces();//获取class对象的所有接口
优点:
运行期类型的判断,动态类加载,动态代理使用反射。 代理对象
缺点:
性能是一个问题,反射相当于一系列解释操作,通知jvm要做的事情,性能比直接的java代码要慢很多。
java注解是jdk5.0引入的一种注释机制。
Java语言中的类、方法、变量、参数和包等都可以被标注。和Javadoc不同,Java标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java虚拟机标注内容,在运行时可以获取到标注内容。当然它也支持自定义Java标注。
@Override
检查该方法是否可以重写方法。如果发现其父类,或者是引用的接口并没有该方法时,会报编译错误。
@Deprecated
标记过时方法。如果使用该方法,会报编译警告。
@SuppressWarnings
指示编译器去忽略注解中声明的警告。
作用在其他注解的注解(元注解)是:
@Retention(重点)
标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者在运行时可以通过反射访问。
定义了该注解被保留的时间长短:某些注解仅出现在源代码中,而被编译器丢弃;而另一些在class被编译在class文件中;编译在class文件中的注解可能会被虚拟机忽略,而另一些在class被装载时将被读取。
使用这个meta-Annotation可以对注解的”生命周期“限制。
作用:标识需要在什么级别保存该注释信息,用于描述注解的声明周期。
取值:
@Documented
标记这些注解是否包含在用户文档中。
@Target(重点)
用于描述注解的使用范围。
ElementType.TYPE可以应用于类的任何元素。
ElementType.CONSTRUCTOR可以应用于构造函数。
ElementType.FLELD可以应用于字段或属性。
ElementType.LOCAL_VARIABLE 可以应用于局部变量。
ElementType.METHOD 可以应用于方法级注释。
ElementType.PACKAGE 可以应用于包声明。
ElementType.PARAMETER 可以应用于方法的参数。
@Inherited
标记这个注解是继承于哪个注解类(默认注解没有继承于任何子类)
@SafeVarargs
Java7开始支持,忽略任何使用参数未泛型变量的方法或构造函数调用生产的警告。
@Repeatable
java8开始支持,标识某注解可以在同一个声明上使用多次。
创建一个Annotaion
public @interface NotNull {
String message() default "";
}
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {
String message() default “”;
}
private String name;
“token punctuation”>();
/**
设计原则 | 总结 | 目的 |
---|---|---|
开闭原则 | 对扩展开放,对修改关闭 | 降低维护带来的新风险 |
依赖倒置原则 | 高层不应该依赖底层,要面向接口编程 | 便于理解,提高代码的可读性 |
单一职责原则 | 一个类只干一件事,实现类要单一 | 便于理解,提高代码的可读性 |
接口隔离原则 | 一个接口只干一件事,接口要精简单一 | 功能解耦,高聚合,低耦合 |
迪米特法则 | 不该知道的不要知道,一个类应该保持对其它对象最少的了解,降低耦合度 | 只是和朋友说话,不和陌生人说话,减少代码的臃肿 |
里氏替换原则 | 不要破环继承体系,子类重写方法功能发生改变,不应该影响父类方法的含义 | 防止继承泛滥 |
合成复用原则 | 尽量使用组合或者聚合关系实现代码复用,少使用继承 | 降低代码耦合 |
概念:设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。
目的:为了提高代码的可重用性、代码的可读性和代码的可靠性。
本质上:面向对象设计原子弹实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。
优势:
Java设计模式类型
根据模式是用来完成什么工作来划分:
创建型模式:用于描述“怎样创建对象”,它的主要特点是”将对象的创建与使用分离“。提供了单例、原型、工厂方法、抽象工厂、建造者 5 种创建型模式。
结构型模式:用于描述如何将类或对象按某种布局组成更大的结构,提供了代理、适配器、桥接、装饰、外观、享元、组合 7 种结构型模式。
行为型模式:用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎么样分配职责。提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器 11 种行为型模式。
定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
特点:
两种实现模式:
懒汉式单例
特点:类加载时没有生成单例,只有当第一次调用getInstance方法时才去创建这个单例。
public class Singleton{
private static Singleton instance = null;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
饿汉式单例
特点:类一旦加载就创建一个单例,保证在调用getInstance方法之前单例已经存在了。
public class Singleton{
private final static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
**定义:**定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类当中。
按照实际业务场景划分,工厂模式有3中不同的实现方式,分别是简单工厂模式、 工厂方法模式和抽象工厂模式
简单工厂
我们把创建的对象称为”产品“,把创建产品的对象称为”工厂”。如果要创建的产品不多,只要一个工厂类就可以完成,这种模式叫“简单工厂模式“
简单工厂模式中创建实例的方法通常为静态方法。
简单工厂模式的主要角色如下:
特征:代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等.
概念:我们在访问实际对象时,是通过代理对象来访问的,代理模式就是在访问实际对象时引入一定程度的间接性,因为这种间接性,可以附加多种用途。
优点:
结构:
代理实现可以分为静态代理和动态代理。
静态代理
静态代理模式的特点,代理类接受一个Subject接口的对象,任何实现该接口的对象,都可以通过代理类进行代理,增加了通用性。但是也有缺点,每一个代理类都必须实现一遍委托类的接口,如果接口增加方法,则代理类也必须跟着修改。其次,代理类每一个接口对象对应一个委托对象,如果委托类对象非常多,则静态代理类就非常臃肿,难以胜任。
动态代理
动态代理中,代理类并不是在java代码中实现,而是在运行时期生成,相比静态代理,动态代理可以很方便的对委托类的方法进行统一处理,如添加方法调用次数、添加日志功能等等,动态代理分为JDK动态代理和cglib动态代理。
jdk代理
jdk动态代理是实现方式,是通过反射来实现的,借助Java自带的java.lang.reflect.Proxy,通过固定的规则生成。
步骤
Cglib代理
jdk实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理,这就需要CGLib了。CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用顺势织入横切逻辑。但因为采用的是继承,所以不能对final修饰的类进行代理。JDK动态代理与CGLib动态代理均是实现SpringAOP的基础。
总结:
建造者模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示
建造者模式中有以下几个角色:
建造者模式在使用过程中可以演化出多种形式,如
java.lang.StringBuilder的append方法使用的就是建造者模式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RBRyTl4V-1634353769851)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631806135814.png)]
import lombok.Data;
// 产品
@Data
class Product {
String inner;
String outer;
}
// 抽象建造者:抽象出建造过程
interface Builder {
void buildInner();
void buildOuter();
Product buildProduct();
}
// 具体建造者:不同产品,创建不同的具体建造者
class ConcreteBuilder implements Builder {
Product product;
public ConcreteBuilder(Product product) {
this.product = product;
}
@Override
public void buildInner() {
product.inner = "建造产品内部";
}
@Override
public void buildOuter() {
product.outer = "建造产品外部";
}
@Override
public Product buildProduct() {
return product;
}
}
// 指导者:根据建造过程组装产品
class Director {
public Product constructProduct(Builder builder) {
builder.buildInner();
builder.buildOuter();
return builder.buildProduct();
}
}
public class BuilderClient {
public static void main(String[] args) {
ConcreteBuilder concreteBuilder = new ConcreteBuilder(new Product());
Director director = new Director();
Product product = director.constructProduct(concreteBuilder);
// Product(inner=建造产品内部, outer=建造产品外部)
System.out.println(product);
}
}
物理层:利用传输介质为数据链路层提供物理连接,实现比特流的透明传输。
数据链路层:接收来自物理层的位流形式的数据,并封装成帧,传送到上一层
网络层:将网络地址翻译成对应的物理地址,并通过路由选择算法为分组通过通信子网选择最适当的路径。
传输层:在源端与目的端之间提供可靠的透明数据传输
会话层:负责在网络中的两节点之间建立、维持和终止通信
表示层:处理用户信息的表示问题,数据的编码,压缩和解压缩,数据的加密和解密
应用层:为用户的应用进程提供网络通信服务
Socket ServerSocket
数据报 ip+端口+数据
TCP特点:
UDP特点:
TCP | UDP | |
---|---|---|
是否面向连接 | 面向连接 | 无连接 |
传输可靠性 | 可靠 | 不可靠 |
传输形式 | 字节流 | 数据报文段 |
传输效率 | 慢 | 快 |
所需资源 | 多 | 少 |
应用场景 | 要求通信数据可靠,如:文件传输、邮件传输 | 要求通信速度高,如:域名转换、直播 |
首部字节 | 20-60 | 8 |
TCP建立连接的过程叫做握手,握手需要在客户和服务器之间交换三个TCP报文段。
最初客户端和服务端都处于CLOSED(关闭)状态。
一开始,服务器端的TCP服务器进程首先创建传输控制块TCB,准备接收客户端进程的连接请求。然后服务端进程就处于LISTEN(监听)状态,等待客户端的连接请求。如有,立即作出响应。
第一次握手:客户端的TCP客户端进程也是首先创建传输控制块TCB。然后,再打算建立TCP连接时,向服务器发出连接请求报文段SYN,并指明客户端的初始化序列号ISN。此时客户端处于SYN_SENT(请求连接)状态。
首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。
第二次握手:服务器收到客户端的SYN报文之后,会以自己的SYN报文作为应答,并且也是指定了自己的初始化序列号ISN。同时会把客户端的ISN+1作为ACK的值,表示自己已经收到了客户端的SYN,此时服务器处于SYN_RCVD(同步收到)状态
在确认报文段中设置SYN=1,ACK=1,确认号ACK=x+1,初始化序号seq=y
第三次握手:客户端收到SYN报文后,会发送一个ACK报文,当然,也是一样把服务器的ISN+1作为ACK的值,表示已经收到了服务端的SYN报文,此时客户端处于ESTABLISHED
(已建立连接)状态。服务器收到了ACK报文之后,也处于ESTABLISHED
(已建立连接)状态。
确认报文段ACK=1,确认号ACK=y+1,序号seq=x+1,ACK报文段可以携带数据,不携带数据则不消耗序号。
客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。
注意:第三次握手可以携带数据
TCP连接的拆除需要发送四个包,因此称为四次挥手,客户端或服务器端均可发起挥手动作。
刚开始双方都处于ESTABLISHED
(已建立连接)状态,假如是客户端先发起关闭请求。
第一次挥手:客户端发送了一个FIN报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1
状态。
即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
第二次挥手:服务器端收到FIN之后,会发送ACK报文,且把客户端的序列号值+1作为ACK报文的序列号值,表明已经收到客户端的报文了,此时服务端处于CLOSE_WAIT
(关闭等待)状态。
此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务器发出的连接释放报文段。
第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给FIN报文,且指定一个序列号。此时服务端处于LAST_ACK
的状态。
即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
第四次挥手:客户端收到FIN之后,一样发送一个ACK报文作为应答,且把服务端的序列号值+1作为自己的ACK报文的序列号值,此时客户端处于TIME_WAIT状态。需要过一阵子以确保服务端收到自己的ACK报文之后才会进入CLOSED状态,服务端收到ACK报文之后,就处于关闭连接了,处于CLOSED状态
即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。
TCP包头中有32位的序号,有序性是通过序号保证的
tcp通过字节编号,每一个数据字节都会有一个编号,比如发送了三包,每个包100字节,假设第一个包首个字节标号是1,那么发送的三包的编号就是1,101,201,三包数据,只有接收端收到连续的序号的包,才会讲数据包提交到应用层例如收到1,201,101,是不会提交到上层应用层的,只有收到正确连续顺序才会提交,所以就保证了数据的有序性。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U9cnhSlg-1634353769857)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1630205725872.png)]
通过滑动窗口来解决
发送端根据自己的实际情况发送数据。但是,接收端可能收到的是一个毫无关系的数据包又可能会在处理其他问题上花费一些时间。因此在为这个数据包做其他处理时会耗费一些时间,甚至在高负荷的情况下无法接收任何数据。如此一来,如果接收端将本应该接收的数据丢弃的话,就又会触发重发机制,从而导致网络流量的无端浪费。
TCP协议里窗口机制有2种:一种是固定的窗口大小;一种是滑动的窗口。这个窗口大小就是我们一次传输几个数据。对所有数据帧按顺序赋予编号,发送方在发送过程中始终保持着一个发送窗口,只有落在发送窗口内的帧才允许被发送;同时接收方也维持着一个接收窗口,只有落在接收窗口内的帧才允许接收。这样通过调整发送方窗口和接收方窗口的大小可以实现流量控制。
拥塞控制是通过拥塞窗口控制的
有了TCP的滑动窗口控制,收发主机之间即使不再以一个段为单位,而是以一个窗口为单位发送确认应答信号,所以发送主机够连续发送大量数据包。然而,如果在通信刚开始的时候就发送大量的数据包,也有可能会导致网络的瘫痪。
在拥塞控制中,发送方维持一个叫做拥塞窗口cwnd的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。
拥塞控制使用了两个重要的算法:慢启动算法,拥塞避免算法
慢启动算法
慢启动算法的思路,不要一开始就发送大量的数据,先试探一下网络的拥塞程度,也就是说由小到大逐渐增加窗口的大小。慢算法中,每个传输轮次后将cwnd加倍。指数增长
拥塞避免算法
拥塞避免算法也是逐渐增大的cwnd的大小,只是采用的是线性增长
具体来说就是每个传输轮次后将cwnd的大小加一,如果发现出现网络拥塞的话就按照上面的方法重新设置ssthresh的大小并从cwnd=1开售那个重新执行慢开始算法。
(1)序列号、确认应答、超时重传;数据到达接收方,接收方需要发出一个确认应答,表示收到该数据段,并且确认序列号会说明下次接收的数据序列号。如果发送方迟迟未收到确认应答,那么可能是发送数据丢失,也可能是确认应答丢失,这时发送方会等待一定时间后重传。
(2)窗口控制与高速重发控制/快速重传(重复确认应答);TCP利用窗口控制来提高传输速度,意思是在一个窗口大小内,不一定等到应答才能发送下一段数据,窗口大小就是无需等待确认而可以继续发送数据的最大值。
(3)拥塞控制;如果窗口定义的很大,发送端连续发送大量的数据,可能会造成网络的拥堵,甚至网络瘫痪。所以TCP为了防止这种情况而进行了拥塞控制。
TCP建立连接和断开连接过程:三次握手、四次挥手。
DNS(Domain Name System)域名系统,将主机域名转换为ip地址,属于应用层协议,使用UDP传输。
它作为将[域名]和[IP地址]的一个[分布式数据库],能够使人更方便地访问[互联网].
过程:
总结: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。
一、主机向本地域名服务器的查询一般都是采用递归查询。
二、本地域名服务器向根域名服务器的查询的迭代查询。
1)当用户输入域名时,浏览器先检查自己的缓存中是否 这个域名映射的ip地址,有解析结束。
2)若没命中,则检查操作系统缓存(如Windows的hosts)中有没有解析过的结果,有解析结束。
3)若无命中,则请求本地域名服务器解析( LDNS)。
4)若LDNS没有命中就直接跳到根域名服务器请求解析。根域名服务器返回给LDNS一个 主域名服务器地址。
5) 此时LDNS再发送请求给上一步返回的gTLD( 通用顶级域), 接受请求的gTLD查找并返回这个域名对应的Name Server的地址
6) Name Server根据映射关系表找到目标ip,返回给LDNS
7) LDNS缓存这个域名和对应的ip, 把解析的结果返回给用户,用户根据TTL值缓存到本地系统缓存中,域名解析过程至此结束.
会话跟踪是Web程序中常用的技术,用来跟踪用户的整个会话。常用的会话跟踪技术是Cookie与Session。
get : 获取 参数在地址中传输, 大小受限,不安全
post:发送 在请求体 数据大小不限制 安全
TCP/IP 协议你一定听过,TCP/IP 我们一般称之为协议簇,什么意思呢?就是 TCP/IP 协议簇中不仅仅只有 TCP 协议和 IP 协议,它是一系列网络通信协议的统称。而其中最核心的两个协议就是 TCP / IP 协议,其他的还有 UDP、ICMP、ARP 等等,共同构成了一个复杂但有层次的协议栈。
TCP 协议的全称是 Transmission Control Protocol 的缩写,意思是传输控制协议,HTTP 使用 TCP 作为通信协议,这是因为 TCP 是一种可靠的协议,而可靠能保证数据不丢失。
IP 协议的全称是 Internet Protocol 的缩写,它主要解决的是通信双方寻址的问题。IP 协议使用 IP 地址 来标识互联网上的每一台计算机,可以把 IP 地址想象成为你手机的电话号码,你要与他人通话必须先要知道他人的手机号码,计算机网络中信息交换必须先要知道对方的 IP 地址。(关于 TCP 和 IP 更多的讨论我们会在后面详解)
1.HTTP协议传输的数据都是未加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常不安全, HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。
SSL依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密。
2.https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
3.http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
SSL代表安全套接字层。它是一种用于加密和验证应用程序(如浏览器)和Web服务器之间发送的数据的协议。 身份验证 , 加密Https的加密机制是一种共享密钥加密和公开密钥加密并用的混合加密机制。SSL/TLS协议作用:认证用户和服务,加密数据,维护数据的完整性的应用层协议加密和解密需要两个不同的密钥,故被称为非对称加密;加密和解密都使用同一个密钥的 对称加密。 优点在于加密、解密效率通常比较高HTTPS 是基于非对称加密的, 公钥是公开的,
(1)客户端向服务器端发起SSL连接请求;
(2) 服务器把公钥发送给客户端,并且服务器端保存着唯一的私钥
(3)客户端用公钥对双方通信的对称秘钥进行加密,并发送给服务器端
(4)服务器利用自己唯一的私钥对客户端发来的对称秘钥进行解密,
(5)进行数据传输,服务器和客户端双方用公有的相同的对称秘钥对数据进行加密解密,可以保证在数据收发过程中的安全,即是第三方获得数据包,也无法对其进行加密,解密和篡改。
因为数字签名、摘要是证书防伪非常关键的武器。 “摘要”就是对传输的内容,通过hash算法计算出一段固定长度的串。然后,在通过CA的私钥对这段摘要进行加密,加密后得到的结果就是“数字签名”.
我们假设访问的 URL 地址为 http://www.someSchool.edu/someDepartment/home.index,当我们输入网址并点击回车时,浏览器内部会进行如下操作
DNS服务器会首先进行域名的映射,找到访问www.someSchool.edu所在的地址,然后HTTP 客户端进程在 80 端口发起一个到服务器 www.someSchool.edu 的 TCP 连接(80 端口是 HTTP 的默认端口)。在客户和服务器进程中都会有一个套接字与其相连。
HTTP 客户端通过它的套接字向服务器发送一个 HTTP 请求报文。该报文中包含了路径 someDepartment/home.index 的资源,我们后面会详细讨论 HTTP 请求报文。
HTTP 服务器通过它的套接字接受该报文,进行请求的解析工作,并从其存储器(RAM 或磁盘)中检索出对象 www.someSchool.edu/someDepartment/home.index,然后把检索出来的对象进行封装,封装到 HTTP 响应报文中,并通过套接字向客户进行发送。
HTTP 服务器随即通知 TCP 断开 TCP 连接,实际上是需要等到客户接受完响应报文后才会断开 TCP 连接。
HTTP 客户端接受完响应报文后,TCP 连接会关闭。HTTP 客户端从响应中提取出报文中是一个 HTML 响应文件,并检查该 HTML 文件,然后循环检查报文中其他内部对象。
检查完成后,HTTP 客户端会把对应的资源通过显示器呈现给用户。
至此,键入网址再按下回车的全过程就结束了。上述过程描述的是一种简单的请求-响应全过程,真实的请求-响应情况可能要比上面描述的过程复杂很多。
域名解析 --> 发起TCP的3次握手 --> 建立TCP连接后发起http请求 --> 服务器响应http请求,浏览器得到html代码 --> 浏览器解析html代码,并请求html代码中的资源(如js、css、图片等) --> 浏览器对页面进行渲染呈现给用户。
HTTP 协议主要由三大部分组成:
起始行(start line):描述请求或响应的基本信息;
头部字段(header):使用 key-value 形式更详细地说明报文;
消息正文(entity):实际传输的数据,它不一定是纯文本,可以是图片、视频等二进制数据。
其中起始行和头部字段并成为 请求头 或者 响应头,统称为 Header;消息正文也叫做实体,称为 body。HTTP 协议规定每次发送的报文必须要有 Header,但是可以没有 body,也就是说头信息是必须的,实体信息可以没有。而且在 header 和 body 之间必须要有一个空行(CRLF),如果用一幅图来表示一下的话,我觉得应该是下面这样
如图,这是 http://www.someSchool.edu/someDepartment/home.index 请求的请求头,通过观察这个 HTTP 报文我们就能够学到很多东西,首先,我们看到报文是用普通 ASCII 文本书写的,这样保证人能够可以看懂。然后,我们可以看到每一行和下一行之间都会有换行,而且最后一行(请求头部后)再加上一个回车换行符。
每个报文的起始行都是由三个字段组成:方法、URL 字段和 HTTP 版本字段。
其实session是一个存在服务器上的类似于一个散列表格的文件。里面存有我们需要的信息,在我们需要用的时候可以从里面取出来。类似于一个大号的map吧,里面的键存储的是用户的sessionid,用户向服务器发送请求的时候会带上这个sessionid。这时就可以从中取出对应的值了。
在HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。而从HTTP/1.1起,默认使用长连接,用以保持连接特性。在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。
CSRF(Cross-Site Request Forgery)的全称是“跨站请求伪造”,指攻击者通过跨站请求,以合法的用户的身份进行非法操作。可以这么理解CSRF攻击:攻击者盗用你的身份,以你的名义向第三方网站发送恶意请求。CRSF能做的事情包括利用你的身份发邮件,发短信,进行交易转账,甚至盗取账号信息。如何防范CSRF攻击
安全框架:例如Spring Security。
token机制:在HTTP请求中进行token验证,如果请求中没有token或者token内容不正确,则认为CSRF攻击而拒绝该请求。
**验证码:**通常情况下,验证码能够很好的遏制CSRF攻击,但是很多情况下,出于用户体验考虑,验证码只能作为一种辅助手段,而不是最主要的解决方案。
**referer识别:**在HTTP Header中有一个字段Referer,它记录了HTTP请求的来源地址。如果Referer是其他网站,就有可能是CSRF攻击,则拒绝该请求。但是,服务器并非都能取到Referer。很多用户出于隐私保护的考虑,限制了Referer的发送。在某些情况下,浏览器也不会发送Referer,例如HTTPS跳转到HTTP。
1)验证请求来源地址;
2)关键操作添加验证码;
3)在请求地址添加 token 并验证。
HTTP状态码表示客户端HTTP请求的返回结果、标记服务器端的处理是否正常或者是出现的错误,能够根据返回的状态码判断请求是否得到正确的处理。
状态码由3位数字和原因短语组成
状态码分类表
类别 | 原因短语 | |
---|---|---|
1xx | informational(信息性状态码) | 接受的请求正在处理 |
2xx | Success(成功状态码) | 请求正常处理完毕 |
3xx | Redirection(重定向) | 需要进行附加操作以完成请求 |
4xx | client error(客户端错误) | 客户端请求出错,服务器无法处理请求 |
5xx | Server Error(服务器错误) | 服务器处理请求出错 |
各类别常见状态码:
2xx
3xx
4xx
5xx
500 inter Server Error:表示服务器在执行请求时发生了错误,也有可能是web应用存在的bug或某些临时的错误时;
503 Server Unavailable:表示服务器暂时处于超负载或正在进行停机维护,无法处理请求。
SQL注入攻击
攻击者在HTTP请求中注入恶意的SQL代码,服务器使用参数构建数据库SQL命令时,恶意SQL被一起构造,并在数据库中执行。
用户登录,输入用户名 lianggzone,密码 ‘ or ‘1’=’1 ,如果此时使用参数构造的方式,就会出现
select * from user where name = ‘lianggzone’ and password = ‘’ or ‘1’=‘1’
不管用户名和密码是什么内容,使查询出来的用户列表不为空。如何防范SQL注入攻击使用预编译的PrepareStatement是必须的,但是一般我们会从两个方面同时入手。
Web端
1)有效性检验。
2)限制字符串输入的长度。
服务端
1)不用拼接SQL字符串。
2)使用预编译的PrepareStatement。
3)有效性检验。(为什么服务端还要做有效性检验?第一准则,外部都是不可信的,防止攻击者绕过Web端请求)
4)过滤SQL需要的参数中的特殊字符。比如单引号、双引号。
项目中前后端分离部署,所以需要解决跨域的问题。
我们使用cookie存放用户登录的信息,在spring拦截器进行权限控制,当权限不符合时,直接返回给用户固定的json结果。
当用户登录以后,正常使用;当用户退出登录状态时或者token过期时,由于拦截器和跨域的顺序有问题,出现了跨域的现象。
我们知道一个http请求,先走filter,到达servlet后才进行拦截器的处理,如果我们把cors放在filter里,就可以优先于权限拦截器执行。
前端解决
后端解决
跨域资源共享,实现了跨站访问控制, 使得安全地进行跨站数据传输成为可能。
服务器端对于 CORS 的支持,主要就是通过设置响应头Access-Control-Allow-Origin来进行的。如果浏览器检测到相应的设置,就可以允许 Ajax 进行跨域的访问。
只需要在后台中加上响应头来允许域请求!在被请求的 Response header 中加入以下设置,就可以实现跨域访问了。
Spring项目中的解决方法:
手工设置响应头(HttpServletResponse )
使用 HttpServletResponse 对象添加响应头(Access-Control-Allow-Origin)来授权原始域,这里 Origin 的值也可以设置为"*" ,表示全部放行。
使用注解(@CrossOrigin)
在方法上(@RequestMapping)使用注解 @CrossOrigin :
返回新的CorsFilter
在任意配置类,返回一个新的 CorsFilter Bean,并添加映射路径和具体的 CORS 配置信息。
算法 | 特点 | 有效破解方式 | 破解难度 | 其他 |
---|---|---|---|---|
明文保存 | 实现简单 | 无需破解 | 简单 | |
对称加密 | 可以解密出明文 | 获取密钥 | 中 | 需要确保密钥不泄露 |
单向HASH(MD5、SHA1) | 不可解密 | 碰撞、彩虹表 | 中 | |
特殊HASH | 不可解密 | 碰撞、彩虹表 | 中 | 需要确保“盐”不泄露 |
Pbkdf2 | 不可解密 | 无 | 难 | 需要设定合理的参数 |
联系:
JSP是Servlet技术的扩展点,本质上就是Servlet的简易方式。JSP编译后是“类Servlet”
Servlet和JSP最主要的不同点在于:Servlet的应用逻辑是在Java文件中,并且完全从表示层中的HTML里分离开来。
而JSP的情况是Java和HTML可以组合成一个扩展名为.jsp的文件。
JSP侧重于视图,Servlet主要用于控制逻辑
Servlet更多的是类似于一个Controller,用来做控制。
用java编写的服务器端程序,
接收客户端请求 service
处理
响应
服务器负责 客户端 与 servlet之间的通信
服务器启动时创建/第一次访问时创建
init() 初始化
服务 service() doget/dopost -----> springmvc解析—> 方法
销毁 destroy()
不安全,servlet对象是单例的, 成员变量会被多个线程共享, 解决方法:使用ThreadLocal
拦截器它是链式调用,一个应用中可以同时存在多个拦截器Interceptor, 一个请求也可以触发多个拦截器 ,而每个拦截器的调用会依据它的声明顺序依次执行。
首先编写一个简单的拦截器处理类,请求的拦截是通过HandlerInterceptor 来实现,看到HandlerInterceptor 接口中也定义了三个方法。
preHandle() :这个方法将在请求处理之前进行调用。注意:如果该方法的返回值为false ,将视为当前请求结束,不仅自身的拦截器会失效,还会导致其他的拦截器也不再执行。
postHandle():只有在 preHandle() 方法返回值为true 时才会执行。会在Controller 中的方法调用之后,DispatcherServlet 返回渲染视图之前被调用。 有意思的是:postHandle() 方法被调用的顺序跟 preHandle() 是相反的,先声明的拦截器 preHandle() 方法先执行,而postHandle()方法反而会后执行。
afterCompletion():只有在 preHandle() 方法返回值为true 时才会执行。在整个请求结束之后, DispatcherServlet 渲染了对应的视图之后执行。
过滤器的配置比较简单,直接实现Filter 接口即可,也可以通过@WebFilter注解实现对特定URL拦截,看到Filter 接口中定义了三个方法。
过滤器 和 拦截器 均体现了AOP
的编程思想,都可以实现诸如日志记录、登录鉴权等功能,但二者的不同点也是比较多的,接下来一一说明。
实现原理不同
过滤器
是基于函数回调的,拦截器
则是基于Java的反射机制(动态代理)实现的。
在我们自定义的过滤器中都会实现一个 doFilter()
方法,这个方法有一个FilterChain
参数,而实际上它是一个回调接口。ApplicationFilterChain
是它的实现类, 这个实现类内部也有一个 doFilter()
方法就是回调方法。
ApplicationFilterChain
里面能拿到我们自定义的xxxFilter
类,在其内部回调方法doFilter()
里调用各个自定义xxxFilter
过滤器,并执行 doFilter()
方法。
而每个xxxFilter 会先执行自身的 doFilter() 过滤逻辑,最后在执行结束前会执行filterChain.doFilter(servletRequest, servletResponse),也就是回调ApplicationFilterChain的doFilter() 方法,以此循环执行实现函数回调。
使用范围不同
我们看到过滤器 实现的是 javax.servlet.Filter
接口,而这个接口是在Servlet
规范中定义的,也就是说过滤器Filter
的使用要依赖于Tomcat
等容器,导致它只能在web
程序中使用。
而拦截器(Interceptor
) 它是一个Spring
组件,并由Spring
容器管理,并不依赖Tomcat
等容器,是可以单独使用的。不仅能应用在web
程序中,也可以用于Application
、Swing
等程序中。
触发时机不同
过滤器
和 拦截器
的触发时机也不同,我们看下边这张图。
过滤器Filter
是在请求进入容器后,但在进入servlet
之前进行预处理,请求结束是在servlet
处理完以后。
拦截器 Interceptor
是在请求进入servlet
后,在进入Controller
之前进行预处理的,Controller
中渲染了对应的视图之后请求结束。
拦截的请求范围不同
过滤器Filter
执行了两次,拦截器Interceptor
只执行了一次。这是因为过滤器几乎可以对所有进入容器的请求起作用,而拦截器只会对Controller
中请求或访问static
目录下的资源请求起作用。
注入Bean情况不同
因为加载顺序导致的问题,拦截器
加载的时间点在springcontext
之前,而Bean
又是由spring
进行管理。
控制执行顺序不同
看到输出结果发现,先声明的拦截器 preHandle()
方法先执行,而postHandle()
方法反而会后执行。
postHandle()
方法被调用的顺序跟 preHandle()
居然是相反的!如果实际开发中严格要求执行顺序,那就需要特别注意这一点。
我们要知道controller
中所有的请求都要经过核心组件DispatcherServlet
路由,都会执行它的 doDispatch()
方法,而拦截器postHandle()
、preHandle()
方法便是在其中调用的。
发现两个方法中在调用拦截器数组 HandlerInterceptor[]
时,循环的顺序竟然是相反的。。。,导致postHandle()
、preHandle()
方法执行的顺序相反。
一个优秀的(半自动的)数据持久层框架,是一个将sql与java分离的,提供对象关系映射(ORM),
将JDBC中的接口进行了封装,简化了JDBC代码. 提供了动态sql语句使得功能强大.
优点:
1.sql语句与代码分离,存放于xml配置文件中
2.用逻辑标签控制动态SQL的拼接
3.查询的结果集与java对象自动映射,保证名称相同,配置好映射关系即可自动映射或者
4.编写原生SQL 效率高
缺点:
JDBC方式可以用用打断点的方式调试,但是Mybatis不能,需要通过log4j日志输出日志信息帮助调试
对 SQL语句依赖程度很高;并且属于半自动,数据库移植比较麻烦(相比hibernate) 配置方言oracle
第一步:通过Resources加载配置好的mybatis.xml配置文件.
第二步:然后看第二句话,这句话是关键。我们首先new了一个SqlSessionFactoryBuilder对象,他是SqlSessionFactory的构建者。我们调用了他的build()方法.
第三步:我们继续往下走,我们最终的目的是获取一个SqlSession对象,现在我们有了一个SqlSessionFactory了,就愉快的生成SqlSession吧.
第四步:jdk动态代理生成mapper接口的代理对象.
第五步:通过第四步返回的代理对象的方法调用mapper方法最终执行的方法.
第六步: 封装查询结果.
Mybatis核心类:
SqlSessionFactory:
每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为中心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或通过Java的方式构建出 SqlSessionFactory 的实例。SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,建议使用单例模式或者静态单例模式。一个SqlSessionFactory对应配置文件中的一个环境(environment),如果你要使用多个数据库就配置多个环境分别对应一个SqlSessionFactory。
SqlSession:
SqlSession是一个接口,它有2个实现类,分别是DefaultSqlSession(默认使用)以及SqlSessionManager。SqlSession通过内部存放的执行器(Executor)来对数据进行CRUD。此外SqlSession不是线程安全的,因为每一次操作完数据库后都要调用close对其进行关闭,官方建议通过try-finally来保证总是关闭SqlSession。
Executor:
Execounterrevolutionary接口有两个实现类,其中BaseExecutor有三个继承类分别是BatchExecutor(重用语句并执行批量更新),ReuseExecutor(重用预处理语句prepared statement,跟Simple的唯一区别就是内部缓存statement),SimpleExecutor(默认,每次都会创建新的statement)。以上三个就是主要的Executor。通过下图可以看到Mybatis在Executor的设计上面使用了装饰器模式,我们可以用CachingExecutor来装饰前面的三个执行器目的就是用来实现缓存。
#{} 预编译方式 安全
${} 字符串拼接 不安全 传一个列名
resultmap 自定义映射 列名与类中属性名不相同, 关联关系(单个,集合)
resultType 具体的返回结果类型
接口中的方法 与 xml中 匹配
方法名与 xml中id名相同
参数类型 与 xml中参数类型相同
返回值 与 xml中返回值相同
方法能重载 不能重载
接口绑定,就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们通过代理对象调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置。
Mybatis动态sql可以让我们在Xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能,Mybatis提供了9种动态sql标签trim|where|set|foreach|if|choose|when|otherwise|bind。
其执行原理为,使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能。
where
and age = 10
在mybatis的多表操作中,是可以实现查询A时,一并把其关联对象B给查出来的操作。
现在问题来了,若有一对多的关系“用户-钱包”:某用户A名下有1000个钱包,并且有关联查询的需求,那么我们每次查询用户,难道都要把他的1000个钱包全都查询出来吗?当然不是,由于user对象中包含钱包集合的引用,每次查询后,JVM都会在堆中开辟空间来存储这个集合对象,这样会浪费巨大的内存空间,我们不需要钱包信息的时候,当然就不应该去查询它。那么怎么去控制这个查询的时机呢?这就需要Mybatis的延迟加载了。
延迟加载就是在真正使用数据时才发起查询,不用的时候不查询——也叫按需加载(懒加载)。
反之就是立即加载,无论数据是否需要使用,只要调用查询方法,将马上发起查询。
Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 把一个关联查询 ,拆分为多次查询,需要时,在发出查询,嵌套查询
它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。
(1)一级缓存: 其存储作用域为SqlSession,当 SqlSession.flush 或 close 之后,该 SqlSession中的所有 Cache 就将清空,默认打开一级缓存。
(2)二级缓存与一级缓存其机制相同,其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache .要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置 ;
当配置了二级缓存后,关闭sqlSession时会将数据写入到二级缓存,下次在新会话中仍然可以从二级缓存中查询到数据.
Spring是一个轻量级Java开源工具,它是为了解决企业应用开发的复杂性而创建的,即简化Java开发。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ftr9YZYb-1634353769859)(D:\桌面\tu\1628498683597.png)]
Core Container(核心容器)
Bean:管理Beans
Core:Spring核心
Context:配置文件
ExpressionLanguage:SpEL表达式
AOP(切面编程)
Aspects:AOP框架
Data Access(数据库整合)
JDBC,ORM,OXM,JMS,Transaction
Web(MVC Web开发)
Web,Servlet,Portlet,Struts
Test(Junit整合)
Spring依赖反射,反射影响性能。
不要频繁地使用反射,大量地使用反射会带来性能问题。
通过反射直接访问实例会比访问方法快很多,所以应该优先采用访问实例的方式。
控制反转是一种设计思想,就是将原本在程序中手动创建对象的控制权,交由Spring框架来管理。
IOC容器是具有依赖注入功能的容器,负责对象的实例化、对象的初始化,对象和对象之间依赖关系配置、对象的销毁、对外提供对象的查找等操作,对象的整个生命周期都是由容器来控制。我们需要使用的对象都由IOC容器进行管理,不需要我们再去手动通过new的方式去创建对象,由IOC容器直接帮我们组装好,当我们需要使用的时候直接从IOC容器中直接获取就可以了。
正控:若要使用某个对象,需要自己去负责对象的创建。
反控:若要使用某个对象,只需要从Spring容器中获取需要使用的对象,不关心对象的创建过程,也就是把创建对象的控制权反转给了Spring框架。
目的:降低耦合度。
底层实现方式:解析xml/扫描注解标签+工厂模式+反射机制
控制反转是一种通过描述(XML或注解)并通过第三方去生成或获取特定对象的方式。在Spring中实现控制反转的是IOC容器,其实现方式是依赖注入(DI)。
作用:
优点:
功能:
概念:组件之间依赖关系由容器在运行期间决定,形象的说,即由容器动态的将某个依赖关系注入到组件中。通过依赖注入机制,只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,有谁实现。
目的:提升组件的重用的频率,并为系统搭建一个灵活、可扩展的平台。
概念:组件之间依赖关系由容器在运行期间决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。通过依赖注入机制,只需要通过简单的配置,而无需任何代码就可以指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
目的:提升组件重用的频率,并为系统搭建应该灵活、可扩展的平台。
基于xml配置方式
bean:配置需要 spring管理的类
id:生成的对象名
class:全类名
name:对象别名,可以为多个
scope(作用域):
Xml配置方式依赖注入
指Spring创建对象的过程中,将对象依赖属性通过配置设置给该对象。
set注入:容器通过调用无参构造器或无参static工厂方法实例化bean之后,调用该bean的setter方法,即实现了基于setter的依赖注入。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JS0wHxah-1634353769860)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1630976202459.png)]
xml配置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XfrWlI8Q-1634353769860)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1630976221951.png)]
构造器注入:通过容器触发一个类的构造器来实现的,该类由一系列参数,每个参数代表一个对其他类的依赖。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xsaSL7x7-1634353769861)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1630976354305.png)]
xml配置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m4DufSGI-1634353769862)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1630976382048.png)]
注解方式实现
注解开发准备工作:需要的jar包,注解功能封装在AOP包中,导入Spring AOP jar包即可。
开启注解扫描
注解创建对象
@Component(value=“user”)等于
@Service
@Repository
都可以实现创建对象功能,只是为了后续扩展功能,在不同的层使用不同的注解标记
注解方式注入属性
byType自动注入@Autowired
需要在引用属性上使用注解@Autowired,该注解默认使用按类型自动装配Bean的方式。使用该注解完成属性注入时,类中无需setter
byName自动注入@Autowired与@Qualifier
需要在引用属性上联合使用注解@Autowired与@Qualifier。@Qualifier的value属性用与指定要匹配的Bean的id值。
JDK注解@Resource自动注入
Spring提供了对jdk中@Resource注解的支持。@Resource注解既可以按名称匹配Bean,也可以按类型匹配Bean。默认是按名称注入。
byName注入引用类型属性
@Resource注解指定其name属性,则name的值即为按照名称进行匹配的Bean的id。
注解的工作步骤主要由以下几步:
@Autowired//默认按type注入 , Spring提供的注解
@Qualifier(“userDao”)//一般作为@Autowired()的修饰用 按照对象名注入
UserDao userDao;
@Resource(name=“cusInfoService”)//默认按name注入,可以通过name和type属性进行选择性注入, jdk提供
UserDao userDao;
比如权限认证、日志、事务处理。
概念
面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。利用AOP可以堆业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发效率。
spring容器创提供声明式事务;允许用户自定义切面
以下名词需要了解下:
动态代理中,代理类并不是在java代码中实现,而是在运行时期生成,相比静态代理,动态代理可以很方便的对委托类的方法进行统一处理,如添加方法调用次数、添加日志功能等等,动态代理分为JDK动态代理和cglib动态代理。
jdk代理
jdk动态代理是实现方式,是通过反射来实现的,借助Java自带的java.lang.reflect.Proxy,通过固定的规则生成。
步骤
Cglib代理
jdk实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理,这就需要CGLib了。CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用顺势织入横切逻辑。但因为采用的是继承,所以不能对final修饰的类进行代理。JDK动态代理与CGLib动态代理均是实现SpringAOP的基础。
总结:
声明式事务:管理建立在AOP基础上,本质是对方法前后进行拦截,所以声明式事务是方法级别的。
管理方式:基于XML配置;基于注解实现
编程式事务:在项目中很少使用,需要注入一个事务管理对象TransactionTemplate,然后在我们代码中需要提交事务或回滚事务时自己写代码实现。
指的是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。例如:methodA事务方法调用methodB事务方法时,methodB是继续在调用这methodA的事务中运行,还是为自己开启应该新事务运行,这就是由methodB的事务传播行为决定的。
在methodB上加@Transaction(Propagation=XXX)设置决定。
事务传播行为是Spring框架独有的事务增强特性,不属于事务实际提供方数据库行为。
Spring定义了七种传播行为:
事务传播行为类型 | 说明 |
---|---|
PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行 |
PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行,如果当前存在事务,就把当前事务挂起 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
在spring容器中,BeanFactory接口是IOC容器要实现的最基础的接口,定义了管理Bean的最基本方法,例如获取实例、基本的判断等。
BeanFactory有多个子接口来进一步扩展bean相关的功能。
ApplicationContext也间接继承了BeanFactory,如果说BeanFactory是Spring的心脏,那么ApplicationContext就是完整的身躯。他们都可以当作Spring的容器,Spring容器是生成Bean实例的工厂,并管理容器中的Bean。
区别:
**宏观上来讲,**spring Bean的生命周期可以分为5个阶段:
实例化Instantiation
创建对象
属性赋值Populate
给属性注入值
初始化Initialization
初始化bean,根据配置为bean添加额外的功能将bean对象放入容器中,使用
销毁Destruction
细化
实例化一个Bean–也就是我们常说的new;
按照Spring上下文对实例化的Bean进行配置–也就是IOC注入;
(1)如果这个Bean以及实现了BeanNameAware接口,会调用它实现的setBeanName(String)方法,此处传递的就是Spring配置文件中Bean的id值。
(2)如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory,传递的是Spring工厂自身
(3)如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext方法,传入Spring上下文(这个方式可以实现2的内容,比2更好,因为ApplicationContext是BeanFactory的子接口,有更多的实现方法)。
(4)如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessBeforeInitialization方法;(BeanPostProcessor经常被用作是Bean内容的更改,并且由于这个是在Bean初始化结束时调用那个的方法,也可以被应用内存或缓存技术)。
(5)如果Bean在Spring配置文件中配置了init-method属性会自动调用其配置的初始化方法。
(6)如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessAfterInitialization方法。
以上工作完成后就可以应用这个Bean了,这个Bean是一个Singleton(单例模式)的,所以一般情况下我们调用同一个id的Bean会是在内容地址相同的实例。
当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用那个其实现的destory()方法。
最后,如果这个Bean的Spring配置中配置了destory-metnod属性,会自动调用其配置的销毁方法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qJ4wvB8h-1634353769863)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631011210856.png)]
不是线程安全的。
Spring容器中的bean是否线程安全,容器本身并没有提供bean的线程安全策略,因此可以说Spring容器中的Bean本身不具备线程安全的特性,但是具体还是要结合具体scope的Bean情况。
默认是单例
作用域 | 字符 | 描述 |
---|---|---|
单例 | singleton | 整个应用中只会创建一个实例 |
原型 | prototype | 每次注入时都新建一个实例 |
会话 | session | 为每个会话创建一个实例 |
请求 | request | 为每个请求创建一个实例 |
线程安全要从单例与原型分别进行说明
原型Bean
对于原型Bean,每次创建一个新对象,也就是线程之间并不存在Bean共享,自然是不会有线程安全的问题。
单例Bean
对于单例Bean,所有线程都共享一个单例实例Bean,因此是存在资源的竞争。
bean又分为:
如果一个单例Bean是一个无状态的Bean,也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。比如Spring MVC的Controller、Service、Dao等只关注方法本身,如果只是调用里面的方法,而且多线程调用一个实例的方法,会在内存中复制变量,这个是自己线程的工作内存,是安全的。
但是如果Bean是有状态的,那就需要开发人员自己来进行线程安全的保证,最简单的办法就是把Bean的作用域改为protopyte,这样每次请求Bean就相当于是new Bean() 这样就可以保证线程安全了。
scope 单例 原型
Spring默认创建对象是在启动Spring的时候。
把lazy-init=“true”(在bean中配置)后先启动了spring容器,然后就是我们调用该类的时候,spring容器才帮我们创建对象。减少启动Spring的时间,减少web服务器在运行的负担。
意义:tomcat启动时就创建配置文件中的所有bean对象,如果有些类或者配置文件的书写有误,这时候,spring容器就会报错,那么自然spring容器也就启动不起来了。这种情况可以避免,我们到了后面真正要调用该类的时候才报错。当然这种做法,会把一些类有早的加载到内存中。
当我们选择在调用某个类的时候,spring容器才帮我们创建这个类,首先我们可以解决第一种情况出现的问题,节省了内存但是这时候,类和配置文件中许多隐藏的错误,在调用的时候才发现,这时候添加了查错的压力。
A对象依赖了B对象,B对象依赖了A对象。
发生在哪?
是个问题吗?
如果不考虑Spring,循环依赖并不是问题,因为对象之间相互依赖很正常。
但是在Spring中循环依赖就是问题了。
因为,在Spring中一个对象并不是简单new出来的,而是会经过一系列的Bean生命周期,就是因为Bean的生命周期所以才会出现循环依赖问题。
一级缓存
singletonObjects,存放完全实例化且属性赋值完成的 Bean ,可以直接使用
二级缓存
earlySingletonObjects,存放早期 Bean 的引用,尚未装配属性的 Bean
三级缓存
singletonFactories,存放实例化完成的 Bean 工厂
产生循环依赖的问题,主要是:A创建时->需要B->去创建->需要A,从而产生了循环。
A,B循环依赖,先初始化A,先暴露一个半成品A,再去初始化依赖的B,初始化B时如果发现B依赖A,也就是循环依赖,就注入半成品A,之后初始化完毕B,再回到A的初始化过程时就解决了循环依赖,在这里只需要一个Map能缓存半成品A就行了,也就是二级缓存就够了,打不死这份二级缓存存的是Bean对象,如果这个对象存在代理,那应该注入的是代理,而不是Bean,此时二级缓存无法既缓存Bean,又缓存代理,因此三级缓存做到了缓存工厂,也就是生成代理,二级缓存就能解决缓存依赖,三级缓存解决的是代理。
SpringMVC是Spring框架的一个模块,SpringMVC和Spring无需通过中间整合层进行整合。
SpringMVC是一个基于MVC的WEB框架,方便前后数据的传输。
SpringMVC拥有控制器,接受外部请求,解析参数传给服务层。
SpringMVC是一个基于java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把模型-视图-控制器分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几个部分来简化开发。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KnChqC3D-1634353769864)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631237447277.png)]
用户向服务器发送请求,请求被Spring前端控制DispatcherServlet捕获
前端控制器DispatcherServlet接收请求后,调用处理器映射HandlerMapping
处理器映射器HandlerMapping根据请求的url找到处理该请求的处理器Handler,将处理器Handler返回给前端控制器DispatcherServlet。
DispatcherServlet根据获得的Handler,选择一个合适的HandlerAdapter。在填充Handler的入参过程中,根据自己的配置,Spring将帮你做一些额外的工作:
Handler执行完成后,向DispatcherServlet返回一个ModelAndView对象;
根据返回的ModelAndView选择一个适合的ViewResolver返回给DispatcherServlet;
ViewResolver结合Model和View,来渲染视图
将渲染结果返回给客户端
SpringMVC组件
@Controller
在SpringMVC中,Controller主要负责处理前端控制器发过来的请求,经过业务逻辑层处理之后封装成一个model,并将其返回给view进行展示。@Controller注解通常用于类上。
@RequestMapping
注解是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上的注解会将一个特定请求或者请求模式映射到一个控制器上,标识类中所有响应请求的方法都是以该地址作为父路径;方法的级别上注解标识进一步指定到处理方法的映射关系。
@PathVariable
用来获取URL参数。url/{id}
@RequestParam
从Request里获取参数 url?id=1
@RequestBody
用于接收前端传来的实体,将json转换为Java对象。
@ResponseBody注解
将Controller方法返回的对象,转化为Json对象响应给客户端。
**post:**在web.xml中配置一个CharacterEncodingFilter过滤器,设置编码为utf-8
get:在tomcat配置文件中添加与项目工程编码一致的编码类型
实现原理不同
过滤器和拦截器 底层实现方式大不相同,过滤器 是基于函数回调的,拦截器 则是基于Java的反射机制(动态代理)实现的。
使用范围不同
我们看到过滤器 实现的是 javax.servlet.Filter 接口,而这个接口是在Servlet规范中定义的,也就是说过滤器Filter 的使用要依赖于Tomcat等容器,导致它只能在web程序中使用。
触发时机不同
过滤器 和 拦截器的触发时机也不同,我们看下边这张图。
过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。
拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。
拦截的请求范围不同
过滤器几乎可以对所有进入容器的请求起作用,而拦截器只会对Controller中请求或访问static目录下的资源请求起作用。
Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。
Spring Boot 主要有如下优点:
启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:
Spring JavaConfig 是 Spring 社区的产品,它提供了配置 Spring IoC 容器的纯Java 方法。因此它有助于避免使用 XML 配置。使用 JavaConfig 的优点在于:
(1)面向对象的配置。由于配置被定义为 JavaConfig 中的类,因此用户可以充分利用 Java 中的面向对象功能。一个配置类可以继承另一个,重写它的@Bean 方法等。
(2)减少或消除 XML 配置。基于依赖注入原则的外化配置的好处已被证明。但是,许多开发人员不希望在 XML 和 Java 之间来回切换。JavaConfig 为开发人员提供了一种纯 Java 方法来配置与 XML 配置概念相似的 Spring 容器。从技术角度来讲,只使用 JavaConfig 配置类来配置容器是可行的,但实际上很多人认为将JavaConfig 与 XML 混合匹配是理想的。
(3)类型安全和重构友好。JavaConfig 提供了一种类型安全的方法来配置 Spring容器。由于 Java 5.0 对泛型的支持,现在可以按类型而不是按名称检索 bean,不需要任何强制转换或基于字符串的查找。
Spring Boot 默认使用 application.properties 或 application.yml 作为其全局配置文件,我们可以在该配置文件中对各种自动配置属性进行修改,并使之生效。
SpringBoot的自动配置是基于SpringFactories机制实现的。
Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到META-INF/spring.factories配置文件中的所有自动配置类,并对其进行加载,而这些自动配置类都是以AutoConfiguration结尾来命名的,它实际上就是一个JavaConfig形式的Spring容器配置类,它能通过以Properties结尾命名的类中取得在全局配置文件中配置的属性如:server.port,而XxxxProperties类是通过@ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。
加载流程
1、去META-INF 目录下找到这个spring.factories文件
2、通过 文件内指定的类路径,找到 配置类
3、配置类加载进 属性类
4、配置类通过属性类的参数构建一个新的bean
实现自己的start
编写属性类
@ConfigurationProperties(prefix = "redis")
public class RedisProperties {
private Integer port;
private String host;
private String password;
private int index;
//省略了get set 方法
}
之后我们就可以在properties 中 使用 redis.port=
这样子来指定参数了
编写配置类
@Configuration //只有当Jedis 存在的时候 才执行,就是说一定要引入了Jedis的依赖才会执行这个配置 @ConditionalOnClass(Jedis.class) //引入属性类 @EnableConfigurationProperties(RedisProperties.class) public class RedisAutoConfiguration { @Bean //当这个bean不存在的时候才执行,防止重复加载bean @ConditionalOnMissingBean public Jedis jedis(RedisProperties redisProperties) { Jedis jedis = new Jedis(redisProperties.getHost(), redisProperties.getPort()); jedis.auth(redisProperties.getPassword()); jedis.select(redisProperties.getIndex()); return jedis; } }
编写spring.factories文件
在resources 目录下创建入口文件,编写内容
org.springframework.boot.autoconfigure.EnableAutoConfiguration=xyz.xiezihao.myjedis.RedisAutoConfiguration
测试
然后我们新建一个springboot项目,在pom中加入依赖
xyz.xiezihao
redis-start
0.0.1-SNAPSHOT
然后写一个测试文件
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestStartApplicationTests {
@Resource
private Jedis jedis;
@Test
public void contextLoads() {
jedis.set("zhu","大肥猪");
String zhu = jedis.get("zhu");
System.out.println(zhu);
}
}
在 Spring Boot 里面,可以使用以下几种方式来加载配置。
1)properties文件;
2)YAML文件;
3)系统环境变量;
4)命令行参数;
等等……
YAML 是一种人类可读的数据序列化语言。它通常用于配置文件。与属性文件相比,如果我们想要在配置文件中添加复杂的属性,YAML 文件就更加结构化,而且更少混淆。可以看出 YAML 具有分层配置数据。
YAML 现在可以算是非常流行的一种配置文件格式了,无论是前端还是后端,都可以见到 YAML 配置。那么 YAML 配置和传统的 properties 配置相比到底有哪些优势呢?
配置有序,在一些特殊的场景下,配置有序很关键
支持数组,数组中的元素可以是基本数据类型也可以是对象
简洁
相比 properties 配置文件,YAML 还有一个缺点,就是不支持 @PropertySource 注解导入自定义的 YAML 配置。
Spring Boot 是否可以使用 XML 配置 ?
Spring Boot 推荐使用 Java 配置而非 XML 配置,但是 Spring Boot 中也可以使用 XML 配置,通过 @ImportResource 注解可以引入一个 XML 配置。
spring boot 核心配置文件是什么?bootstrap.properties 和 application.properties 有何区别 ?
单纯做 Spring Boot 开发,可能不太容易遇到 bootstrap.properties 配置文件,但是在结合 Spring Cloud 时,这个配置就会经常遇到了,特别是在需要加载一些远程配置文件的时侯。
spring boot 核心的两个配置文件:
bootstrap (. yml 或者 . properties):boostrap 由父 ApplicationContext 加载的,比 applicaton 优先加载,配置在应用程序上下文的引导阶段生效。一般来说我们在 Spring Cloud Config 或者 Nacos 中会用到它。且 boostrap 里面的属性不能被覆盖;
application (. yml 或者 . properties): 由ApplicatonContext 加载,用于 spring boot 项目的自动化配置。
Spring Profiles 允许用户根据配置文件(dev,test,prod 等)来注册 bean。因此,当应用程序在开发中运行时,只有某些 bean 可以加载,而在 PRODUCTION中,某些其他 bean 可以加载。假设我们的要求是 Swagger 文档仅适用于 QA 环境,并且禁用所有其他文档。这可以使用配置文件来完成。Spring Boot 使得使用配置文件非常简单。
为了在自定义端口上运行 Spring Boot 应用程序,您可以在application.properties 中指定端口。server.port = 8090
安全
为了实现 Spring Boot 的安全性,我们使用 spring-boot-starter-security 依赖项,并且必须添加安全配置。它只需要很少的代码。配置类将必须扩展WebSecurityConfigurerAdapter 并覆盖其方法。
由于 Spring Boot 官方提供了大量的非常方便的开箱即用的 Starter ,包括 Spring Security 的 Starter ,使得在 Spring Boot 中使用 Spring Security 变得更加容易,甚至只需要添加一个依赖就可以保护所有的接口,所以,如果是 Spring Boot 项目,一般选择 Spring Security 。当然这只是一个建议的组合,单纯从技术上来说,无论怎么组合,都是没有问题的。Shiro 和 Spring Security 相比,主要有如下一些特点:
Spring Security 是一个重量级的安全管理框架;Shiro 则是一个轻量级的安全管理框架
Spring Security 概念复杂,配置繁琐;Shiro 概念简单、配置简单
Spring Security 功能强大;Shiro 功能简单
CSRF 代表跨站请求伪造。这是一种攻击,迫使最终用户在当前通过身份验证的Web 应用程序上执行不需要的操作。CSRF 攻击专门针对状态改变请求,而不是数据窃取,因为攻击者无法查看对伪造请求的响应。
监视器
Spring boot actuator 是 spring 启动框架中的重要功能之一。Spring boot 监视器可帮助您访问生产环境中正在运行的应用程序的当前状态。有几个指标必须在生产环境中进行检查和监控。即使一些外部应用程序可能正在使用这些服务来向相关人员触发警报消息。监视器模块公开了一组可直接作为 HTTP URL 访问的REST 端点来检查状态。
默认情况下,所有敏感的 HTTP 端点都是安全的,只有具有 ACTUATOR 角色的用户才能访问它们。安全性是使用标准的 HttpServletRequest.isUserInRole 方法实施的。 我们可以使用来禁用安全性。只有在执行机构端点在防火墙后访问时,才建议禁用安全性。
我们如何监视所有 Spring Boot 微服务?
Spring Boot 提供监视器端点以监控各个微服务的度量。这些端点对于获取有关应用程序的信息(如它们是否已启动)以及它们的组件(如数据库等)是否正常运行很有帮助。但是,使用监视器的一个主要缺点或困难是,我们必须单独打开应用程序的知识点以了解其状态或健康状况。想象一下涉及 50 个应用程序的微服务,管理员将不得不击中所有 50 个应用程序的执行终端。为了帮助我们处理这种情况,我们将使用位于的开源项目。 它建立在 Spring Boot Actuator 之上,它提供了一个 Web UI,使我们能够可视化多个应用程序的度量。
前后端分离开发日益流行,大部分情况下,我们都是通过 Spring Boot 做前后端分离开发,前后端分离一定会有接口文档,不然会前后端会深深陷入到扯皮中。一个比较笨的方法就是使用 word 或者 md 来维护接口文档,但是效率太低,接口一变,所有人手上的文档都得变。在 Spring Boot 中,这个问题常见的解决方案是 Swagger ,使用 Swagger 我们可以快速生成一个接口文档网站,接口一旦发生变化,文档就会自动更新,所有开发工程师访问这一个在线网站就可以获取到最新的接口文档,非常方便。
我们都知道,新创建一个 Spring Boot 项目,默认都是有 parent 的,这个 parent 就是 spring-boot-starter-parent ,spring-boot-starter-parent 主要有如下作用:
定义了 Java 编译版本为 1.8 。
使用 UTF-8 格式编码。
继承自 spring-boot-dependencies,这个里边定义了依赖的版本,也正是因为继承了这个依赖,所以我们在写依赖时才不需要写版本号。
执行打包操作的配置。
自动化的资源过滤。
自动化的插件配置。
针对 application.properties 和 application.yml 的资源过滤,包括通过 profile 定义的不同环境的配置文件,例如 application-dev.properties 和 application-dev.yml。
Spring Boot 打成的 jar 和普通的 jar 有什么区别 ?
Spring Boot 项目最终打包成的 jar 是可执行 jar ,这种 jar 可以直接通过 java -jar xxx.jar 命令来运行,这种 jar 不可以作为普通的 jar 被其他项目依赖,即使依赖了也无法使用其中的类。
Spring Boot 的 jar 无法被其他项目依赖,主要还是他和普通 jar 的结构不同。普通的 jar 包,解压后直接就是包名,包里就是我们的代码,而 Spring Boot 打包成的可执行 jar 解压后,在 \BOOT-INF\classes 目录下才是我们的代码,因此无法被直接引用。如果非要引用,可以在 pom.xml 文件中增加配置,将 Spring Boot 项目打包成两个 jar ,一个可执行,一个可引用。
1)打包用命令或者放到容器中运行
2)用 Maven/ Gradle 插件运行
3)直接执行 main 方法运行
可以不需要,内置了 Tomcat/ Jetty 等容器。
1)继承spring-boot-starter-parent项目
2)导入spring-boot-dependencies项目依赖
Spring 提供了一种使用 ControllerAdvice 处理异常的非常有用的方法。 我们通过实现一个 ControlerAdvice 类,来处理控制器类抛出的所有异常。
使用 Spring Boot 实现分页非常简单。使用 Spring Data-JPA 可以实现将可分页的传递给存储库方法。
在微服务中,一个完整的项目被拆分成多个不相同的独立的服务,各个服务独立部署在不同的服务器上,各自的 session 被从物理空间上隔离开了,但是经常,我们需要在不同微服务之间共享 session ,常见的方案就是 Spring Session + Redis 来实现 session 共享。将所有微服务的 session 统一保存在 Redis 上,当各个微服务对 session 有相关的读写操作时,都去操作 Redis 上的 session 。这样就实现了 session 共享,Spring Session 基于 Spring 中的代理过滤器实现,使得 session 的同步操作对开发人员而言是透明的,非常简便。
定时任务也是一个常见的需求,Spring Boot 中对于定时任务的支持主要还是来自 Spring 框架。
在 Spring Boot 中使用定时任务主要有两种不同的方式,一个就是使用 Spring 中的 @Scheduled 注解,另一个则是使用第三方框架 Quartz。
使用 Spring 中的 @Scheduled 的方式主要通过 @Scheduled 注解来实现。
使用 Quartz ,则按照 Quartz 的方式,定义 Job 和 Trigger 即可。
缺点
虽然Spring的组件代码是轻量级的,但它的配置确实重量级的。虽然Spring引入了注解功能,但是仍然需要编写大量的模板化配置文件。
项目的依赖管理也是一件耗时耗力的事情,在环境搭建时,需要分析要导入大量库的坐标,而且还需要分析导入与之有依赖关,一旦选错依赖的版本,随之而来的不兼容问题就会严重阻碍项目的开发进度。
SpringBoot对上述Spring的缺点进行的改善和优化,基于约定优于配置的思想,可以让开发人员不必在配置与逻辑业务之间进行思维的切换,全身心投入到逻辑业务的代码编写中,从而大大提高了开发的效率,一定程度上缩短了项目周期。
在spring的基础上进行开发的,
解决spring问题: 依赖多,模板化的配置
搭建非常方便
起步依赖 直接将相关的jar包直接进行依赖
自动装配
SpringBoot本身并不提供Spring框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于Spring框架的应用程序。也就是说,它并不是用来代替Spring的解决方案,而是和Spring框架紧密结合用于提升Spring开发者体验的工具。
SpringBoot以约定大于配置的核心思想,从而使开发人员不再需要定义样板化的配置。使他集成了大量常用的第三方库配置(如Redis,MongoDB,Jpa RabbitMQ,Quartz等等),SpringBoot应用中这些第三方库几乎可以零配置的开箱即用,通过这种方式,SpringBoot致力于在蓬勃发展的快速应用开发领域成为领导者。
SpringBoot你只需要“run”就可以非常轻易的构建独立的、生产级别的Spring应用。
创建独立的Spring应用程序;
直接内嵌tomcat、jetty和undertow
提供了固定化的“starter”配置,以简化构建配置;
尽可能的自动配置Spring和第三方库;
提供产品级的功能,如:安全指标,运行状况监测和外部化配置等;
绝对不会生成代码,并且不需要XML配置。
1.SpringBoot的核心功能
(1)起步依赖
起步依赖就是将具备某种功能的坐标打包在一起,并提供一些默认的功能。
(2)自动配置
SpringBoot的自动配置是一个运行时(更准确地说,是应用程序启动时)的过程,考虑了众多因素,才决定Spring配置应该用哪个,不该用哪个。该过程是Spring自动完成的。
@Component用在类上 创建该类的对象,并管理
@Bean用在方法
public User {
return new User();
}
Json web token(JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。 因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。
(1)传统的session认证
http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。
但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来。
(2)基于session认证所显露的问题
Session:每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
扩展性:用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求再这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
CSRF(跨站请求伪造):因为是基于cookie来进行用户识别的,cookie如果被截获,用户很容易受到跨站请求伪造的攻击。
(3)基于token的鉴别机制
基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。
流程上是这样的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PCsG0Mka-1634353769867)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631587773413.png)]
用户使用账号和密码发出post请求;
服务器使用私钥创建一个jwt;
服务器返回这个jwt给浏览器;
浏览器将该jwt串在请求头中向服务器发送请求;
服务器验证该jwt;
返回响应的资源给浏览器。
身份认证这种场景下,一旦用户完成了登录,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO) 中比较广泛的使用了该技术。信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。
(1)优点
简洁(Compact):可以通过URL,POST参数或者在HTTP haeder发送,因为数据量小,传输速度也很快;
自包含(Self-contained):负载中包含了所有的用户所需要的信息,避免了多次查询数据库;
因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持;
不需要在服务器端保存会话信息,特别适用于分布式微服务。
3.JWT的构成
JWT是由三段信息构成的,将这三段信息文本用.连接一起就构成了JWT字符串。如:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiw ibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab3 0RMHrHDcEfxjoYZgeFONFh7HgQ
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload,用户的信息),第三部分是签证(signature)。
(1)第一部分
header
jwt的头部承载两部分信息:
声明类型,这里是jwt
声明加密算法 通常直接使用HMAC HS256
完整的头部就像下面的JSON:
{
‘typ’: ‘JWT’,
‘alg’: ‘HS256’
}
然后将头部进行base64转码,构成了第一部分:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
10010101 01010101 100101 010101 010100
(2)第二部分
payload
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:
标准中注册的声明;
公共的声明(公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务,需要的必要信息,但不建议加敏感信息,如密码,因为该部分在客户端可解密);
私有的声明。
定义一个payload:
{
“sub”: “1234567890”,
“name”: “John Doe”,
“admin”: true
}
然后将其进行base64转码,得到jwt的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4i OnRydWV9
(3)第三部分
signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header(base64后的)
payload(base64后的)
secret
这个部分需要base64转码后的header和base64转码后的payload使用,连接组成的字符串,然后通过header中声明的加密方式进行加密secret组合加密,然后就构成了jwt的第三部分。
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
虚拟机就是一台虚拟的计算机。通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的计算机系统。它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机(例如:VMware)和程序虚拟机(例如:java虚拟机)。无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源。
VMware
完全对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。
java虚拟机(JVM)
专门为执行某个单个计算机程序而设计。Java虚拟机是一种执行java字节码文件的虚拟计算机,它拥有独立的运行机制。
java技术的核心就是java虚拟机,因为所有的java程序都运行在java虚拟机内部。
java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器码指令执行,每一条java指令,java虚拟机中都有详细定义,如怎么去操作数,怎么处理操作数,处理结果放在哪。
主要功能:
现在的JVM不仅可以执行java字节码文件,还可以执行其他语言编译后的字节码文件,是一个跨语言平台。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gFeLVLYZ-1634353769868)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631765807309.png)]
JVM是运行在操作系统之上的,它与硬件没有直接的交互。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M87EteIN-1634353769868)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631765967233.png)]
过程:程序在执行之前先要把java代码转换成字节码(class文件),jvm首先需要把字节码通过一定的方式类加载器把文件加载到内存中运行时数据区,而字节码文件是jvm的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎将字节码翻译成底层系统指令再交有CPU去执行,而这个过程中需要调用其他语言的接口本地库接口来实现整个程序的功能,这就是这4个主要组成部分的职责与功能。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iyPD7G8d-1634353769869)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631765990422.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6j3BhBvL-1634353769869)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631766008532.png)]
java编译器编译过程中,任何一个节点执行失败就会造成编译失败。虽然各个平台的java虚拟机内部实现细节不尽相同,但是它们执行的字节码内容却是一样的。
JVM主要任务就是负责将字节码装载到其内部,解释/编译为对应平台上的机器指令执行。JVM使用类加载器装载class文件。
类加载完成后,会进行字节码校验,字节码校验通过之后JVM解释器会把字节码翻译成机器码交由操作系统执行。
但不是所有的代码都是解释执行,JVM对此做了优化,比如HoySpot虚拟机,它本身提供了JIT。
java编译器输入的指令流基本上是一种基于栈的指令集架构。
两种架构特点
1.基于栈式架构的特点:
2.基于寄存器式架构特点:
由于跨平台的设计,java指令集都是根据栈来设计的,不同CPU架构不同,所以不能设计为基于寄存器的。
面试题:时至今日,HotSpot虚拟机的宿主环境已经不局限于嵌入式平台了,那么为什么不将架构更换为性能更好的寄存器指令集架构呢?
答:两种指令集架构各有优劣,并存发展。首先,在设计和实现上,基于栈式的架构更简单。其次无论平台的资源是否受限制,基于栈式的架构都是可以使用的。(针对栈式的优点,可以继续balabala…)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DIKNsNvG-1634353769870)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631766032770.png)]
类加载器子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的文件标识。
当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这3步骤统称为类加载或类初始化。
类被加载到 JVM 开始,到卸载出内存,整个生命周期如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GyG6O4QC-1634353769870)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631766045934.png)]
就是将已经读入内存的类的二进制数据合并到JVM运行时环境中去,包含以下步骤:
验证
检验被加载的类是否有正确的内部结构,并和其他类协调一致。
准备
准备阶段则负责为类的静态属性分配内存,并设置默认初始值;不包含final修饰的static实例变量,在编译时进行初始化。不会为实例变量初始化。
解析
将类的二进制数据中的符号引用替换成直接引用。
类什么时候初始化?
注意:对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。
类的初始化顺序
对static修饰的变量或语句块进行赋值。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
顺序是:父类static -> 子类static -> 父类构造方法 -> 子类构造方法
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2E0r8yPz-1634353769871)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631766060018.png)]
自定义类加载器(User-Defined ClassLoader)
从概念上来讲,自定义类加载器一般指的是程序汇总有开发人员自定义的一类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
扩展类加载器(Extension ClassLoader)
Java语言编写的,由sun.misc.Launcher$ExtClassLoader实现,父类加载器为null。
派生于ClassLoader类。
上层类加载器为引导类加载器。
它负责加载JRE的扩展目录。
从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK系统安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的jar放在此目录下,也会自动由扩展类加载器加载。
应用程序类加载器(系统类加载器 Application ClassLoader)
Java语言编写的,由sun.misc.Launcher$AppClassLoader实现,父类加载器为ExtClassLoader。
派生于ClassLoader类。
上层类加载器为扩展类加载器。
加载我们自己定义的类。
该类加载器是程序中默认的类加载器。
通过类名.class.getClassLoader(),ClassLoader.getSystemClassLoader()来获得。
引导类加载器(启动类加载器/根类加载器)(Bootstrap ClassLoader)
这个类加载器使用C/C++语言实现,嵌套在JVM内部。用来加载Java核心类库。
并不继承于java.lang.ClassLoader没有父加载器。
负责加载扩展类加载器和应用类加载器,并为它们指定父类加载器。
出于安全考虑,引用类加载器只加载器包名为java,javax,sun等开头的类。
注意:ClassLoader类,它是一个抽象类,其后所有类加载器都继承自ClassLoader(不包括启动类加载器)
类加载器加载Class大致要经过如下8个步骤:
JVM的类加载机制主要有3种
细讲一下双亲委派机制(面试):
工作原理:
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器区执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父加载器无法完成此加载任务,子加载器才会尝试自己去加载,如果均加载失败,就会抛出ClassNotFoundException异常,这就是双亲委派模式。即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了了时,儿子自己才想办法去完成。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RQ7V7q9D-1634353769872)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1631766079714.png)]
双亲委派优点:
作用:防止恶意代码污染java源代码。
比如我们定义了一个类名为String所在包也命名为java.lang,因为这个类本来属于jdk的,如果没有沙箱安全机制,这个类将会污染到系统中的String,但是由于沙箱安全机制,所以就委托顶层的引导类加载器查找这个类,如果没有的话就委托给扩展类加载器,再没有就委托到系统类加载器。但是由于String就是jdk源代码,所以在引导类加载器那里就加载到了,先找到先使用,所以就使用引导类加载器里面的String,后面的一概不能使用,这就保证了不被恶意代码污染。
JVM规定,每个类或者接口被首次主动使用时才对其进行初始化,有主动使用,自然就有被动使用。
主动使用:
被动使用:
除了上面的几种主动使用其余就是被动使用了。
引用该类的静态常量,不会导致初始化,但是也有意外,这里的常量是指已经指定字面量的常量,对于那些需要一些计算才能得出结果的常量就会导致初始化。