5年码农:老家一套房(100W)+一辆车(23W)+50w存款,全靠这页知识点!毫不吝啬分享给所有兄弟姐妹!

老家一套房+一辆车+50w存款,全靠这页知识点!毫不吝啬分享给所有你兄弟姐妹!

一、java

1、集合

分类:

List:List接口继承Collection接口,实现了List接口的类称为List集合。

1)ArrayList集合

数据结构:数组

特点:查询快,因为数组是有角标的,可以通过角标访问元素,而且元素是可以重复。

缺点:插入和删除比较慢,添加元素时是没有锁的所以非线程安全的,多线程操作的时可能会有问题。

扩容机制:

①当我们创建一个arrayList集合时,初始数组的容量为0

②当我们第一次Add()添加元素时,集合会扩容到10

③当我们继续往容器内添加元素,这个时候会扩容到原来容器的长度的1.5倍,比如之前是10,那么会创建一个新的容器扩容到1.5倍后,容器容量为25返回,然后把之前的数组添加到新的数组里。

2) LinkedList集合

数据结构:头结点和尾结点的双向链

特点:添加和删除比较快,添加可以通过头插或者尾插的方式添加元素add(int index, E element)

缺点:查询比较慢,因为查询时,要遍历链表通过一个一个对比获取结果。同样在操作集合时是没有加锁的,所以在多线程情况下是不安全的

1)HashSet集合

Set:Set集合与Collection基本上完全一样,它没有提供任何额外的方法。实际上Set就是Collection,只是行为略有不同(Set不允许包含重复元素)

set集合其实底层就是hashMap扩容机制一样

2)LinkedHashSet集合

Map:Map集合和Collection集合没有任何关系。Collection集合是以单个方式存储元素的,而Map集合是以键值对的方式存储元素,所有Map集合的Key是无序不可重复的,key和value都是引用数据类型,存的都是内存的地址。

1)HashMap集合

数据结构:

JDK1.8之前:数组+单链表

在这里插入图片描述

JDK1.8之后:数组+单链表+红黑树

在这里插入图片描述

特点:储存方式为K:V的形式,可以允许K为Null且只有一个,允许多个V为Null。Key是不允许重复的

缺点:非线程安全,因为Put方法没有添加锁操作,所以在多线程情况下可能会出现数据不最准确问题

扩容机制:要从put方法说起

首先要知道hashMap的属性有哪些

//维护的hash桶(也就是数组)
transient Node<K,V>[] table;
				 //node元素属性
        static class Node<K, V> implements Map.Entry<K, V> {
            //用来定位数组索引位置
            final int hash;
            //key值
            final K key;
            //value值
            V value;
            //链表的下一个node
            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;
            }
        }

①创建HashMap,如果没有指定初始化容量默认为16(建议给默认初始容器,元素数/0.75+1,防止多次扩容)。

②例如put(“姓名”:“张三”),系统会根据hascode()方法获取姓名的hashCode值,然后在根据hash算法(高位运算和取模运算)获取到该key的存储位置。

③如果添加元素时桶的元素超过了0.75的阈值那么会进行一次resize()扩容,扩容为原来容器的2倍,并返回新的容器,最后把之前的元素重新进行hash算法插入到新的容器中。

④如果如果添加的元素位置已经有了一个元素,这时就会发生了hash碰撞。这个时候就会通过equals()方法进行再次对比是否key一致,如果一致那么就直接覆盖Value,如果不一致那么就通过拉链法在当前桶的位置向下转换链表结构(PS:JDK1.8之前是头插法,多线程情况下可能会造成环形链表造成死循环,而JDK1.8之后改为了尾插法,虽然解决了死循环但是在多线程情况下还会造成覆盖值非线程安全)。

⑤如果当前桶的链表长度超过8,此时会把链表转换成红黑树结构。当前红黑数的数据小于6时,再次转换成原来的单链表结构。

在这里插入图片描述

2)HashTable集合

数据结构:同hashMap一致

在这里插入图片描述

默认属性:初始化为11,扩容为原来的两倍+1

优点:HashTable是线程安全的,所有的方法都加synchronized关键字

缺点:性能比较差,多线程会造成堵塞

3)ConcurrentHashMap集合

数据结构:

JKD1.8之前:分段数组+单链表

在这里插入图片描述

JKD1.8之后:同hashMap数据结构一直(数组+链表+红黑树)

优点:线程安全(JDK1.8之前采用了分段锁的概念,把数组切分,并把每个却分出来的加上一把分段锁Segment,从而提高访问元素效率,在JDK1.8之后摒弃了分段锁并且才用了和hashMap一样的数据机构,同时使用了synchronized关键字来保证线程安全按,因为在JDK1.6以后,对synchronized关键字优化了很多例如CAS和锁升级)

备注:1.7之前put添加元素需要经历两次hash运算才能确定添加元素的位置。

2、线程

概念:线程与进程相似,但线程是⼀个⽐进程更⼩的执⾏单位。⼀个进程在其执⾏的过程中可以产⽣多个线程。与进程不同的是同类的多个线程共享进程的堆和⽅法区资源,但每个线程有⾃⼰的程序计数器、虚拟机栈和本地⽅法栈,所以系统在产⽣⼀个线程,或是在各个线程之间作切换⼯作时,负担要⽐进程⼩得多,也正因为如此,线程也被称为轻量级进程。

线程状态:

①New:尚未启动的线程状态

②Runnble:可运行的线程状态(准备好,随时可以被CPU调用)

③blocked:线程阻塞等待监视器锁定的线程状态

④waiting:等待线程状态,例如不带超时时间的方法:Object.wait()、Thread.join()

⑤time waiting:具有指定等待时间的线程状态比如:Thread.sleep(numbeer)

⑥Terminate:终止线程状态。

在这里插入图片描述

终止线程正确方法:

①首先是不能使用stop()方法来执行终止线程,会引发数据安全性问题。(数据非一致性)

②正确方法为interrupt()方法执行终止,可以保证安全性问题。(数据一致性)

线程之前的通信:

wait/notify、pack/unPak

volatile有一个关键的特性:保证内存可见性,即多个线程访问内存中的同一个被volatile关键字修饰的变量时,当某一个线程修改完该变量后,需要先将这个最新修改的值写回到主内存,从而保证下一个读取该变量的线程取得的就是主内存中该数据的最新值,这样就保证线程之间的透明性,便于线程通信。

3、JVM

jvm运行时数据区:

线程独占:每个线程都会有它独立的空间,随线程生命周期而创建和销毁

线程共享:所有线程能访问这块内存数据,随虚拟机或者GC而创建和销毁

线程共享部分:方法区和堆存内存

线程独占区:虚拟机栈,本地方法栈,程序计数器

方法区:JVM用来存储加载的类信息、常量、静态变量、编译后的代码等数据,虚拟机规范这是一个逻辑区。具体实现根据不同的虚拟机来实现。如:HotSpot在JDK1.7中方法区放在永久代,JDK1.8放在元数据空间,并且通过GC机制对这个区域进行管理。

堆内存:还可以继续细分为老年代、新生代(Eden、From Survivor、To Survivor)JVM启动时创建,存放对象的实例。垃圾回收期主要就是管理堆内存。如果满了,就会出现OutOfMemroyError,后续在内存模中详解。

虚拟机栈:每个线程都在这个空间中有一个私有的空间,线程栈由多个栈帧组成,一个线程会执行一个或多个方法,一个方法对应一个栈帧。

栈帧内容包括:局部变量表、操作数栈、动态链接、方法会返回地址、附加信息等,栈内存默认最大为1M超出则抛出StackOverflowError。

