赞
踩
参考:https://blog.csdn.net/bo123_/article/details/88605984
Java 是面向对象的编程语言,对象就是面向对象程序设计的核心。其基本思想是使用对象、类、继承、封装、多态等基本概念来进行程序设计。从现实世界中客观存在的事物(即对象)出发来构造软件系统,并且在系统构造中尽可能运用人类的自然思维方式。所谓对象就是真实世界中的实体,对象与实体是一一对应的,也就是说现实世界中每一个实体都是一个对象,它是一种具体的概念。对象有以下特点:
简单来说,我们自然世界中的每一个个体都是一个对象,而每一个个体又都不一样,有的黑皮肤,有的黄皮肤,有的白皮肤,有的长头发,有的短头发,高矮胖瘦各不相同。但是因为我们都有相同的一些特性,比如会讲话,有手有脚,有五官,是哺乳动物,正因为我们这些共同点,所以我们共同属于一个种类——人类。人类,就是人的总称,也是相似对象的一种抽象。
Java之所以可以“一次编译,到处运行”,一是因为JVM针对各种操作系统、平台都进行了定制,二是因为无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用。因此,也可以看出字节码对于Java生态的重要性。
之所以被称之为字节码,是因为字节码文件由十六进制值组成,而JVM以两个十六进制值为一组,即以字节为单位进行读取。在Java中一般是用javac命令编译源代码为字节码文件,一个.java文件从编译到运行的示例如图所示。
Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。
Java 语言的数据类型分为两种:基本数据类型和引用数据类型。
基本数据类型包括 boolean(布尔型)、float(单精度浮点型)、char(字符型)、byte(字节型)、short(短整型)、int(整型)、long(长整型)和 double (双精度浮点型)共 8 种,如下图所示。
引用数据类型建立在基本数据类型的基础上,包括数组、类和接口。引用数据类型是由用户自定义,用来限制其他数据的类型。另外,Java 语言中不支持 C++ 中的指针类型、结构类型、联合类型和枚举类型。
参考:https://blog.csdn.net/dianzijinglin/article/details/52251868
java中的修饰符分为类修饰符,字段修饰符,方法修饰符。
权限访问修饰符有public,protected,default,private,这四种级别的修饰符都可以用来 修饰类、方法和字段。
final修饰符
final的意思是不可变,他可以修饰类、字段、方法。
abstract修饰符
abstract是抽象的意思,用来修饰类和方法。
static修饰符
static用来修饰 内部类,方法,字段。
public class StaticClassTest {
public static void main(String[] args) {
//静态内部类可以直接new
StaticInner si=new StaticInner();
//非静态内部类需创建一个父类的实例,方能new
StaticClassTest sct=new StaticClassTest();
Inner i=sct.new Inner();
}
class Inner{
}
static class StaticInner{
}
}
多态本质上多态分两种:
重载(overload)就是编译时多态的一个例子,编译时多态在编译时就已经确定,运行的时候调用的是确定的方法。
我们通常所说的多态指的都是运行时多态,也就是编译时不确定究竟调用哪个具体方法,一直延迟到运行时才能确定。这也是为什么有时候多态方法又被称为延迟方法的原因。
Java实现多态有 3 个必要条件:继承、重写和向上转型。
构造器不能被继承,因此不能被重写,但可以被重载。每一个类必须有自己的构造函数,负责构造自己这部分的构造。子类不会覆盖父类的构造函数,相反必须一开始调用父类的构造函数。
抽象类
抽象类就是为了继承而存在的,如果你定义了一个抽象类,却不去继承它,那么等于白白创建了这个抽象类,因为你不能用它来做任何事情。
对于一个父类,如果它的某个方法在父类中实现出来没有任何意义,必须根据子类的实际需求来进行不同的实现,那么就可以将这个方法声明为abstract方法,此时这个类也就成为abstract类了。
包含抽象方法的类称为抽象类,但并不意味着抽象类中只能有抽象方法,它和普通类一样,同样可以拥有成员变量和普通的成员方法。
注意,抽象类和普通类的主要有三点区别:
在其他方面,抽象类和普通的类并没有区别。
接口
接口中可以含有 变量和方法,但是要注意:
从这里可以隐约看出接口和抽象类的区别,接口是一种极度抽象的类型,它比抽象类更加“抽象”,并且一般情况下不在接口中定义变量。
不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类
== 常用于相同的基本数据类型之间的比较,也可用于相同类型的对象之间的比较
equals方法主要用于两个对象之间,检测一个对象是否等于另一个对象
看一看Object类中equals方法的源码:
public boolean equals(Object obj) {
return (this == obj);
}
它的作用也是判断两个对象是否相等,般有两种使用情况:
hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。hashcode是一个native方法 。
/**
* Returns a hash code value for the object. This method is
* supported for the benefit of hash tables such as those provided by
* {@link java.util.HashMap}.
*/
hashCode() 的作用是获取 哈希码,也称为 散列码 ;它实际上是返回一个 int整数 。
以 HashMap如何检查重复 为例子来说明为什么要有 hashCode:
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
如果两个hash值不相等,则说明key不是重复的;如果hash相同,那么会进一步比较 == 或 equals() 。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
不可变对象 指对象一旦被创建,状态就不能再改变,任何修改都会创建一个新的对象 ,如 String、Integer及其它包装类.不可变对象最大的好处是线程安全。
String、StringBuffer、StringBuilder
String类中使用字符数组保存字符串,因为有“final”修饰符,所以 string对象是不可变的。对于已经存在的String对象的修改都是重新创建一个新的对象,然后把新的值保存进去。
/** The value is used for character storage. */
private final char value[];
StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,这两种对象都是 可变的。
char[] value;
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
如果只是在单线程中使用字符串缓冲区,那么StringBuilder的效率会更高些
。值得注意的是StringBuilder是在JDK1.5版本中增加的。以前版本的JDK不能使用该类。
便于实现字符串池(String pool)
多线程安全
在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全。
避免安全问题
在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。
加快字符串处理速度
由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。
总体来说,String不可变的原因要包括 设计考虑,效率优化,以及安全性 这三大方面。
参考:http://tangxman.github.io/2015/07/27/the-difference-of-java-string-pool/
java中常量池的概念主要有三个: 全局字符串常量池, class文件常量池, 运行时常量池。我们现在所说的就是全局字符串常量池。
JVM为了提升性能和减少内存开销,避免字符的重复创建,其维护了一块特殊的内存空间,即字符串池。当需要使用字符串时,先去字符串池中查看该字符串是否已经存在,如果存在,则可以直接使用,如果不存在,初始化,并将该字符串放入字符串常量池中。
字符串常量池的位置也是随着JDK版本的不同而位置不同。
在jdk1.7及其以后,全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。
在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。
String b = new String("aaa");
,程序会在堆内存中开辟一片新空间存放新对象,同时会将”aaa”字符串放入常量池, 相当于创建了两个对象
,无论常量池中有没有”aaa”字符串,程序都会在堆内存中开辟一片新空间存放新对象
。@Test
public void test(){
// String a = "2";
// String b = "2";
// System.out.println(a == b); // 1.7 true
String a = new String("2");
String b = "2";
System.out.println(a == b); // 1.7 false
String s = new String("2");
s.intern();
String s2 = "2";
System.out.println(s == s2); // 1.6 false 1.7 false
String s3 = new String("3") + new String("3");
s3.intern();
String s4 = "33";
System.out.println(s3 == s4); // 1.6 false 1.7 true
}
运行结果:
jdk6
false
false
jdk7
false
true
intern()函数 :intern函数的作用是将对应的符号常量进入特殊处理,在JDK1.6以前 和 JDK1.7以后有不同的处理:
String s = new String("2");
s.intern();
String s2 = "2";
System.out.println(s == s2); // 1.6 false 1.7 false
不是。Java 中的基本数据类型只有 8 个 :byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(referencetype),Java 5 以后引入的枚举类型也算是一种比较特殊的引用类型。
HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。
Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是 Integer。从 Java 5 开始引入了自动装箱/拆箱机制
Java 为每个原始类型提供了包装类型:
基本类型和包装类型的区别主要有以下 几点:
自动装箱:将基本数据类型重新转化为对象
// 声明一个Integer对象,用到了自动的装箱:解析为:Integer num = Integer.valueOf(9);
Integer num = 9;
9是属于基本数据类型的,原则上它是不能直接赋值给一个对象Integer的。但jdk1.5 开始引入了自动装箱/拆箱机制,就可以进行这样的声明,自动将基本数据类型转化为对应的封装类型,成为一个对象以后就可以调用对象所声明的所有的方法。
自动拆箱:将对象重新转化为基本数据类型
/ /声明一个Integer对象
Integer num = 9;
// 进行计算时隐含的有自动拆箱
System.out.print(num--);
因为 对象时不能直接进行运算的,而是要转化为基本数据类型后才能进行加减乘除
。
由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。
Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)。
int a = 10000;
Integer b = new Integer(10000);
Integer c=10000;
System.out.println(a == b); // true
System.out.println(a == c); // true
非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。
两者在内存中的地址不同。
Integer b = new Integer(10000);
Integer c=10000;
System.out.println(b == c); // false
对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false。
Integer i = 100;
Integer j = 100;
System.out.print(i == j); //true
Integer i = 128;
Integer j = 128;
System.out.print(i == j); //false
当值在 -128 ~ 127之间时,java会进行自动装箱,然后会对值进行缓存,如果下次再有相同的值,会直接在缓存中取出使用。缓存是通过Integer的内部类IntegerCache来完成的。当值超出此范围,会在堆中new出一个对象来存储。
/**
* (1)在-128~127之内:静态常量池中cache数组是static final类型,cache数组对象会被存储于静态常量池中。
* cache数组里面的元素却不是static final类型,而是cache[k] = new Integer(j++),
* 那么这些元素是存储于堆中,只是cache数组对象存储的是指向了堆中的Integer对象(引用地址)
* (2)在-128~127 之外:新建一个 Integer对象,并返回。
*/
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high) {
return IntegerCache.cache[i + (-IntegerCache.low)];
}
return new Integer(i);
}
反射是在运行状态中:
这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。
Class.forName(“类的路径”);当你知道该类的全路径名时,你可以使用该方法获取 Class 类对象。
Class clz = Class.forName("java.lang.String");
类名.class。这种方法只适合在编译前就知道操作的 Class。
Class clz = String.class;
对象名.getClass()
String str = new String("Hello");
Class clz = str.getClass();
如果是基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象。
使用反射获取一个对象的步骤:
Class clz = Class.forName("com.zhenai.api.Apple");
Constructor appleConstructor = clz.getConstructor();
Object appleObj = appleConstructor.newInstance();
而如果要调用某一个方法,则需要经过下面的步骤:
Method setPriceMethod = clz.getMethod("setPrice", int.class);
setPriceMethod.invoke(appleObj, 14);
Class actionClass=Class.forName(“MyClass”);
Object action=actionClass.newInstance();
Method method = actionClass.getMethod(“myMethod”,null);
method.invoke(action,null);
这份Class对象里维护着该类的所有Method,Field,Constructor的cache,这份cache也可以被称作根对象
。参考:https://blog.csdn.net/qq_41701956/article/details/123473592
Java 泛型(generics)是 JDK 5 中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,即给类型指定一个参数,其在编译时才确定具体的参数。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
泛型有三种使用方式,分别为:泛型类、泛型接口和泛型方法。
public class 类名 <泛型类型1,...> {
}
泛型本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间「擦除」泛型语法并相应的做出一些类型转换动作。
Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类 Exception (异常)和 Error (错误)
。
Exception 和 Error 二者都是 Java 异常处理的重要子类,各自都包含大量子类。
I/O 模型简单的理解:就是 用什么样的通道进行数据的发送和接收
,很大程度上决定了程序通信的性能。
Java共支持3种网络编程模型/IO模式:BIO
、NIO
、AIO
。
Java BIO
:同步并阻塞(传统阻塞型)
,服务器实现模式为 一个连接对应一个线程
,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
Java NIO
: 同步非阻塞
,服务器实现模式为 一个线程处理多个请求(连接)
,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。
Java AIO(NIO.2)
:异步非阻塞
,AIO 引入 异步通道
的概念,采用了 Proactor 模式
,简化了程序编写,有效的请求才启动线程
,它的特点是:先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
BIO、NIO、AIO适用场景分析
连接数目比较小且固定的架构
,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。连接数目多且连接比较短(轻操作)的架构
,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4
开始支持。连接数目多且连接比较长(重操作)的架构
,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7
开始支持。流
的方式处理数据,而 NIO 以 块
的方式处理数据,块 I/O 的效率比流 I/O 高很多;阻塞
的,NIO 则是 非阻塞
的字节流和字符流
进行操作,而 NIO 基于 Channel(通道)
和 Buffer(缓冲区)
进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道System.out.println(4.0 - 3.6);
运行结果:0.3999999999999999
这种舍入误差的主要原因是浮点数值采用二进制系统表示,而在二进制系统中,无法精确的表示分数1/10,就像是十进制无法精确的标识1/3(0.333333…)一样。
计算过程解析
将十进制的 4.0 转换成 二进制,将十进制的 3.6 转换成二进制:
BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);
System.out.println(x); /* 0.1 */
System.out.println(y); /* 0.1 */
System.out.println(Objects.equals(x, y)); /* true */
懒汉式:双重锁检查机制
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这里涉及到两个常见问题:
问题一:为什么要 double-check?去掉第二次的 check 行不行?
有两个线程同时调用 getInstance 方法,并且 singleton 是空的:
假设没有第二层 if,那么线程 2 也可能会创建一个新实例,这样就破坏了单例,所以第二层 if 肯定是需要的。
而对于第一个 check 而言,如果去掉它,那么所有线程都只能串行执行,效率低下,所以两个 check 都是需要保留。
问题二:为什么要加volatile?
这是因为 new 一个对象的过程,其实并不是原子的,至少包括以下这 3 个步骤:
这里需要留意一下这 3 个步骤的顺序,因为指令重排,所以上面所说的三个步骤的顺序,并不是固定的。虽然看起来是 1-2-3 的顺序,但是在实际执行时,也可能发生 1-3-2 的情况,也就是说,先把 singleton 对象指向在第一步中分配的内存空间,再调用。
如果发生了 1-3-2 的情况,线程 1 首先执行新建实例的第一步,也就是分配单例对象的内存空间,然后线程 1 因为被重排序,所以去执行了新建实例的第三步,也就是把 singleton 指向之前的内存地址,在这之后对象不是 null,可是这时第 2 步并没有执行。假设这时线程 2 进入 getInstance 方法,由于这时 singleton 已经不是 null 了,所以会通过第一重检查并直接返回 singleton 对象并使用,但其实这时的 singleton 并没有完成初始化,所以使用这个实例的时候会报错。
最后,线程 1“姗姗来迟”,才开始执行新建实例的第二步——初始化对象,可是这时的初始化已经晚了,因为前面已经报错了。
使用volatile可以防止刚讲到的重排序的发生,也就避免了拿到没完成初始化的对象。
public class Singleton {
//定义一个私有的静态的 Singleton 变量,并进行初始化赋值(创建一个对象给变量赋值)
private static Singleton singleton = new Singleton();
//私有空参数构造方法,不让用户直接创建对象
private Singleton(){}
//定义一个公共的静态方法,返回 Singleton 对象
public static Singleton getInstance(){
return singleton;
}
}
public class Singleton {
private static Singleton singleton;
static {
singleton = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return singleton;
}
}
程序设计语言中有关参数传递给方法(或函数)的两个专业术语:
按值调用表示方法接收的是调用着提供的值,而按引用调用则表示方法接收的是调用者提供的变量地址(如果是C语言的话来说就是指针啦,当然java并没有指针的概念)。
这里我们需要注意的是一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值,这句话相当重要,这是按值调用与引用调用的根本区别。
java中并不存在引用调用,这点是没错的,因为java程序设计语言确实是采用了按值调用,即call by value。也就是说方法得到的是所有参数值的一个拷贝,方法并不能修改传递给它的任何参数变量的内容。
详细参考:https://blog.csdn.net/vtopqx/article/details/107339897
/**
* java中的按值调用
*/
public class CallByValue {
private static int x=10;
public static void updateValue(int value){
value = 3 * value;
}
public static void main(String[] args) {
System.out.println("调用前x的值:"+x);
updateValue(x);
System.out.println("调用后x的值:"+x);
}
}
运行程序,结果如下:
调用前x的值:10
调用后x的值:10
可以看到x的值并没有变化,接下来我们一起来看一下具体的执行过程:
分析:
结论:当传递方法参数类型为基本数据类型(数字以及布尔值)时,一个方法是不可能修改一个基本数据类型的参数。
声明一个User对象类型:
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name=name;
this.age=age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
执行类如下:
/**
* java中的按值调用
*/
public class CallByValue {
private static User user=null;
public static void updateUser(User student){
student.setName("Lishen");
student.setAge(18);
}
public static void main(String[] args) {
user = new User("zhangsan",26);
System.out.println("调用前user的值:"+user.toString());
updateUser(user);
System.out.println("调用后user的值:"+user.toString());
}
}
运行结果如下:
调用前user的值:User [name=zhangsan, age=26]
调用后user的值:User [name=Lishen, age=18]
很显然,User的值被改变了,也就是说方法参数类型如果是引用类型的话,引用类型对应的值将会被修改,下面我们来分析一下这个过程:
过程分析:
结论:当传递方法参数类型为引用数据类型时,一个方法将修改一个引用数据类型的参数所指向对象的值。
虽然到这里两个数据类型的传递都分析完了,也明白的基本数据类型的传递和引用数据类型的传递区别,前者将不会修改原数据的值,而后者将会修改引用所指向对象的值。可通过上面的实例我们可能就会觉得java同时拥有按值调用和按引用调用啊,可惜的是这样的理解是有误导性的,虽然上面引用传递表面上体现了按引用调用现象,但是java中确实只有按值调用而没有按引用调用。到这里估计不少人都蒙逼了,下面我们通过一个反例来说明(回忆一下开头我们所说明的按值调用与按引用调用的根本区别)。
/**
* java中的按值调用
*/
public class CallByValue {
private static User user=null;
private static User stu=null;
/**
* 交换两个对象
* @param x
* @param y
*/
public static void swap(User x,User y){
User temp =x;
x=y;
y=temp;
}
public static void main(String[] args) {
user = new User("user",26);
stu = new User("stu",18);
System.out.println("调用前user的值:"+user.toString());
System.out.println("调用前stu的值:"+stu.toString());
swap(user,stu);
System.out.println("调用后user的值:"+user.toString());
System.out.println("调用后stu的值:"+stu.toString());
}
}
我们通过一个swap函数来交换两个变量user和stu的值,在前面我们说过,如果是按引用调用那么一个方法可以修改传递引用所对应的变量值,也就是说如果java是按引用调用的话,那么swap方法将能够实现数据的交换,而实际运行结果是:
调用前user的值:User [name=user, age=26]
调用前stu的值:User [name=stu, age=18]
调用后user的值:User [name=user, age=26]
调用后stu的值:User [name=stu, age=18]
我们发现user和stu的值并没有发生变化,也就是方法并没有改变存储在变量user和stu中的对象引用。swap方法的参数x和y被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝的值而已,最终,所做的事都是白费力气罢了。在方法结束后x,y将被丢弃,而原来的变量user和stu仍然引用这个方法调用之前所引用的对象。
这个过程也充分说明了java程序设计语言对对象采用的不是引用调用,实际上是对象引用进行的是值传递,当然在这里我们可以简单理解为这就是按值调用和引用调用的区别,而且必须明白即使java函数在传递引用数据类型时,也只是拷贝了引用的值罢了,之所以能修改引用数据是因为它们同时指向了一个对象,但这仍然是按值调用而不是引用调用。
hashcode、equals、wait、wait(long)、notify、notifyAll、finalize、getClass、toString、clone
notify() 和 notifyAll() 都是 Object 对象用于通知处在等待该对象的线程的方法。
notify 可能会导致死锁,而 notifyAll 则不会
任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行 synchronized 中的代码使用 notifyall, 可以唤醒 所有处于 wait 状态的线程,使其重新进入锁的争夺队列中,而 notify 只能唤醒一个。
wait() 应配合 while 循环使用,不应使用 if ,务必在 wait() 调用前后都检查条件,如果不满足,必须调用notify() 唤醒另外的线程来处理,自己继续 wait() 直至条件满足再往下执行。
notify() 是对 notifyAll() 的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致死锁。正确的场景应该是 WaitSet 中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果唤醒的线程无法正确处理,务必确保继续 notify() 下一个线程,并且自身需要重新回到 WaitSet 中。
浅拷贝
深拷贝
clone()
Object类中的clone()方法是 protected 关键字修饰的本地方法(使用native关键字修饰),我们完成克隆需要重写该方法。
注意:按照惯例重写的时候一个要将protected修饰符修改为public,这是JDK所推荐的做法,但是我测试了一下,复写的时候不修改为public也是能够完成拷贝的。但是还是推荐写成public。
我们重写的clone方法一个要实现Cloneable接口。虽然这个接口并没有什么方法,但是必须实现该标志接口。如果不实现将会在运行期间抛出:CloneNotSupportedException异常
Object中本地clone()方法,默认是浅拷贝
clone()浅拷贝
package cn.cupcat.java8;
public class Person implements Cloneable{
private String name;
private int age;
private int[] ints;
public int[] getInts() {
return ints;
}
public Person(String name, int age, int[] ints) {
this.name = name;
this.age = age;
this.ints = ints;
}
public void setInts(int[] ints) {
this.ints = ints;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
/**
* 默认实现
* */
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class CloneTest {
@Test
public void test() throws CloneNotSupportedException {
int[] ints = {1,2,3};
String name = "zhangxiangyang";
int age = 23;
Person person = new Person("zhangxiangyang",age,ints);
System.out.print("一:克隆前: age = "+ age + "... name = "+ name + " 数组:");
for (int i : ints){
System.out.print(i + " ");
}
System.out.println();
//拷贝
Person clonePerson = (Person) person.clone();
int clonePersonAge = clonePerson.getAge();
String clonePersonName = clonePerson.getName();
int[] ints1 = clonePerson.getInts();
System.out.print("二:克隆后: age = "+ clonePersonAge + "... name = "+ clonePersonName + " 数组: ");
for (int i : ints1){
System.out.print(i + " ");
}
System.out.println();
//修改:
ints1[0] = 50;
//修饰
clonePerson.setName("666666666");
age = person.getAge();
name = person.getName();
System.out.println();
System.out.print("三:修改后原对象: age = "+ age + "... name = "+ name + "数组 ");
for (int i : ints){
System.out.print(i + " ");
}
System.out.println();
System.out.println("四:person == clonePerson ? "+ (person == clonePerson ));
}
}
一:克隆前: age = 23... name = zhangxiangyang 数组:1 2 3
二:克隆后: age = 23... name = zhangxiangyang 数组: 1 2 3
三:修改后原对象: age = 23... name = zhangxiangyang数组 50 2 3
四:person == clonePerson ? false
ints1[0] = 50;
//修饰
clonePerson.setName("666666666");
clone()深拷贝
/**
* 深拷贝
* */
@Override
public Object clone() throws CloneNotSupportedException {
Person person = new Person(name,age);
int[] ints = new int[this.ints.length];
System.arraycopy(this.ints,0,ints,0,ints.length);
person.setInts(ints);
return person;
}
args的作用:在程序启动时可以用来指定外部参数
举例:打开idea中的项目配置编辑框
在环境临时配置一栏,设置运行时的服务端口为8989
-D:表示向运行类传参
这样就能覆盖启动的程序中原本设置好的服务端口了。为什么能在程序启动过程中用临时传参覆盖掉原本代码中默认的配置,就是因为这个server.port=8989 能够通过args参数传给主函数
或者在jar包启动运行的时候,通过java -jar jar包名 [参数] 将这个参数传递给main函数的args
越无序越快
(加入随机化后基本不会退化),平均常数最小,不需要额外空间,不稳定排序。所以大于或等于47或少于286会进入快排,而在大于或等于286后,会有个小动作:“// Check if the array is nearly sorted”。这里第一个作用是先梳理一下数据方便后续的归并排序,第二个作用就是即便大于286,但在降序组太多的时候(被判断为没有结构的数据,The array is not highly structured,use Quicksort instead of merge sort.),要转回快速排序。
动态代理: 利用Java的反射技术(Java Reflection),在运行时创建一个实现某些给定接口的新类(也称“动态代理类”)及其实例(对象),代理的是接口(Interfaces),不是类(Class),也不是抽象类。在运行时才知道具体的实现,spring aop就是此原理
。
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
newProxyInstance,方法有三个参数:
解决Hash冲突方法有:开放定址法、再哈希法、链地址法(拉链法)、建立公共溢出区
。HashMap中采用的是 链地址法 。
开放定址法也称为再散列法
,基本思想就是,如果p=H(key) 出现冲突时,则以p 为基础,再次hash, p1=H§ ,如果p1再次出现冲突,则以p1为基础,以此类推,直到找到一个不冲突的哈希地址pi 。 因此开放定址法所需要的hash表的长度要大于等于所需要存放的元素,而且因为存在再次hash,所以只能在删除的节点上做标记,而不能真正删除节点。再哈希法(双重散列,多重散列)
,提供多个不同的hash函数,当R1=H1(key1) 发生冲突时,再计算R2=H2(key1) ,直到没有冲突为止。 这样做虽然不易产生堆集,但增加了计算的时间。链地址法(拉链法)
,将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。建立公共溢出区
,将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区。Class.forName(“com.mysql.jdbc.Driver”);
connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "root");
statement = connection.createStatement();
resultSet= statement.executeQuery(sql);
rowMapper.rowMapper(resultSet);
finally {
if (resultSet!=null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (statement!=null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection!=null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
多个线程顺序打印、哲学家就餐问题:并发编程之模式篇
JDBC连接流程::JDBC获取数据库连接
Servlet工作流程:JavaWeb三大组件之Servlet详解
SSO的token实现方式:最近看了看使用Token令牌的方式实现SSO(单点登录)
参考:https://blog.csdn.net/qq_36389060/article/details/123893425
参考:https://blog.csdn.net/lgxzzz/article/details/124970034
原理:比较两个相邻的元素,将值大的元素交换到右边
思路:依次比较相邻的两个数,将比较小的数放在前面,比较大的数放在后面。
最后一个数一定是数组中最大的一个数,所以在比较第二趟的时候,最后一个数是不参加比较的
。最后两个数是不参与比较的
。最好情况:有可能第二趟就是有序的了,如果一趟冒泡没发生交换就继续进行了 O(n)
。
// 优化 O(n)
// 有可能第二趟就是有序的了,如果一趟冒泡没发生交换就继续进行了
public void Bubble2(int[] nums) {
boolean flag = false;
for (int i = 0; i < nums.length - 1; i++) {
for (int j = 0; j < nums.length - 1 - i; j++) {
if (nums[j] > nums[j+1]) {
flag = true;
int temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
}
}
if (flag == false) {
break;
} else {
flag = false;
System.out.println(Arrays.toString(nums));
}
}
}
public void sort(int[] nums) {
for (int i = 1; i < nums.length; i++) {
for (int j = i; j > 0 ; j--) {
// 和前面数据比较,小于则插入
if (nums[j] < nums[j-1]) {
int temp = nums[j-1];
nums[j-1] = nums[j];
nums[j] = temp;
}
}
}
for (int num : nums) {
System.out.print(num+" ");
}
}
选择排序思路
// 选择排序
public void selectSort(int[] nums) {
for (int i = 0; i < nums.length - 1; i++) {
int minIndex = i;
int min = nums[i];
for (int j = i + 1; j < nums.length; j++) {
if (min > nums[j]) {
min = nums[j];
minIndex = j;
}
}
// 交换
if (minIndex != i) {
nums[minIndex] = nums[i];
nums[i] = min;
}
System.out.println(Arrays.toString(nums));
}
}
快速排序步骤
需要注意的是:若选择最左边的数据作为key,则需要end先走;若选择最右边的数据作为key,则需要bengin先走
);选取最左边的值作为key
);此时key的左边都是小于key的数,key的右边都是大于key的数
;//快速排序 hoare版本(左右指针法)
void QuickSort(int* arr, int begin, int end)
{
//只有一个数或区间不存在
if (begin >= end)
return;
int left = begin;
int right = end;
//选左边为key
int keyi = begin;
while (begin < end)
{
//右边选小 等号防止和key值相等 防止顺序begin和end越界
while (arr[end] >= arr[keyi] && begin < end)
{
--end;
}
//左边选大
while (arr[begin] <= arr[keyi] && begin < end)
{
++begin;
}
//小的换到右边,大的换到左边
swap(&arr[begin], &arr[end]);
}
swap(&arr[keyi], &arr[end]);
keyi = end;
//[left,keyi-1]keyi[keyi+1,right]
QuickSort(arr, left, keyi - 1);
QuickSort(arr,keyi + 1,right);
}
《算法》一书提到了非常好的解决办法:三相切割快速排序
。
多了个等于pivot了
。// QuickSort
public class Fun{
public static int[] arr = {7,12,13,5,8,8,23,13,8};
public static void main(String[] args) {
int high = arr.length - 1, low = 0;
quickSort(low, high);
for(int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + ",");
}
}
public static void quickSort(int start, int end) {
if(start >= end)
return;
else {
int temp = arr[start];
int index = start + 1, low = start, high = end;
while(index <= high) {
if(temp > arr[index]) swap(low++,index++);
else if(temp < arr[index]) swap(high--,index);
else index++;
}
quickSort(start, low - 1);
quickSort(high + 1, end);
}
}
public static void swap(int t1, int t2){
int temp;
temp = arr[t1];
arr[t1]= arr[t2];
arr[t2] = temp;
}
参考:https://www.cnblogs.com/chengxiao/p/6129630.html
堆是一棵顺序存储的完全二叉树。
堆排序的基本思想是:
图解说明
假设给定无序序列结构如为{4,6,8,6,9}
步骤一 :首先将无序序列构造成大顶堆
步骤二:构造初始堆
。将给定无序序列构造成一个大顶堆
(一般升序采用大顶堆,降序采用小顶堆
)。
从最后一个非叶子结点开始
(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整
。步骤三 :将 堆顶元素
与 末尾元素
进行 交换
,使末尾元素最大
。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
/**
* Created by chengxiao on 2016/12/17.
* 堆排序demo
*/
public class HeapSort {
public static void main(String []args){
int []arr = {9,8,7,6,5,4,3,2,1};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int []arr){
//1.构建大顶堆
for(int i=arr.length/2-1;i>=0;i--){
//从第一个非叶子结点从下至上,从右至左调整结构
adjustHeap(arr,i,arr.length);
}
//2.调整堆结构+交换堆顶元素与末尾元素
for(int j=arr.length-1;j>0;j--){
swap(arr,0,j);//将堆顶元素与末尾元素进行交换
adjustHeap(arr,0,j);//重新对堆进行调整
}
}
/**
* 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
* @param arr
* @param i
* @param length
*/
public static void adjustHeap(int []arr,int i,int length){
int temp = arr[i];//先取出当前元素i
for(int k=i*2+1;k<length;k=k*2+1){//从i结点的左子结点开始,也就是2i+1处开始
if(k+1<length && arr[k]<arr[k+1]){//如果左子结点小于右子结点,k指向右子结点
k++;
}
if(arr[k] >temp){//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
arr[i] = arr[k];
i = k;
}else{
break;
}
}
arr[i] = temp;//将temp值放到最终的位置
}
/**
* 交换元素
* @param arr
* @param a
* @param b
*/
public static void swap(int []arr,int a ,int b){
int temp=arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
如图所示:
这棵二叉树为满二叉树,也可以说深度为k,有2^k-1个节点的二叉树。
完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。
优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系。
前面介绍的树,都没有数值的,而二叉搜索树是有数值的了,二叉搜索树是一个有序树。
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:在符合二叉查找树的条件下,它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
BTree的特点
BTree又叫 多路平衡搜索树
,一颗m叉的BTree特性如下:
ceil为向上取整
BTree作用
B-Tree是为磁盘等外存储设备设计的一种平衡查找树
。因此在讲B-Tree之前先了解下磁盘的相关知识。
系统从磁盘读取数据到内存时是以 磁盘块(block)
为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来
,而不是需要什么取什么。
InnoDB存储引擎中有 页(Page)的概念
, 页是其磁盘管理的最小单位
。InnoDB存储引擎中 默认每个页的大小为16KB
,可通过参数innodb_page_size将页的大小设置为4K、8K、16K,在MySQL中可通过如下命令查看页的大小:
mysql> show variables like 'innodb_page_size'
而系统一个磁盘块的存储空间往往没有这么大,因此InnoDB每次申请磁盘空间时都会是若干地址连续磁盘块来达到页的大小16KB。InnoDB在把磁盘数据读入到磁盘时会以页为基本单位,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘I/O次数,提高查询效率。
BTree查询数据的过程
B-Tree结构的数据可以让系统高效的找到数据所在的磁盘块。为了描述B-Tree,首先定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data为一行记录中除主键外的数据。对于不同的记录,key值互不相同。B-Tree中的每个节点根据实际情况可以包含大量的关键字信息和分支,如下图所示为一个 3阶的B-Tree
:
每个节点占用一个盘块的磁盘空间
,一个节点上有两个升序排序的关键字
和三个指向子树根节点的指针
,指针存储的是子节点所在磁盘块的地址
。两个关键词划分成的三个范围域
对应三个指针指向的子树的数据的范围域。以根节点为例,关键字为17和35,P1指针指向的子树的数据范围为小于17,P2指针指向的子树的数据范围为17~35,P3指针指向的子树的数据范围为大于35。
模拟查找关键字29的过程:
读入内存
。【磁盘I/O操作第1次
】读入内存
。【磁盘I/O操作第2次
】读入内存
。【磁盘I/O操作第3次
】在磁盘块8中的关键字列表中找到关键字29
。分析上面过程,发现 需要3次磁盘I/O操作,和3次内存查找操作
。由于内存中的关键字是一个有序表结构,可以利用二分法查找提高效率
。而 3次磁盘I/O操作是影响整个B-Tree查找效率的决定因素
。B-Tree相对于AVLTree缩减了节点个数,使每次磁盘I/O取到内存的数据都发挥了作用,从而提高了查询效率。
B+Tree的特点
B+Tree的叶子节点保存所有的key信息,依key大小顺序排列
。所有的非叶子节点都可以看作是key的索引部分
。MySQL中B+Tree相对于BTree的特点
由于B+Tree只有叶子节点保存key信息,查询任何key都要从root走到叶子。所以B+Tree的查询效率更加稳定
。
B+Tree的查询流程
将上一节中的B-Tree优化,由于B+Tree的非叶子节点只存储键值信息,假设每个磁盘块能存储4个键值及指针信息,则变成B+Tree后其结构如下图所示:
通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点
(啥意思????????),而且所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对B+Tree进行两种查找运算:
可能上面例子中只有22条数据记录,看不出B+Tree的优点,下面做一个推算:InnoDB存储引擎中页的大小为16KB
因为是估值,为方便计算,这里的K取值为 10 ^ 3
)。也就是说一个深度为3的B+Tree索引可以维护 10^3 * 10^3 * 10^3 = 10亿
条记录(啥意思,为啥是相乘????????)。一个磁盘块代表一次IO
,很明显数据量多的情况下,IO次数也会多,会影响查询性能,于是在B树的基础上衍生出了B+树。实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree的高度一般都在2~4层。mysql的InnoDB存储引擎在设计时 是将根节点常驻内存的
,也就是说查找某一键值的行记录时 最多只需要1 - 3次磁盘I/O操作
。
B+Tree是在B-Tree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是用B+Tree实现其索引结构。
每个节点中
不仅包含数据的 key值
,还有data值
。而每一个页的存储空间是有限的:
深度较大
,增大查询时的磁盘I/O次数
,进而影响查询效率。所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息
,这样 可以大大加大每个节点存储的key值数量,降低B+Tree的高度
。参考 敖丙CSDN:https://blog.csdn.net/qq_35190492/article/details/109503539
红黑树也是对树结构的一种高度综合运用,涉及到多叉树,树平衡调整,节点旋转等等。
参考:https://blog.csdn.net/qq_36389060/article/details/123893425
时间复杂度:O(logn)
/**
* 704. 二分查找 TODO 存在多种写法
* 二分查找条件:数组有序 + 数组无重复元素
*/
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
// 防止数值过大相加超过边界
int mid = left + (right - left) / 2;
if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] == target) {
return mid;
}
}
return -1;
}
参考:https://blog.csdn.net/qq_36389060/article/details/124097552
解决问题
我们把Session数据放在Redis中(使用Redis模拟Session)实现Session共享,并通过设置domain解决cookie跨域问题 。并且将 将登录功能单独抽取出来做成一个子系统,这里我们称为SSO系统:
SSO系统(登录系统)中的用户登录逻辑如下:
// 登录功能(SSO单独的服务)
@Override
public TaotaoResult login(String username, String password) throws Exception {
// 1.根据用户名查询用户信息
TbUserExample example = new TbUserExample();
Criteria criteria = example.createCriteria();
criteria.andUsernameEqualTo(username);
List<TbUser> list = userMapper.selectByExample(example);
if (null == list || list.isEmpty()) {
return TaotaoResult.build(400, "用户不存在");
}
// 2.根据查询到的用户进行核对密码
TbUser user = list.get(0);
if (!DigestUtils.md5DigestAsHex(password.getBytes()).equals(user.getPassword())) {
return TaotaoResult.build(400, "密码错误");
}
// 3.密码核对成功则登录成功
// 4.生成一个用户token,并存入redis
String token = UUID.randomUUID().toString();
jedisCluster.set(USER_TOKEN_KEY + ":" + token, JsonUtils.objectToJson(user));
// 5.设置用户token的过期时间
jedisCluster.expire(USER_TOKEN_KEY + ":" + token, SESSION_EXPIRE_TIME);
return TaotaoResult.ok(token);
}
用户登录其他子系统时,其它子系统会请求 SSO(登录系统)中的登录方法进行登录流程(就是上面的逻辑),将返回的token写到Cookie中,下次访问时HTTP请求会自动把Cookie带上。
public TaotaoResult login(String username, String password,
HttpServletRequest request, HttpServletResponse response) {
//请求参数
Map<String, String> param = new HashMap<>();
param.put("username", username);
param.put("password", password);
//登录处理
String stringResult = HttpClientUtil.doPost(REGISTER_USER_URL + USER_LOGIN_URL, param);
TaotaoResult result = TaotaoResult.format(stringResult);
//登录出错
if (result.getStatus() != 200) {
return result;
}
//登录成功后把取token信息,并写入cookie
String token = (String) result.getData();
//写入cookie
CookieUtils.setCookie(request, response, "TT_TOKEN", token);
//返回成功
return result;
}
之后用户每次发起HTTP请求时,都会自动将Cookie都会带上,后端可以通过拦截器得到token,进而判断该用户是否已经登录。
单点登录(SSO)流程总结
到这里,其实我们会发现其实就两个变化:
本来将用户信息存到Session,现在将用户信息存到Redis
。到这里,我们已经可以实现单点登录了。
数据库中的表
至少需要五张表才能完成权限管理:权限表、角色表、用户表、权限角色表、角色用户表。
实现流程
参考:https://blog.csdn.net/qq_49721447/article/details/122620425
参考一:https://blog.csdn.net/zzti_erlie/article/details/107001096
参考二:https://blog.csdn.net/qq_38128179/article/details/84956552
现在绝大多数公司的项目都是前后端分离的,前后端分离后势必会遇到跨域问题。
我们debug会发现,reponse为undefined,提示消息为Network Error。
所以当你和前端联调的时候一直请求失败,报网络错误,一般情况下是后端没有做跨域配置。
注意此时并不是后端没有收到请求,而是收到请求了,也返回结果了,但是浏览器将结果拦截了,并且报错。
同源策略
出于浏览器的同源策略限制。同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。
同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)。
为什么要有同源策略呢?
举个例子,以银行转账为例,看看你的钱是怎么没的:
这就是著名的CSRF攻击(跨站请求伪造,当然还有很多其他方式),还有如果第5步不对请求的来源进行校验,那么你的钱已经被转走了。
html页面中的如下三个标签是允许跨域加载资源的
<img src=XXX>
<link href=XXX>
<script src=XXX>
什么是跨域
当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域。
虽然同源策略保证了安全,但一些合理的用途也会受到影响。例如:前端项目在a域名下发送一个Ajax请求到b域名,由于同源策略我们的Ajax请求会报错,导致不能正常请求接口。
解决跨域的方式有很多种,简单介绍2个。
JSONP主要是利用
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(预检请求)(not-so-simple request)。
非简单请求
在正式的跨域请求前,发送一个OPTIONS请求去询问服务器是否接受接下来的跨域请求,携带如下header:
Origin: 发起请求原来的域
Access-Control-Request-Method: 将要发起的跨域请求方式(GET/POST/…)
Access-Control-Request-Headers: 将要发起的跨域请求中包含的请求头字段
服务器在返回中增加如下header来表明是否允许这个跨域请求。浏览器收到后进行检查如果不符合要求则不会发起后续请求
Access-Control-Allow-Origin: 允许哪些域来访问(*表示允许所有域的请求)
Access-Control-Allow-Methods: 允许哪些请求方式
Access-Control-Allow-Headers: 允许哪些请求头字段
Access-Control-Allow-Credentials: 是否允许携带Cookie
简单请求
只要同时满足以下两大条件,就属于简单请求:
(1) 请求方法是以下三种方法之一:
HEAD
GET
POST
(2)HTTP请求头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
简单请求会在请求中加入Origin头,直接发起请求,不会先询问了,后端返回相应的header即可。在响应中检查Access-Control-Allow-Origin,不符合要求就报错。
理解了跨域的本质,再看各种配置其实就是根据请求往reponse中增加header。
配置如下Filter,CrossDomainFilter是对javax.servlet.Filter的封装,本质上是一个Filter。
可以看到我多返回了一个header,Access-Control-Max-Age,他表明了询问结果的有效期限,即在3600s之内浏览器可以不必再次询问。
@Component
@WebFilter(filterName = "crossDomain", urlPatterns = "/*")
public class CrossDomainFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 此处可以进行白名单检测
if(CorsUtils.isCorsRequest(request)) {
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
response.setHeader("Access-Control-Allow-Methods", request.getHeader("Access-Control-Request-Method"));
response.setHeader("Access-Control-Max-Age", "3600");
}
// 是个OPTIONS请求,header已设好,不用执行后续逻辑,直接return
if(CorsUtils.isPreFlightRequest(request)) {
return;
}
filterChain.doFilter(request, response);
}
}
看一下用到的工具类
public abstract class CorsUtils {
// 请求中有 origin 这个header则返会true
public static boolean isCorsRequest(HttpServletRequest request) {
return (request.getHeader(HttpHeaders.ORIGIN) != null);
}
public static boolean isPreFlightRequest(HttpServletRequest request) {
return (isCorsRequest(request) && HttpMethod.OPTIONS.matches(request.getMethod()) &&
request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) != null);
}
}
@Configuration
public class GlobalCorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 添加映射路径
registry.addMapping("/**")
// 允许的域
.allowedOrigins("*")
// 允许携带cookie
.allowCredentials(true)
// 允许的请求方式
.allowedMethods("GET","POST", "PUT", "DELETE")
// 允许的请求头
.allowedHeaders("*");
}
};
}
}
支持更细粒度的配置,可以用法方法上或者类上。
@RestController
@RequestMapping("resource")
@CrossOrigin({"http://127.0.0.1:8080"})
public class ResourceController {
}
参考:https://www.processon.com/view/link/62c7dab21e08534607e6810e
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。