本地方法栈:和虚拟机栈功能类似,虚拟机栈是为虚拟机执行Java方法而准备的,本地方法栈是为虚拟机使用Native本地方法而准备的。虚拟机规范没有具体的实现,由不同的虚拟机厂商去实现。HotSpot虚拟机中虚拟机栈和本地方法栈实现方式一样的,同样超出大小后会抛出StackOverflowError。

程序计数器:记录当前线程执行字节码的位置,存储的是字节码指令地址,如果执行Native方法,则计数器值为空。每个线程都在这个空间有一个私有的空间,占用内存空间很少。CPU同一时间,只会执行一条线程中的指令,JVM多线程会轮流切换并分配CPU执行时间的方式。为了线程切换后需要通过程序计数器,来恢复正确的执行位置。

在这里插入图片描述

内存屏障和CPU缓存:

多级缓存分为L1、L2、L3,CPU在读取数据时,先在L1中寻找,在从L2寻找,再从L3寻找,然后是内存,再然后就是外存储器。

在这里插入图片描述

两个问题:

1、CPU高速缓存下有一个问题:

缓存中的数据与主内存的数据并不是实时同步的,各CPU间缓存的数据也不是实时同步,在同一个时间点,各CPU所看到同一内存地址的数据的值可能是不一致的。

2、CPU执行指令重排序优化下有一个问题:

虽然遵守了as-if-serial语义,单仅在单CPU自己执行的情况下能保证结果正确,多核多线程中,指令逻辑无法分辨因果关联,可能出现乱序执行,导致程序运行结果错误。

解决:

内存屏障:处理器提供了两个内存屏障指令用于解决上述两个问题。

写内存屏障:在指令后插入,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。强制写入主内存这种显示调用,CPU就不会因为性能考虑而区队指令重排序

读内存屏障:在指令前插入,可以让高速缓存中的数据失效,强制向主内存中读取数据。强制读取主内存内容,让CPU缓存与主内存保持一致,避免了缓存导致的一致性问题。

jvm内存模型:

有两个线程1和线程2,线程2不断循环执行业务代码,当线程1对主内存标志位修改时,线程2并未读取到,导致线程2死循环,这就是多线程下内存不可见问题
在这里插入图片描述

Volatile关键字:

1.多线程情况下保证,一个线程对主内存共享变量的修改,可以及时的被其他线程看到

2.禁止指令重排序

类加载机制:

步骤:

在这里插入图片描述

系统类加载器:

1.Bootstrap ClassLoader:启动类加载器

负责将JAVA_HOME/jre/lib目录中的jar包加载到内存中,包括rt.jar、resources.jar和charsets.jar等等。也可以在JVM启动时,指定-Xbootclasspath参数,来改变Bootstrap ClassLoader的加载目录。

2.Extensions ClassLoader:扩展类加载器

负责将JAVA_HOME/jre/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。

3.App ClassLoader:系统类加载器

负责加载Classpath目录下的的所有jar和Class文件,是程序中的默认类加载器。这里的Classpath是指我们Java工程的bin目录。也可以加载通过-Djava.class.path选项所指定的目录下的jar和Class文件。

双亲委派模型:

在这里插入图片描述

为了避免重复加载类,由下向上逐级委托,由上向下逐级查找

反射:

反射就是在程序运行中,获取Class对象并操作对象的过程。

背景知识:一个类在内存中只有一个Class对象

通过反射获取类的Class的方式:

//第一种(已知类的路径)
Class c = Class.forName("相对路径")
//第二种(已知对象实例)
A a = new A();
Class c = a.getClass()
//第三种(已知具体的类,最安全)
Class c =  A.class
//第四种(已知Class对象,获取父类的Class对象)如果C继承了B,那么可以通过C的Class对象获取B的Class对象
Class B = c.getSuperClass()

通过反射拿到的对象可以获取到当前类有哪些注解,实现了哪些接口,构造器,属性,方法,

垃圾回收:

JVM在回收时首先要标记哪些可以被回收哪些是不能被回收。

可达性分析算法:简单说。将对象及其引用关系看做一个图,选定活动的对象作为GC Roots,然后跟踪引用联调,如果一个对象和GC Roots之间不可达,也就是不存在引用,那么即可认定为是可以回收的对象。

在这里插入图片描述

绿色代表有引用,棕色代表没有引用,即可以被回收。

在这里插入图片描述

垃圾回收算法:

在这里插入图片描述

分代收集:

在这里插入图片描述

在这里插入图片描述

4、线程池

在这里插入图片描述

执行源码:

在这里插入图片描述

创建线程池参数:

1.corePoolSize 线程池核心线程大小

2.maximumPoolSize 线程池最大线程数量

3.keepAliveTime 空闲线程存活时间

4.unit 空闲线程存活时间单位

5.workQueue 工作队列:

①ArrayBlockingQueue:基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。

②LinkedBlockingQuene:基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而基本不会去创建新线程直到maxPoolSize(很难达到Interger.MAX这个数),因此使用该工作队列时,参数maxPoolSize其实是不起作用的。

③SynchronousQuene:一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。

④PriorityBlockingQueue:具有优先级的无界阻塞队列,优先级通过参数Comparator实现。

6.threadFactory 线程工厂

7.handler 拒绝策略

①CallerRunsPolicy:该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。

②AbortPolicy:该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。

③DiscardPolicy:该策略下,直接丢弃任务,什么都不做。

④DiscardOldestPolicy:该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列

线程池定义如果确定线程数量?

N:cpu数量

如果是CPU密集型应用,则线程池大小设置为N+1

如果是IO密集型应用,则线程池大小设置为2N+1

5、锁

CAS自旋锁:

compare and swap:比较并交换

package com.test;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * @author 郜玉皓
 * @Description CAS自旋锁机制实现
 * @date 2023年10月19日 10:46 AM
 */
public class UnsafeTest {
    volatile int value = 0;
    static long valueSet;
    /**
     * 不安全的
     * 直接操作内存 强大的api
     */
    static Unsafe unsafe;

    //反射获取unsafe得值
    static {
        try {
            //获取Unsafe
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            //设置可以访问
            theUnsafe.setAccessible(true);
            //拿到对象
            unsafe = (Unsafe)theUnsafe.get(null);
            //获取当前对象value的偏移量(value在内存的储存地址)
            valueSet = unsafe.objectFieldOffset(UnsafeTest.class.getDeclaredField("value"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 比较并替换自旋获取当前对象的value值并做+1操作
     */
    public void add() {
        int current;
        //如果线程很多时候会导致内存爆炸,线程会堆积在这个地方不断的循环获取
        do {
            //获取内存valueSet的值
            current = unsafe.getIntVolatile(this, valueSet);
            //和主内存对比值是否相同,如果不同那么无法添加,需要重新获取valueSet的值
        } while (!unsafe.compareAndSwapInt(this, valueSet, current, current + 1));
    }

    public static void main(String[] args) throws InterruptedException {
        UnsafeTest unsafeTest = new UnsafeTest();
        //创建两个线程并同时调用加加操作
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    unsafeTest.add();
                }
            }).start();
        }
        Thread.sleep(2000L);
        System.out.println(unsafeTest.value);
    }
}

ABA问题:

已知三个线程,同时对一个内存的value变量a值进行修改,当线程1把内存的value值改成B时,计入了修改次数1。应该线程2来修改时,线程3要比线程2提前修改把B值提前修改成原来的a,此时线程3计入修改次数2,线程2此时又把内存的a值改成了c,也同样计入了修改值。此时的线程2计入的修改值就有问题了,应该线程2不能修改成功才对。因为线程2来修改时并不知道线程3已经修改过。这种问题的场景很少理解即可。

在这里插入图片描述

synchronized 关键字:

属于最基本的线程通信机制,基于对象监视器(Monitor)实现的。

java中的每个对象都和一个监视器相关联,一个线程可以锁定或解锁。

一次只有一个线程可以锁定监视器。

视图锁定改建时期的任何其他线程都会被阻塞,直到它们可以获得改监视器上的锁定位置

特性:可重入、独享、悲观锁

锁的范围:类锁、对象锁、
在这里插入图片描述

二、Spring框架

什么是spring框架:Spring 是⼀种轻量级开发框架,旨在提⾼开发⼈员的开发效率以及系统的可维护性。Spring 官⽹:https://spring.io/。我们⼀般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使⽤这些模块可以很⽅便地协助我们进⾏开发。这些模块是:核⼼容器、数据访问/集成,、Web、AOP(⾯向切⾯编程)、⼯具、消息和测试模块。⽐如:Core Container 中的 Core 组件是Spring 所有组件的核⼼,Beans 组件和 Context 组件是实现IOC和依赖注⼊的基础,AOP组件⽤来实现⾯向切⾯编程。

spring框架在使用动态代理时,如果实现了了接口那么就使用JDK动态代理,如果没有实现接口,那么就使用Cglib动态代理。

JDK动态代理:

原理:在 JDK 动态代理中,每一个动态代理类都必须实现 InvocationHandler 接口,并且每个动态代理类都关联了一个 handler。当我们通过代理对象调用一个方法的时候,这个方法的调用,就会被转发为由 InvocationHandler 接口的 invoke 方法来进行调用。invoke 方法会根据传入的代理对象、方法名称以及参数决定调用代理的哪个方法。详情见下 InvocationHandler 类的源码注释。

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

使用步骤:

0.被代理类实现一个自定义接口(接口为公共抽取的行为)

1.动态代理类实现 InvocationHandler 接口并重写 invoke 方法(重写invoke方法时,其内部调用目标类的方法,也就是参数method)

2.使用 Proxy.newProxyInstance() 方法获得代理对象

3.通过代理对象调用目标方法。

注意:这里调用的代理类的方法其实是生成代理对象的方法也就是Proxy的子类,并不是被代理类的方法

//0.自定义的接口
public interface IHello {
    void sayHello();
}

//被代理类实现自定义接口
public class HelloImpl implements IHello {
    @Override
    public void sayHello() {
        System.out.println("Hello world!");
    }
}
//1.代理类实现InvocationHandler接口
public class MyInvocationHandler implements InvocationHandler {
    /** 目标对象 */
    private Object target;

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

    //重写invoke方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("------插入前置通知代码-------------");
        // 执行相应的目标方法
        Object rs = method.invoke(target,args);
        System.out.println("------插入后置处理代码-------------");
        return rs;
    }
}
//2.通过获取代理类并调用代理类的方法
public class MyProxyTest {
    public static void main(String[] args){      
        /**
         * Proxy类中还有个将2~4步骤封装好的简便方法来创建动态代理对象,
         *其方法签名为:newProxyInstance(ClassLoader loader,Class<?>[] instance, InvocationHandler h)
         */
        IHello  iHello2 = (IHello) Proxy.newProxyInstance(IHello.class.getClassLoader(), // 加载接口的类加载器
                new Class[]{IHello.class}, // 一组接口
                new MyInvocationHandler(new HelloImpl())); // 自定义的InvocationHandler
        iHello2.sayHello();
    }
}

Cglib动态代理:

原理:CGLIB 动态代理中,通过字节码技术(使用了字节码处理框架 ASM)为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。

使用步骤:

1.CGLIB动态代理中,要求代理类必须要实现 MethodInterceptor 接口并重写 intercept()。

2.通过 CGLIB 动态代理,获取代理对象。

3.通过代理对象调用目标方法时,对目标方法的调用,将被转发到 MethodInterceptor 接口的 intercept() 方法来进行调用。在 intercept() 方法中,我们除了会调用委托方法外,还会进行一些增强操作,如日志的记录,调用量的监控等。

使用需要引入依赖:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>版本号</version>
</dependency>
/**
 * 自定义MethodInterceptor
 */
public class MyMethodInterceptor implements MethodInterceptor{
 
    /**
     * sub:cglib生成的代理对象
     * method:被代理对象方法
     * objects:方法入参
     * methodProxy: 代理方法
     */
    @Override
    public Object intercept(Object sub, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("======插入前置通知======");
        Object object = methodProxy.invokeSuper(sub, objects);
        System.out.println("======插入后者通知======");
        return object;
    }
}
public class Client {
    public static void main(String[] args) {
        // 代理类class文件存入本地磁盘方便我们反编译查看源码
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/lbs/CodeSpace");
        // 通过CGLIB动态代理获取代理对象的过程
        Enhancer enhancer = new Enhancer();
        // 设置enhancer对象的父类 
        // 代理类是继承了服务类 !!!
        enhancer.setSuperclass(HelloService.class);
        // 设置enhancer的回调对象
        enhancer.setCallback(new MyMethodInterceptor());
        // 创建代理对象
        HelloService proxy= (HelloService)enhancer.create();
        // 通过代理对象调用目标方法
        proxy.sayHello();
    }
}

IOC:

IoC(Inverse of Control:控制反转)是⼀种设计思想,就是 将原本在程序中⼿动创建对象的控制权,交由Spring框架来管理。 IoC 在其他语⾔中也有应⽤,并⾮ Spring 特有。 IoC 容器是 Spring⽤来实现 IoC 的载体, IoC 容器实际上就是个Mapkeyvalue,Map 中存放的是各种对象。将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注⼊。这样可以很⼤程度上简化应⽤的开发,把应⽤从复杂的依赖关系中解放出来。 IoC 容器就像是⼀个⼯⼚⼀样,当我们需要创建⼀个对象的时候,只需要配置好配置⽂件**/**注解即可,完全不⽤考虑对象是如何被创建出来的。在实际项⽬中⼀个 Service 类可能有⼏百甚⾄上千个类作为它的底层,假如我们需要实例化这个Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把⼈逼疯。如果利⽤IoC 的话,你只需要配置好,然后在需要的地⽅引⽤就⾏了,这⼤⼤增加了项⽬的可维护性且降低了开发难度。

Ioc容器:BeanFactory是Spring框架中IoC容器的顶层接⼝,它只是⽤来定义⼀些基础功能,定义⼀些基础规范,⽽ApplicationContext是它的⼀个⼦接⼝,所以ApplicationContext是具备BeanFactory提供的全部功能的。通常,我们称BeanFactory为SpringIOC的基础容器,ApplicationContext是容器的⾼级接⼝,⽐BeanFactory要拥有更多的功能,⽐如说国际化⽀持和资源访问(xml,java配置类)等等。

启动 IoC 容器的⽅式:Java环境下启动IoC容器

BeanFactory:顶级容器,定义了容器的基本行为

ResourceLoader:加载资源接口,实现有:ClassPathXmlApplicationContext,FileSystemXmlApplicationContext,AnnotationConfifigApplicationContext

ClassPathXmlApplicationContext:从类的根路径下加载配置⽂件

FileSystemXmlApplicationContext:从磁盘路径上加载配置⽂件

AnnotationConfifigApplicationContext:纯注解模式下启动Spring容器

ApplicationContext:最核心容器,扩展了容器的高级行为,间接的继承了BeanFactory顶级容器接口,还有ResourceLoader接口

springIoc容器三种高级特性:

lazy-Init 延迟加载:ApplicationContext 容器的默认⾏为是在启动服务器时将所有 singleton bean 提前进⾏实例化。提前实例化意味着作为初始化过程的⼀部分,ApplicationContext 实例会创建并配置所有的singleton,如果当bean加载属性设置为懒加载的时候,那么实例对象会在第一次调用getBean()方法时实例化。

FactoryBeanBeanFactory:BeanFactory接⼝是容器的顶级接⼝,定义了容器的⼀些基础⾏为,负责⽣产和管理Bean的⼀个⼯⼚,具体使⽤它下⾯的⼦接⼝类型,⽐如ApplicationContext;此处我们重点分析FactoryBean,Spring中Bean有两种,⼀种是普通Bean,⼀种是⼯⼚Bean(FactoryBean),FactoryBean可以⽣成某⼀个类型的Bean实例(返回给我们),也就是说我们可以借助于它⾃定义Bean的创建过程。Bean创建的三种⽅式中的静态⽅法和实例化⽅法和FactoryBean作⽤类似,FactoryBean使⽤较多,尤其在Spring框架⼀些组件中会使⽤,还有其他框架和Spring框架整合时使⽤。

// 可以让我们⾃定义Bean的创建过程(完成复杂Bean的定义)
public interface FactoryBean<T> {
    @Nullable
    // 返回FactoryBean创建的Bean实例,如果isSingleton返回true,则该实例会放到Spring容器的单例对象缓存池中Map
    T getObject() throws Exception;

    @Nullable
    // 返回FactoryBean创建的Bean类型
    Class<?> getObjectType();

    // 返回作⽤域是否单例
    default boolean isSingleton() {
        return true;
    }
}

//测试类获取bean类型 加上"&"是返回工厂bean类型,不加返回bean实例类型
applicationContext.getBean("&companyBean");

spring的两个后置处理器

Spring提供了两种后处理bean的扩展接⼝,分别为 BeanPostProcessor 和BeanFactoryPostProcessor,两者在使⽤上是有所区别的。

**区别: **

①容器(BeanFactory)初始化之后可以使⽤BeanFactoryPostProcessor对BeanDefinition做一些后续操作。(BeanDefinition是生成spingBean的前身,这里还没有转换成我们想要的springBean所以操作的是BeanDefinition对象)

//在工厂初始化之后,对BeanDefinition进行后续操作
@FunctionalInterface
public interface BeanFactoryPostProcessor {

	void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;

}

②BeanPostProcessor是在Bean生命周期中的某个环节(bean对象初始化之前和初始化之后)对bean进行处理操作的。(这里操作的是由BeanDefinition转换过来的Bean对象,操作的也是我们想要的bean对象)

//springBean生命周期在bean对象初始化之前和初始化之后对bean的操作
//用法,在你的配置类中实现BeanPostProcessor接口并重写下面的两个方法即可
public interface BeanPostProcessor {

  //bean初始化之前对bean进行操作
	@Nullable
	default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}

 //bean初始化之后对bean进行操作
	@Nullable
	default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}

}

BeanFactoryPostProcessor:BeanFactory级别的处理,是针对整个Bean的⼯⼚进⾏处理,典型应⽤:PropertyPlaceholderConfigurer此接⼝只提供了⼀个⽅法,⽅法参数为ConfigurableListableBeanFactory,该参数类型定义了⼀些⽅法其中有个⽅法名为getBeanDefinition()的⽅法,我们可以根据此⽅法,找到我们定义bean 的BeanDefinition对象。然后我们可以对定义的属性进⾏修改,以下是BeanDefinition中的⽅法

BeanDefinition:其实就是我们bean的描述信息,比如是否是单例,属性有哪些。

在这里插入图片描述

BeanDefinition注册到容器的过程流程图:

当容器加载我们定义好的Bean时,首先会把定义好的Bean转换为BeanDefinition,这里注意的是,在加载Bean时,此时BeanDefinition还不是完整定义好的,其属性还是加载进来时属性原值,经过beanFactoryPostprocessor方法会把原值替换成我们设置的好的属性值。

注意:
当前 Bean(dataSource)对象加载到BeanDefinition时,username 属性值 还是 ${jdbc.username}等等。。。。
执行完beanFactoryPostprocessor()方法后会把对象属性 username 的值 ${jdbc.username} 替换成 “root”

在这里插入图片描述

扩展:BeanFactoryPostProcessor下有一个子类BeanDefinitionRegistryPostProcessor,BeanDefinitionRegistryPostProcessor下有个一实现类ConfigurationClassPostProcessor(这个类就是SpringBoot自动装配原理核心)中方法跟踪: processConfigBeanDefinitions () —> parse() —> parse() —>processConfigurationClass() —> processConfigurationClass() —> doProcessConfigurationClass()方法处理注解

IOC容器创建过程:

IOC容器( BeanFactory)创建流程:

在这里插入图片描述

refresh()方法内部调用

@Override
	public void refresh() throws BeansException, IllegalStateException {
		// 对象锁加锁
		synchronized (this.startupShutdownMonitor) {
			/*
				Prepare this context for refreshing.
			 	刷新前的预处理
			 	表示在真正做refresh操作之前需要准备做的事情:
					设置Spring容器的启动时间,
					开启活跃状态,撤销关闭状态
					验证环境信息里一些必须存在的属性和属性是否合法
			 */
			prepareRefresh();

			/*
				该方法中的 
				①refreshBeanFactory()方法创建了DefaultListableBeanFactory
						创建集合
						createBeanFactory()
						   容器两个属性
						   BeanDefinitionMap 元素为0
						   BeanDefinitionNames 元素为0
						加载BeanDefinitions   
						loadBeanDefinitions()
						  容器两个属性
						   BeanDefinitionMap 元素不为0
						   BeanDefinitionNames 元素不为0  
				②getBeanFactory()方法把创建好的DefaultListableBeanFactory返回
				ps:此时BeanDefinition中并不是我们定义好的完整BeanDefinition,期中还是我们加载进来的原始值
			 */
			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

			/*
				Prepare the bean factory for use in this context.
				BeanFactory的预准备工作(BeanFactory进行一些设置,比如context的类加载器等)
			 */
			prepareBeanFactory(beanFactory);

			try {
				/*
					Allows post-processing of the bean factory in context subclasses.
					BeanFactory准备工作完成后进行的后置处理工作,子类通过重写这个方法来在BeanFactory创建并预准备完成以后做进一步的设置
				 */
				postProcessBeanFactory(beanFactory);
// ================================================以上为容器创建和准备工作========================================================= 
				/*
					Invoke factory processors registered as beans in the context.
					实例化实现了BeanFactoryPostProcessor接口的Bean,并调用接口方法
					ps:此时BeanDefinition已经是我们设置好的属性值,已经完成属性赋值操作
				 */
				invokeBeanFactoryPostProcessors(beanFactory);

				/*
					Register bean processors that intercept bean creation.
					注册BeanPostProcessor(Bean的后置处理器),在创建bean的前后等执行
				 */
				registerBeanPostProcessors(beanFactory);

				/*
					Initialize message source for this context.
					初始化MessageSource组件(做国际化功能;消息绑定,消息解析);
				 */
				initMessageSource();

				/*
					Initialize event multicaster for this context.
					初始化事件派发器
				 */
				initApplicationEventMulticaster();

				/*
					Initialize other special beans in specific context subclasses.
					子类重写这个方法,在容器刷新的时候可以自定义逻辑;如创建Tomcat,Jetty等WEB服务器
				 */
				onRefresh();

				/*
					Check for listener beans and register them.
					注册应用的监听器。就是注册实现了ApplicationListener接口的监听器bean
				 */
				registerListeners();

				/*
					Instantiate all remaining (non-lazy-init) singletons.
					初始化所有剩下的非懒加载的单例bean
					初始化创建非懒加载方式的单例Bean实例(未设置属性)
                    填充属性
                    初始化方法调用(比如调用afterPropertiesSet方法、init-method方法)
                    调用BeanPostProcessor(后置处理器)对实例bean进行后置处理
				 */
				finishBeanFactoryInitialization(beanFactory);

				/*
					Last step: publish corresponding event.
					完成context的刷新。主要是调用LifecycleProcessor的onRefresh()方法,并且发布事件(ContextRefreshedEvent)
				 */
				finishRefresh();
			}

			catch (BeansException ex) {
				if (logger.isWarnEnabled()) {
					logger.warn("Exception encountered during context initialization - " +
							"cancelling refresh attempt: " + ex);
				}

				// Destroy already created singletons to avoid dangling resources.
				//销毁已经创建的单例以避免让费资源
				destroyBeans();

				// Reset 'active' flag.
				cancelRefresh(ex);

				// Propagate exception to caller.
				throw ex;
			}

			finally {
				// Reset common introspection caches in Spring's core, since we
				// might not ever need metadata for singleton beans anymore...
				resetCommonCaches();
			}
		}
	}

SpringBean的生命周期:

在这里插入图片描述

spring之循环依赖:循环依赖其实就是循环引⽤,也就是两个或者两个以上的 Bean 互相持有对⽅,最终形成闭环。⽐如A依赖于B,B依赖于C,C⼜依赖于A。

在这里插入图片描述

Spring中循环依赖场景有:

①构造器的循环依赖(构造器注⼊)

②Field 属性的循环依赖(set注⼊)

其中,构造器的循环依赖问题⽆法解决,只能拋出 BeanCurrentlyInCreationException 异常,在解决属性循环依赖时,spring采⽤的是提前暴露对象的⽅法。

Spring 的循环依赖的理论依据: Java 的引⽤传递,当获得对象的引⽤时,对象的属性是可以延后设置的,但是构造器必须是在获取引⽤之前Spring通过setXxx或者@Autowired⽅法解决循环依赖其实是通过提前暴露⼀个ObjectFactory对象来完成的,简单来说ClassA在调⽤构造器完成对象初始化之后,在调⽤ClassA的setClassB⽅法之前就把ClassA实例化的对象通过ObjectFactory提前暴露到Spring容器中。

在这里插入图片描述

spring事务:

①原⼦性(Atomicity) 原⼦性是指事务是⼀个不可分割的⼯作单位,事务中的操作要么都发⽣,要么都不发⽣。从操作的⻆度来描述,事务中的各个操作要么都成功要么都失败

②⼀致性(Consistency) 事务必须使数据库从⼀个⼀致性状态变换到另外⼀个⼀致性状态。例如转账前A有1000,B有1000。转账后A+B也得是2000。⼀致性是从数据的⻆度来说的,(1000,1000) (900,1100),不应该出现(900,1000)

③隔离性(Isolation) 事务的隔离性是多个⽤户并发访问数据库时,数据库为每⼀个⽤户开启的事务,每个事务不能被其他事务的操作数据所⼲扰,多个并发事务之间要相互隔离。⽐如:事务1给员⼯涨⼯资2000,但是事务1尚未被提交,员⼯发起事务2查询⼯资,发现⼯资涨了2000块钱,读到了事务1尚未提交的数据(脏读)

④持久性(Durability)持久性是指⼀个事务⼀旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发⽣故障也不应该对其有任何影响。

spring四种隔离级别:

①Serializable(串⾏化):可避免脏读、不可重复读、虚读情况的发⽣。(串⾏化) 最⾼

②Repeatable read(可重复读):可避免脏读、不可重复读情况的发⽣。(幻读有可能发⽣) 第⼆该机制下会对要update的⾏进⾏加锁

③Read committed(读已提交):可避免脏读情况发⽣。不可重复读和幻读⼀定会发⽣。 第三

④Read uncommitted(读未提交):最低级别,以上情况均⽆法保证。(读未提交) 最低

spring事务传播机制:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

AOP:

AOP(Aspect-Oriented Programming:⾯向切⾯编程)能够将那些与业务⽆关,却为业务模块所共同调⽤的逻辑或责任(例如事务处理、⽇志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接⼝,那么Spring AOP会使⽤JDKProxy,去创建代理对象,⽽对于没有实现接⼝的对象,就⽆法使⽤ JDK Proxy 去进⾏代理了,这时候Spring AOP会使⽤Cglib ,这时候Spring AOP会使⽤ Cglib ⽣成⼀个被代理对象的⼦类来作为代理。

以下图片做为实际开发中遇到的场景

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

AOP相关术语:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

AOP注解代码切面实例:

@Configuration
@ComponentScan("com.xxxx")
@EnableAspectJAutoProxy //开启spring对注解AOP的⽀持
public class SpringConfiguration {
}
 /**
     * 模拟记录⽇志
     */
    @Component
    @Aspect
    public class LogUtil {
        /**
         * 第⼀步:编写⼀个⽅法
         * 第⼆步:在⽅法使⽤@Pointcut注解
         * 第三步:给注解的value属性提供切⼊点表达式
         * 细节:
         * 1.在引⽤切⼊点表达式时,必须是⽅法名+(),例如"pointcut()"。
         * 2.在当前切⾯中使⽤,可以直接写⽅法名。在其他切⾯中使⽤必须是全限定⽅法名。
         */
        @Pointcut("execution(* com.lagou.service.impl.*.*(..))")
        public void pointcut() {
        }
				
        @Before("pointcut()")
        public void beforePrintLog(JoinPoint jp) {
            Object[] args = jp.getArgs();
            System.out.println("前置通知:beforePrintLog,参数是:" +
                    Arrays.toString(args));
        }

        @AfterReturning(value = "pointcut()", returning = "rtValue")
        public void afterReturningPrintLog(Object rtValue) {
            System.out.println("后置通知:afterReturningPrintLog,返回值
                    是:"+rtValue);
        }

        @AfterThrowing(value = "pointcut()", throwing = "e")
        public void afterThrowingPrintLog(Throwable e) {
            System.out.println("异常通知:afterThrowingPrintLog,异常是:" + e);
        }

        @After("pointcut()")
        public void afterPrintLog() {
            System.out.println("最终通知:afterPrintLog");
        }
    }

三、SpringBoot

概念:约定优于配置(Convention over Configuration),又称按约定编程,是一种软件设计规范。本质上是对系统、类库或框架中一些东西假定一个大众化合理的默认值(缺省值)。例如在模型中存在一个名为User的类,那么对应到数据库会存在一个名为user的表,此时无需做额外的配置,只有在偏离这个约定时才需要做相关的配置(例如你想将表名命名为t_user等非user时才需要写关于这个名字的配置)。如果所用工具的约定与你的期待相符,便可省去配置;反之,你可以配置来达到你所期待的方式。简单来说就是假如你所期待的配置与约定的配置一致,那么就可以不做任何配置,约定不符合期待时才需要对约定进行替换配置。

好处:大大减少了配置项

特性:

1、 SpringBoot Starter:他将常用的依赖分组进行了整合,将其合并到一个依赖中,这样就可以一次性添加到项目的Maven或Gradle构建中;

2、 使编码变得简单,SpringBoot采用 JavaConfig的方式对Spring进行配置,并且提供了大量的注解,极大的提高了工作效率。

3、 自动配置:SpringBoot的自动配置特性利用了Spring对条件化配置的支持,合理地推测应用所需的bean并自动化配置他们;

4、 使部署变得简单,SpringBoot内置了三种Servlet容器,Tomcat,Jetty,undertow.我们只需要一个Java的运行环境就可以跑SpringBoot的项目了,SpringBoot的项目可以打成一个jar包。

springboot全局配置文件:

Spring Boot使用一个application.properties或者application.yaml的文件作为全局配置文件

如果是2.4.0之前版本,优先级properties>yaml

但是如果是2.4.0的版本,优先级yaml>properties

springboot常用注解:

@Configuration:声明一个类作为配置类

@Bean:声明在方法上,将方法的返回值加入Bean容器

@Value:属性注入

@ConfigurationProperties(prefix = “jdbc”):批量属性注入

@PropertySource(“classpath:/jdbc.properties”)指定外部属性文件。在类上添加

springboot启动注解:

@SpringBootApplication

@Target({ElementType.TYPE})//注解的适用范围,Type表示注解可以描述在类、接口、注解或枚举中
@Retention(RetentionPolicy.RUNTIME)// 表示注解的生命周期,Runtime运行时
@Documented // 表示注解可以记录在javadoc中
@Inherited // 表示可以被子类继承该注解
@SpringBootConfiguration // 标明该类为配置类
@EnableAutoConfiguration// 启动自动配置功能
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
    // 根据class来排除特定的类,使其不能加入spring容器,传入参数value类型是class类型。
    @AliasFor(annotation = EnableAutoConfiguration.class) Class<?>[] exclude() default {};
    // 根据classname 来排除特定的类,使其不能加入spring容器,传入参数value类型是class的全 类名字符串数组。
    @AliasFor(annotation = EnableAutoConfiguration.class) String[] excludeName() default {}; 
    // 指定扫描包,参数是包名的字符串数组。 
    @AliasFor(annotation = ComponentScan.class, attribute = "basePackages") String[] scanBasePackages() default {};
    // 扫描特定的包,参数类似是Class类型数组。 
    @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses") Class<?>[] scanBasePackageClasses() default {}; }
}

从上述源码可以看出,@SpringBootApplication注解是一个组合注解,前面 4 个是注解的元数据信息, 我们主要看后面 3 个注解@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan三个核心注解,关于这三个核心注解的相关说明具体如下

@SpringBootConfiguration

@SpringBootConfiguration : SpringBoot 的配置类,标注在某个类上,表示这是一个 SpringBoot的配置类。

查看@SpringBootConfiguration注解源码,核心代码具体如下。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME) 
@Documented @Configuration 
// 配置类的作用等同于配置文件,配置类也是容器中的一个对象
public @interface SpringBootConfiguration { }

从上述源码可以看出,@SpringBootConfiguration注解内部有一个核心注解@Configuration,该注解是Spring框架提供的,表示当前类为一个配置类(XML配置文件的注解表现形式),并可以被组件扫描器扫描。由此可见,@SpringBootConfiguration注解的作用与@Configuration注解相同,都是标识一个可以被组件扫描器扫描的配置类,只不过@SpringBootConfiguration是被Spring Boot进行了重新封装命名而已

@EnableAutoConfiguration

// 自动配置包
@AutoConfigurationPackage
// Spring的底层注解@Import,给容器中导入一个组件;
// 导入的组件是AutoConfigurationPackages.Registrar.class
@Import(AutoConfigurationImportSelector.class)
// 告诉SpringBoot开启自动配置功能,这样自动配置才能生效。
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    // 返回不会被导入到 Spring 容器中的类
    Class<?>[] exclude() default {};
    // 返回不会被导入到 Spring 容器中的类名
    String[] excludeName() default {};
}

Spring 中有很多以 Enable 开头的注解,其作用就是借助 @Import 来收集并注册特定场景相关的Bean ,并加载到 IOC 容器。@EnableAutoConfiguration就是借助@Import来收集所有符合自动配置条件的bean定义,并加载到IoC容器。

关于条件注解:

关于条件注解的讲解

@Conditional是Spring4新提供的注解,它的作用是按照一定的条件进行判断,满足条件给容器注册bean。

@ConditionalOnBean:仅仅在当前上下文中存在某个对象时,才会实例化一个Bean。

@ConditionalOnClass:某个class位于类路径上,才会实例化一个Bean。

@ConditionalOnExpression:当表达式为true的时候,才会实例化一个Bean。基于SpEL表达式的条件判断。

@ConditionalOnMissingBean:仅仅在当前上下文中不存在某个对象时,才会实例化一个Bean。

@ConditionalOnMissingClass:某个class类路径上不存在的时候,才会实例化一个Bean。

@ConditionalOnNotWebApplication:不是web应用,才会实例化一个Bean。

@ConditionalOnWebApplication:当项目是一个Web项目时进行实例化。

@ConditionalOnNotWebApplication:当项目不是一个Web项目时进行实例化。

@ConditionalOnProperty:当指定的属性有指定的值时进行实例化。

@ConditionalOnJava:当JVM版本为指定的版本范围时触发实例化。

@ConditionalOnResource:当类路径下有指定的资源时触发实例化。

@ConditionalOnJndi:在JNDI存在的条件下触发实例化。

@ConditionalOnSingleCandidate:当指定的Bean在容器中只有一个,或者有多个但是指定了首选的Bean时触发实例化。

springboot启动流程(包含自动装配):

1.当启动springBoot应用程序的时候,首选会创建启动类的对象(SpringApplication),在对象的构造方法中会进行某些参数初始化工作,最主要的是判断当前应用程序的类型及其初始化器和监听器,在这个过程中会去加载当前应用程序的spring.factories文件并且会把我们不需要的配置文件过滤掉留下我们需要的配置文件全路径类名称(包名+类名),然后把这些留下的配置保存到缓存中以便后续调用。

2.SpringApplication应用程序启动完后,会执行run方法,来完成整个启动,在启动过程中有两个最主要的方法,第一个叫做prepareContext,第二个叫做refreshContext方法,在这两个关键步骤中完成自动装配的核心功能,前面的处理逻辑包含了上下文对象的创建,和banner的打印,异常报告的准备工作,方便后续来进行调用。

3.在prepareContext方法中主要完成了是对上下文对象的初始化工作,包括了属性赋值,比如环境对象,在整个过程中有个重要的方法叫做load,load方法主要完成一件事就是将当前启动类作为一个BeanDefinition注册到registry中,方便后续进行BeanFactoryPostProcessor调用执行的时候,找到对应的主类,来完成@SpringBootApplicaition,@EnableAutoConfiguration等注解的解析。

4.在refreshContext方法中会进行整个容器的刷新过程,会调用spring的refresh方法,refresh方法中有13个重要的步骤,来完成整个容器的启动,在自动装配的过程中会执行invokeBeanFactoryPostProcessors方法,在此方法中主要是对ConfigurationClassPostProcessor类的处理,这个类是BeanFactoryPostProcessor的子类,也是BeanDefinitionRegistryPostProcessor子类,然后执行postProcessBeanDefinitionRegistry方法的过程总会解析处理各种注解。包含@PropertySource ,@ComponentScan,@Import,@Bean等注解,其中最主要的就是@Import注解、

5.在解析@Import注解的时候有一个getImports方法,从主类开始递归解析注解,然后在processImports方法中对Import得类进行分类,然后通过DeferredImportSelectorHandler类中的process方法来完成EnableAutoConfiguration的加载

精髓

  1. SpringBoot 启动会加载大量的自动配置类
  2. 我们看我们需要实现的功能有没有 SpringBoot 默认写好的自动配置类
  3. 我们再来看这个自动配置类中到底配置了哪些组件;(只要我们有我们要用的组件,我们就不需要再来配置了)
  4. 给容器中自动配置类添加组件的时候,会从 properties 类中获取某些属性,我们就可以在配置文件中指定这些属性的值。xxxAutoConfiguration :自动配置类,用于给容器中添加组件从而代替之前我们手动完成大量繁琐的配置。xxxProperties : 封装了对应自动配置类的默认属性值,如果我们需要自定义属性值,只需要根据xxxProperties 寻找相关属性在配置文件设值即可。
SpringBoot starter机制:

SpringBoot中的starter是一种非常重要的机制,能够抛弃以前繁杂的配置,将其统一集成进starter,应用者只需要在maven中引入starter依赖,SpringBoot就能自动扫描到要加载的信息并启动相应的默认配置。starter让我们摆脱了各种依赖库的处理,需要配置各种信息的困扰。SpringBoot会自动通过classpath路径下的类发现需要的Bean,并注册进IOC容器。SpringBoot提供了针对日常企业应用研发各种场景的spring-boot-starter依赖模块。所有这些依赖模块都遵循着约定成俗的默认配置,并允许我们调整这些配置,即遵循“约定大于配置”的理念。

四、SpringCloud

概念:Spring Cloud并没有重复制造轮⼦,它只是将⽬前各家公司开发的⽐较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot⻛格进⾏再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了⼀套简单易懂、易部署和易维护的分布式系统开发⼯具包。

如前所述,Spring Cloud是⼀个微服务相关规范,这个规范意图为搭建微服务架构提供⼀站式服务,采⽤组件(框架)化机制定义⼀系列组件,各类组件针对性的处理微服务中的特定问题,这些组件共同来构成Spring Cloud微服务技术栈。

六大组件:
  1. Spring Cloud Eureka:服务注册与发现(已经不在维护,替换成Nacos)
  2. Spring Cloud Zuul:服务网关(已经不在维护,替换GateWay)
  3. Spring Cloud Ribbon:客户端负载均衡
  4. Spring Cloud Feign:声明性的Web服务客户端(不在维护,OpenFeign)
  5. Spring Cloud Hystrix:断路器
  6. Spring Cloud Confifig:分布式统一配置管理
Eureka

在这里插入图片描述

在这里插入图片描述

五、MySQL

数据结构

引擎

索引

Mysql中根据 索引中是否存在数据,将索引分为了两种类型的索引:聚簇索引和非聚簇索引。

聚簇索引

聚簇索引是在索引树的叶子节点中保存了完整的数据信息,则索引称为聚簇索引。Mysql的 InnoDB引擎为主键创建的主键索引即是聚簇索引,数据文件即是索引文件。MyIsam引擎中的主键索引也是非聚簇索引。

非聚簇索引

非聚簇索引是指索引树的叶子节点中保存的不是完整的数据信息,而是指向数据的一个指针或者是地址信息等(MyIsam的主键索引是指向数据的地址信息,InnoDB 中是主键的Id)。如果需要获取全部的数据信息,需要进行一次回表的操作。

联合索引

联合索引是同时对多个列创建的索引。索引树中的叶子节点会同时包含多个列的值,叶子节点之间的排序,会根据创建索引时候列的顺序排序,排序满足最左前缀规则。

最左前缀规则:例如当存在一个联合索引,包括了A、B、C三列的时候,如果查询是使用了 A、【A、B】、【A、B、C】的查询方式,则可以使用这个索引,如果是 使用了 【B、C】作为查询条件,则不满足最左前缀原则,则不会使用这个索引。

因为Mysql在创建联合索引的时候,是先去比较第一列的值,当第一列的值相同的时候,才会比较第二列的值,当第二列的值相同的时候,再去比较第三列的值,以此类推,直到最后的一列。所以,当跳过第一列的值,直接匹配第二列的时候,此时第二列的值不一定是有序的,所以无法使用。

覆盖索引

当使用某个查询条件的时候,如果需要查询的所有字段在索引中已经存在,即可直接返回,不用再根据查询到的主键ID进行回表操作,称为覆盖索引。

如果我们为name和age 创建了一个索引。当我们查询name为指定值的数据的age 和Id的时候,就会使用覆盖索引。因为在索引数据已经存在了 name 和 age 的值(主键ID的值是每一个索引都会存在的),不会再根据id回表聚簇索引查询数据。

如果在索引中不存在查询的字段数据信息,则会在查找到满足条件的数据之后,会根据查找到数据的主键ID,回表聚簇索引查找其他的信息,最终返回数据。

唯一索引

唯一索引要求指定的列的值不能存在相同的值,数据库在每一次进行添加或者修改的时候,都会进行数据的检查,如果数据存在了重复的值,则会直接返回失败。

索引失效
  • 索引列上做运算
    like查询以"%“开头因为Mysql在比较字符串的时候,是根据字符串从左到右做比较,满足最左匹配原则,当使用”%"开头查询的时候,就会破坏最左原则,所以不能使用索引。

  • 破坏最左前缀原则:联合索引查询的时候,如果不满足最左前缀原则,则不会使用索引

  • 范围查询的右边索引会失效,当有A、B、C三个联合索引的时候,如果where A = 123 and B > 20 and C = 456;则C的索引会失效。因为 B 会返回多个值,会根据返回的多个值再去做匹配查询。因为在多个值的情况下,C的排序未必是有序的,可能是乱序的情况,无法使用索引。

  • or 查询,or 前后条件只要包含非索引列,那么就不会使用到索引

  • 不等于!=或者<>导致索引失效

  • is not null无法使用索引,is null可以使用索引

  • 字符串没有加单引号:会发生类型的隐式转换

事务

事务的四大特征:

一般来说,事务是必须满足4个条件(ACID)::原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。

原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。(比如:A向B转账,不可能A扣了钱,B却没有收到)

隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。(比u人:A正在从一张银行卡里面取钱,在A取钱的过程中,B不能向这张银行卡打钱)

持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

隔离级别:

(1)读未提交:read uncommitted

  • 事物A和事物B,事物A未提交的数据,事物B可以读取到
  • 这里读取到的数据叫做“脏数据”
  • 这种隔离级别最低,这种级别一般是在理论上存在,数据库隔离级别一般都高于该级别

(2)读已提交:read committed

  • 事物A和事物B,事物A提交的数据,事物B才能读取到
  • 这种隔离级别高于读未提交
  • 换句话说,对方事物提交之后的数据,我当前事物才能读取到
  • 这种级别可以避免“脏数据”
  • 这种隔离级别会导致“不可重复读取”
  • Oracle默认隔离级别

(3)可重复读:repeatable read

  • 事务A和事务B,事务A提交之后的数据,事务B读取不到
  • 事务B是可重复读取数据
  • 这种隔离级别高于读已提交
  • 换句话说,对方提交之后的数据,我还是读取不到
  • 这种隔离级别可以避免“不可重复读取”,达到可重复读取
  • 比如1点和2点读到数据是同一个
  • MySQL默认级别
  • 虽然可以达到可重复读取,但是会导致“幻像读”

(4)串行化:serializable

  • 事务A和事务B,事务A在操作数据库时,事务B只能排队等待
  • 这种隔离级别很少使用,吞吐量太低,用户体验差
  • 这种级别可以避免“幻像读”,每一次读取的都是数据库中真实存在数据,事务A与事务B串行,而不并发
sql优化
  • EXPLAIN关键子查询判断当前sql是否使用到索引然后根据提示来找出哪些地方需要简历索引。

  • 对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。

  • 应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。

  • 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:

  • select id from t where num is null
    
  • 可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:

  • select id from t where num=0
    
  • 应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:

  • select id from t where num=10 or num=20
    
  • 可以这样查询:

  • select id from t where num=10
    union all
    select id from t where num=20
    
  • 下面的查询也将导致全表扫描:

  • select id from t where name like%abc%
  • 若要提高效率,可以考虑全文检索。

  • in 和 not in 也要慎用,否则会导致全表扫描,如:

  • select id from t where num in(1,2,3)
    
  • 对于连续的数值,能用 between 就不要用 in 了:

  • select id from t where num between 1 and 3
    
  • 很多时候用 exists 代替 in 是一个好的选择:

  • 用下面的语句替换:

select num from a where exists(select 1 from b where num=a.num)

六、Redis

数据格式
String 字符串

常用命令:

setnx,set,get,decr,incr,mget 等。

字符串是最常用的数据类型,他能够存储任何类型的字符串,当然也包括二进制、JSON化的对象、甚至是Base64编码之后的图片。在Redis中一个字符串最大的容量为512MB,可以说是无所不能了。redis的key和string类型value限制均为512MB。虽然Key的大小上限为512M,但是一般建议key的大小不要超过1KB,这样既可以节约存储空间,又有利于Redis进行检索

  • 缓存,热点数据
  • 分布式session
  • 分布式锁
  • INCR计数器
  • 文章的阅读量,微博点赞数,允许一定的延迟,先写入 Redis 再定时同步到数据库
  • 全局ID
  • ​ INT 类型,INCRBY,利用原子性
  • INCR 限流
  • ​ 以访问者的 IP 和其他信息作为 key,访问一次增加一次计数,超过次数则返回 false。
  • setbit 位操作
淘汰策略
  1. noeviction(默认策略):对于写请求不再提供服务,直接返回错误(DEL请求和部分特殊请求除外)
  2. allkeys-lru:从所有key中使用LRU算法进行淘汰(LRU算法:即最近最少使用算法)
  3. volatile-lru:从设置了过期时间的key中使用LRU算法进行淘汰
  4. allkeys-random:从所有key中随机淘汰数据
  5. volatile-random:从设置了过期时间的key中随机淘汰
  6. volatile-ttl:在设置了过期时间的key中,淘汰过期时间剩余最短的

redis怎么实现一致性

一致性就是数据保持一致,在分布式系统中,可以理解为多个节点中数据的值是一致的。

**强一致性:**这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大
**弱一致性:**这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态
**最终一致性:**最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型

解决一直性主要有以下几种方案:

先更新数据库,再更新缓存这套方案,大家是普遍反对的。为什么呢?

有如下两点原因。

(线程安全角度)
同时有请求A和请求B进行更新操作,那么会出现

线程 A 更新了数据库
线程 B 更新了数据库
线程 B 更新了缓存
线程 A 更新了缓存
这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。缓存和数据库的数据不一致了。缓存保存的是老数据,数据库保存的是新数据。这就导致了脏数据,因此不考虑。

该方案会导致不一致的原因是。同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:

(1)请求 A 进行写操作,删除缓存
(2)请求 B 查询发现缓存不存在
(3)请求 B 去数据库查询得到旧值
(4)请求 B 将旧值写入缓存
(5)请求 A 将新值写入数据库

上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。

那么,如何解决呢?

采用延时双删策略
(1)先淘汰缓存
(2)再写数据库(这两步和原来一样)
(3)休眠1秒,再次淘汰缓存
这么做,可以将1秒内所造成的缓存脏数据,再次删除。

七、Rabbitmq消息中间件

https://p.ipic.vip/3pgayj.jpg

八、Dubbo

注册发现流程图

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

工作原理

https://p.ipic.vip/9leg4u.jpg

九、微服务

优点:

1.服务部署更加灵活,每个应用都可以是一个独立的项目,可以独立部署,不依赖其他服务,耦合性降低

2.技术更新灵活,在大型单体应用中,技术更新往往是非常困难的,而微服务可以根据业务特点灵活选择技术栈

3.应用的性能得到很大提升,大型单体项目往往启动就会成为一个很大的难关,而采用微服务以后,整个系统的性能得到很大提升

4.更容易组合专门的团队,:在单体应用时团队成员往往需要对系统的各个部分都要有深入了解,门槛石很高的,采用微服务以后,可以给每个微服务组建专门的团队

缺点:

1.服务调用的复杂性增高,例如网络问题,容错问题负载均衡问题高并发问题等等

2.分布式事务,尽量不要使用微服务事务

3.测试难度提升

4.运维部署项目难度提升

十、算法

冒泡排序
  1. 从第一个元素开始,比较相邻的两个元素。如果第一个比第二个大,则进行交换。
  2. 轮到下一组相邻元素,执行同样的比较操作,再找下一组,直到没有相邻元素可比较为止,此时最后的元素应是最大的数。
  3. 除了每次排序得到的最后一个元素,对剩余元素重复以上步骤,直到没有任何一对元素需要比较为止。
public void bubbleSortOpt(int[] arr) {
 
    if(arr == null) {
        throw new NullPoniterException();
    }
    if(arr.length < 2) {
        return;
    }
    int temp = 0;
    for(int i = 0; i < arr.length - 1; i++) {
        for(int j = 0; j < arr.length - i - 1; j++) {
            if(arr[j] > arr[j + 1]) {
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}
快速排序

快速排序的思想很简单,就是先把待排序的数组拆成左右两个区间,左边都比中间的基准数小,右边都比基准数大。接着左右两边各自再做同样的操作,完成后再拆分再继续,一直到各区间只有一个数为止。

举个例子,现在我要排序 4、9、5、1、2、6 这个数组。一般取首位的 4 为基准数,第一次排序的结果是:

2、1、4、5、9、6

可能有人觉得奇怪,2 和 1 交换下位置也能满足条件,为什么 2 在首位?这其实由实际的代码实现来决定,并不影响之后的操作。以 4 为分界点,对 2、1、4 和 5、9、6 各自排序,得到:

1、2、4、5、9、6

不用管左边的 1、2、4 了,将 5、9、6 拆成 5 和 9、6,再排序,至此结果为:

1、2、4、5、6、9

public void quicksort(int[] arr, int start, int end) {
 
    if(start < end) {
        // 把数组中的首位数字作为基准数
        int stard = arr[start];
        // 记录需要排序的下标
        int low = start;
        int high = end;
        // 循环找到比基准数大的数和比基准数小的数
        while(low < high) {
            // 右边的数字比基准数大
            while(low < high && arr[high] >= stard) {
                high--;
            }
            // 使用右边的数替换左边的数
            arr[low] = arr[high];
            // 左边的数字比基准数小
            while(low < high && arr[low] <= stard) {
                low++;
            }
            // 使用左边的数替换右边的数
            arr[high] = arr[low];
        }
        // 把标准值赋给下标重合的位置
        arr[low] = stard;
        // 处理所有小的数字
        quickSort(arr, start, low);
        // 处理所有大的数字
        quickSort(arr, low + 1, end);
    }
}
选择排序

选择排序是一种简单直观的排序算法,首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

1. 简单选择排序

选择排序思想的暴力实现,每一趟从未排序的区间找到一个最小元素,并放到第一位,直到全部区间有序为止。

public void selectSort(int[] arr) {
    // 遍历所有的数
    for (int i = 0; i < arr.length; i++) {
        int minIndex = i;
        // 把当前遍历的数和后面所有的数进行比较,并记录下最小的数的下标
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[j] < arr[minIndex]) {
                // 记录最小的数的下标
                minIndex = j;
            }
        }
        // 如果最小的数和当前遍历的下标不一致,则交换
        if (i != minIndex) {
            int temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
}

哈哈哈~面试不通过你来打我,希望能帮助大家找工作,如果有帮助到大家希望大家点点赞👍🏻!谢谢!

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值