JAVA基础
Q1:类初始化过程
一个类要创建实例必须先加载并初始化该类
- main()方法所在的类需要先加载和初始化
一个子类要初始化需要先初始化父类
一个类初始化就是执行class文件中的clinit()方法
- clinit()方法由静态类变量显示赋值代码和静态代码块组成
- 类变量显示赋值代码和静态代码块由上到下依次执行
- clinit()方法只执行一次
Q2:实例初始化过程
实例初始化就是执行class文件中init()方法
- init()方法可能重载有多个,有几个构造器就有几个()方法
- init()方法由非静态实例变量显示赋值代码和非静态代码块和对应构造器代码组成
- 非静态实例变量显示赋值代码和非静态代码块按照顺序执行,但是对应的构造器最后执行
- 每次创建实例对象,调用对应的构造器,执行的就是对应的init()方法
- init()方法的首行是super()或者super(实参列表),即对应父类的init()方法
Q3:方法的重写Override
那些方法不可以被重写
final修饰的方法,不可被重写
静态方法,不可被重写
private等子类中不可见的方法
对象的多态
子类重写父类的方法,通过子类对象调用的一定是子类重写过的代码
非静态方法默认的调用对象是this
this对象在构造器或者方法中就是正在创建的对象
== Q1、Q2、Q3一起看==
Q4:方法的参数传递机制
形参是基本数据类型—传递数据值
实参是引用数据类型—传递引用地址
实参是引用数据类型—特殊类型String和包装类对象不可变性
Q5:集合关系图
- 集合只存放引用对象,包括基本对象的引用对象例如:Integer等
- List接口的实现对象均为不唯一(可以重复数据)、有序
- Set接口的实现对象均为唯一,无序
- 普通的Iterator对象无法一边遍历List对象,一边对List对象进行增加、删除和修改的操作,如果想要同时操作,可以使用Iterator的子类ListIterator来创建对象实现上述功能。
实线为继承,虚线为实现
Q6:ArrayList底层原理
Q7:Vector底层原理
Q8:ArrayList和Vector的相同点和不同点
-
相同点:
ArrayList和Vector底层都是数组扩容
都具备数组的优点:查询效率高、元素可重复
都具备数组的缺点:删除和新增效率低 -
不同点:
ArrayList底层扩容为原数组的1.5倍,并且线程不安全,效率高
Vector底层扩容为原数组的2倍,并且线程安全,效率低,已被JDK淘汰
Q9:Iterator和Iterator()和Iterable之间的关系
Q10:HashMap底层原理(JDK1.7版)
- HashMap特点:唯一、无序
- LinkedHashMap继承HashMap可以实现按照输入顺序进行输出,特点:唯一、有序
- 装填因子、负载因子、加载因子为什么是0.75?
1.负载因子设置为1时:空间利用率得到很大满足,但是容易产生哈希碰撞,产生链表,会导致查询效率低;
2.负载因子设置为0.5时:发生哈希碰撞概率低,产生链表概率低,查询效率高,但是空间利用率太低(每次扩容时会产生新数组接收老数组数据,造成空间浪费)
3.综合0.5和1值产生的效果,折中取值0.75,二者兼顾,达到最佳。
Q11:HashMap底层原理JDK1.7版和JDK1.8版区别
Q12:HashSet底层原理
- HashSet的底层实际是利用HashMap进行实现的,在存储数据时,将值存储到HashMap中的KEY的位置上,VALUE位置上使用present=new Object()对象来占位,例如如下模式:map=new HashMap<>();—>map(“具体的值”,new Object());—>相当于map(“具体的值”,null);
Q10和Q12对比记忆
Q13:HashTable底层
- HashTable与HashMap底层一致,用法相同,具体底层可以参考HashMap
- HashTable与HashMap区别:
1.HashTable是在JDK1.0时产生的集合类,效率低,线程安全,KEY不能为null
2.HashMap是JDK1.2时产生的集合类,效率高,线程不安全,并且KEY的null也是可以支持,并且遵循唯一的特点。
Q13参考Q10对比记忆
Q14:TreeMap底层原理
-
TreeMap特点:
1.唯一(没有重复数据),无序(没有按照输入顺序进行输出)、有序(按照升序或者降序进行遍历展示)
2.底层使用二叉树,key的遵循二叉树的特点,放入集合中的key的数据对应的类必须实现比较器(内部比较器【实现Comparable接口重写其中方法】,外部比较器【实现Comparator接口】) -
TreeMap原理图:
Q15:TreeSet底层原理
- 1.TreeSet特点:唯一(没有重复数据),无序(没有按照输入顺序进行输出)、有序(按照升序进行遍历展示)
- 2.底层使用二叉树(数据结构中的一个逻辑结构),并且放入数据时,其内部必须存在内部或者外部的比较器,比较完成后,才能将数据存入TreeSet对象中,尤其是自定义的类型(例如:Student类生成的对象放入TreeSet中,并且要指定比较器的比较规则)
- 3.TreeSet在遍历时得到的升序结果,是因为TreeSet的底层遍历是按照中序遍历实现的,如下为各种遍历方式:
中序遍历: 左子树—根节点—右子树 (当前TreeSet底层所使用的方式)
先序遍历: 根节点—左子树—右子树
后序遍历: 左子树—右子树—根节点
- TreeSet的底层:
TreeSet的底层,实际是利用TreeMap进行实现的,在存储数据时,将值存储到TreeMap中的KEY的位置上,VALUE位置上使用present=new Object()对象来占位,例如如下模式:map=new TreeMap<>();—>map(“具体的值”,new Object());—>相当于map(“具体的值”,null);
Q16:ConcurrentHashMap
- ConcurrentHashMap=HashMap+synchronized+cas,是线程安全的,通过cas控制sizectl(sizecrontroller)变量的正负值,如果sizectl为-1,表示正在初始化数组,-(1+N)表示正在扩容,正数表示扩容阈值,通过cas操作防止多线程进入,并且底层添加元素的方法是由synchronized修饰,所以ConcurrentHashMap整体是安全的。
Q17:作用域修饰符
修饰符 | 同类 | 同包 | 子类可访问 | 整个项目可访问 |
---|---|---|---|---|
private | Y | N | N | N |
default | Y | Y | N | N |
protected | Y | Y | Y | N |
public | Y | Y | Y | Y |
JVM
Q1:JVM垃圾回收机制,GC发生在JVM哪部分,有几种GC,他们的算法是什么?
参考:https://blog.csdn.net/DSL95/article/details/123190755
链接: 点击跳转.
多线程
参考:https://blog.csdn.net/DSL95/article/details/123456684
链接: 点击跳转.
Q1:手写死锁过程
package indi.dsl;
public class DeadLock {
public static void main(String[] args) {
Object a = new Object();
Object b = new Object();
//a线程
new Thread(()->{
synchronized (a){
System.out.println("我是a,我想要等等");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是a,我想要b资源");
synchronized (b){
System.out.println("我给了b资源");
}
}
}).start();
//b线程
new Thread(()->{
synchronized (b){
System.out.println("我是b,我想要等等");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是b,我想要a资源");
synchronized (a){
System.out.println("我给了a资源");
}
}
}).start();
}
}
- 执行结果
Spring
Q1:Spring是什么
- Spring是一个轻量级的控制反转(IOC)和面向切面的(AOP) 的容器框架。
1.从大小与开销两方面而言Spring都是轻量级的;(对代码的侵入性低,spring提供的接口可用可不用)
2.通过控制反转(IOC)的技术达到松耦合的目的;(降低对象之间的强依赖)
3.提供了面向切面编程的丰富支持,容许通过分离应用的业务逻辑与系统级服务进行内聚性开发;
4.包含并管理应用对象(Bean)的配置和生命周期,这个意义上是一个容器;
5.将简单的组件配置,组合成为复杂的应用,这个意义上是一个框架。
Q2: Bean生命周期原理图
- 容器和对象的创建流程:
1.先创建容器
2.加载配置文件,封装成BeanDefinition
3.调用执行BeanFactoryPostProcessor
4.实例化前准备工作:准备beanPostprocessor,准备监听器、时间、广播器
5.实例化
6.初始化
7.获取完整对象
Q3:谈谈对SpringIOC的理解,原理和实现
总:
- 控制反转:理论思想,原来的对象是由使用者来进行控制的,有了spring之后,可以把整个对象交给spring来管理;DI:依赖注入,把对应的属性值注入到具体的对象中,@Autowired注解或populateBean方法完成属性值的注入。
- 容器:存储对象,使用map结构来存储,在spring中一般纯在三级缓存,singletonObjects存放完整的bean对象,整个bean的生命周期,从创建到使用再到销毁的过程全部都是由容器来管理的(bean的生命周期)
分:
- 1.通过顶层根接口beanFactory的子类实现DefaultListableBeanFactory,完成bean工厂的创建。
向bean工厂中设置一些参数,例如:BeanPostProcessor、Aware接口的子类等等属性。
- 2.加载解析bean对象,准备要创建的bean对象的定义对象beanDefinition对象(XML或者注解的解析过程)
- 3.进行beanFactoryPostProcessor扩展点的处理,其中有两个比较重要的子类实现,一个是ConfigurationClassPostProcessor(对于配置类进行扫描并且注册)、PlaceHolderConfigurSupport(配置文件中占位符的处理)
- 4.通过registerBeanPostProcessors注册所有BeanPostProcessor扩展点,方便后续对bean对象完成具体的功能扩展
- 5.通过反射的方式将BeanDefinition对象实例化成具体的bean对象
- 6.bean对象的初始化过程(填充属性,调用aware子类的方法,调用BeanPostProcessor前置处理方法,调用init-method方法,调用BeanPostProcessor的后置处理方法)
- 7.生成完整的bean对象,通过getBean方法可以直接获取
- 8.销毁过程:容器将会检查bean对象的实例,是否实现了Disposablebean接口,或者是否存在自定义的destroy-method方法。如果存在,就调用相应方法进行销毁.
就会为该实例注册一个用于对象销毁的回调( Callback)
注意:Spring容器会检查当前对象实例是否实现了一系列的以Aware命名结尾的接口定义,如果是,则将这些Aware接口定义中规定的依赖注入给当前对象实例
Q4:谈一下SpringIOC的底层实现
- 1.先通过createBeanFactory创建出一个Bean工厂(DefaultListableBeanFactory)
- 2.开始循环创建对象,因为容器中的Bean默认都是单例的,所以优先通过getBean、doGetBean从容器中查找,如果找不到
- 3.通过createBean、doCreateBean方法,以反射的方式创建对象,一般情况下使用的是无参构造方法(getDeclaredConstructor、newInstance)
- 4.进行对象的属性填充populateBean
- 5.进行其他的初始化操作(initializingBean)
Q5:Spring是如何解决循环依赖的问题
- 关键词:三级缓存,提前暴露对象,AOP
总:什么是循环依赖问题,A依赖B,B依赖A
分:先说明bean的创建过程:实例化,初始化(填充属性)
- 1.先创建A对象,实例化A对象,此时A对象中的B属性为空,填充属性B
- 2.从容器中查找B对象,如果找到,直接赋值,此时不存在循环依赖问题(不通),找不到直接创建B对象
- 3.实例化B对象,此时B对象中的A属性为空,填充属性A
- 4.从容器中查找A对象,找不到,直接创建
形成闭环的原因
- 此时,发现A对象是存在的,只不过此时的对象不是一个完整的状态,只完成了实例化但是未完成初始化,如果在程序调用过程中,可以先把非完整状态的A对象优先赋值B对象,此时B对象是完整的,再将完整的B对象赋值给A对象,此时A对象也是完整的。A、B对象的正真赋值可以等待对象被真正使用时来完成,相当于提前暴漏了某个不完整对象的引用,所以解决问题的核心在于实例化和初始化分开操作,这也是解决循环依赖问题的关键
- 当所有的对象都完成实例化和初始化操作之后,还要把完整对象放到容器中,此时在容器中存在对象的几个状态,完成实例化,但未完成初始化和完整状态,因为都在容器中,所以要使用不同的map结构来进行存储,此时就有了一级缓存和二级缓存。如果一级缓存中有了,那么二级缓存中就不会存在同名的对象,因为他们的查找顺序是:一级、二级、三级这样的方法来查找的。一级缓存中放的是完整对象,二级缓存中放的是非完整对象。
为什么需要三级缓存?
- 三级缓存的value类型是ObjectFactory,是一个函数时接口,存在的意义是保证在整个容器的运行过程中同名的bean对象只能有一个。
如果一个对象需要被代理,或者说要生成代理对象,那么要不要优先生成一个普通对象? 答案是:要
- 普通对象和代理对象是不能同时出现在容器中的,一次当一个对象需要被代理的时候,就要使用代理对象覆盖掉之前的普通对象,在实际的调用过程中,是没有办法确定什么时候对象被使用,所以就要求某个对象被调用的时候,优先判断此对象是否需要被代理,类于一种回调机制的实现,因此传入Lambda表达式的时候,可以通过Lambda表达式来执行对象的覆盖过程,getEarlyBeanReference()。
- 因此,所有的bean对象在创建的时候都要优先放到三级缓存中,在后续的使用过程中,如果需要被代理则返回代理对象,如果不需要代理,则直接返回普通对象。
Q6:缓存的放置时间和删除时间
- 三级缓存:createBeanInstance之后:addSingletonFactory
- 二级缓存:第一次从三级缓存确定对象是代理对象还是普通对象的时候,同时删除三级缓存getSingleton
- 一级缓存:生成完整对象之后放到一级缓存,删除二三级缓存:addSingleton
Q7:BeanFactory与FactoryBean有什么区别
- 相同点:都是用来创建bean对象的
- 不同点:使用BeanFactory创建对象的时候,必须要准售严格的生命周期流程,比较复杂,如果想要简单的自定义某个对象的创建,同时创建完成的对象想交给Spring来管理,那么就需要实现FactoryBean接口了,并且重写如下方法:
isSingleton:是否是单例对象
getObjectType:获取返回对象的类型
getObject;自定义创建对象的过程(new、反射、动态代理)
Q8:Spring中用到的设计模式
单例模式:bean默认都是单例的
原型模式:指定作用域为Prototype
工厂模式:BeanFactory
模板方法:postProcessBeanFactory、onRefresh、initPropertyValye
策略模式:XmlBeanDefinitionReader、PropertiesBeanDefinitionReader
观察者模式:Listener、event、multicast
适配器模式:Adapter
装饰着模式:BeanWrapper
责任链模式:使用AOP的时候会先生成一个拦截器
代理模式:动态代理
委托模式:delegate
Q9:Spring的AOP的底层实现原理
总:
- 描述:AOP是IOC的一个扩展功能,先有IOC再有AOP,只是在IOC的整个流程中新增的一个扩展点而已:BeanPostProcessor。
- AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。
- 应用场景:日志 、事务 具体实现:动态代理
分:
- bean的创建过程中有一个步骤可以对bean进行扩展,AOP本身就是一个扩展功能,所以在BeanPostProcessor的后置处理方法中来进行实现
- 1代理对象的创建过程(advice、切面、切点)
- 2.通过JDK或者CGLIB的方式来生成代理对象
- 3.在执行方法调用的时候,会调用到生成的字节码文件中,直接会找到DynamicAdvisoredInterceptor类中的interceptor方法,从此方法开始执行
- 4.根据之前定义好的通知来生成拦截器链
- 5.从拦截器链中一次货期每一个通知开始进行执行,在执行过程中,为了方便找到下一个通知是哪个,会有一个CglibMethodInvocation的对象,找到的时候是从-1的位置开始查找并且执行的。
Q10:Spring的事务是如何回滚的(事务管理如何实现的)
总:
- spring的事务是由AOP来实现的,首先要生成具体的代理对象,然后按照AOP的整套流程来执行具体的操作逻辑,正常情况下要通过通知来完成核心功能,但是事务不是通过通知来实现的,而是通过一个TransactionInterceptor来实现的,然后调用invoke来实现具体的逻辑
分:
- 1.先做准备工作,解析各个方法上的事务相关属性,根据具体的属性来判断是否开始新事物
- 2.当需要开启的时候,获取数据库连接,关闭自动提交功能,开启事务
- 3.执行具体的sql逻辑操作
- 4.在操作过程中,如果执行失败了,那么会通过completeTransactionAfterThrowing看来完成事务的回滚操作,回滚的具体逻辑是通过doRollBack方法来实现的,实现的时候也要先获取连接对象,通过连接对象来回滚
- 5.如果执行过程中,没有任何意外i情况的发生,那么通过commitTransactionAfterReturning来完成事务的提交操作,提交具体逻辑是通过doCommit方法来实现的,实现的时候也要获取连接,通过连接对象来提交。
- 6.当事务执行完毕之后需要清除相关的事务信息cleanupTransaction
Q11:事务的传播机制
- 传播行为:
1、PROPAGATION_REQUIRED:(支持事物)如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
2、PROPAGATION_SUPPORTS:(支持事物)支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。‘
3、PROPAGATION_MANDATORY:(支持事物)支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
4、PROPAGATION_REQUIRES_NEW:(支持事物)创建新事务,无论当前存不存在事务,都创建新事务。
5、PROPAGATION_NOT_SUPPORTED:(不支持事物)以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
6、PROPAGATION_NEVER:(不支持事物)以非事务方式执行,如果当前存在事务,则抛出异常。
7、PROPAGATION_NESTED:(不支持事物)如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
参考:https://www.cnblogs.com/baizhanshi/p/10425467.html
链接: 点击跳转.
Q12:事务的隔离性
- 在进行配置的时候,如果数据库和spring代码中的隔离级别不同,那么以spring的配置为主。
- 隔离级别由上到下以此增强,对数据的严谨性越来越强,数据越安全。
- 脏读:脏读又称无效数据的读出,是指在数据库访问中,事务T1将某一值修改,然后事务T2读取该值,此后T1因为某种原因撤销对该值的修改,这就导致了T2所读取到的数据是无效的,值得注意的是,脏读一般是针对于update操作的。
- 不可重复读:指在一个事务A内,多次读同一个数据,但是事务A没有结束时,另外一个事务B也访问该同一数据。那么在事务A的两次读数据之间,由于事务B的修改导致事务A两次读到的数据可能是不一样的。这就发生了在一个事务内两次读到的数据不一样,这就被称作不可重复读。`不可重复读,是指在数据库访问中,一个事务范围内两个相同的查询却返回了不同数据。
- 幻读(Phantom Read):是指当事务不是独立执行时发生的一种现象。幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还存在没有修改的数据行,就好象发生了幻觉一样.一般解决幻读的方法是增加范围锁RangeS,锁定检索范围为只读,这样就避免了幻读。
SpringMVC
Q1:解决POST请求乱码问题
- 在web.xml文件中配置CharacterEncodingFilter过滤器
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<!-- 配置解决请求乱码问题 -->
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<!-- 配置解决响应乱码问题 -->
<init-param>
<param-name>forceResponseEncoding</param-name>
<param-value>TRUE</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
Q2:解决GET请求乱码问题
- 修改Tomcat中Server.xml配置文件,配置文件中第一个Connector后添加URIEncoding=“UTF-8”
<Connector URIEcoding="UTF-8" port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
Q3:SpringMVC的工作流程
- 首先中央控制器DispatcherServlet接收到用户发送的请求,然后调用处理器映射器HandlerMapping,然后处理器映射器处理完成后,会调用处理器适配器HandlerAdapter,通过处理器适配器找到具体具体的处理器Handler(Controller),处理器处理完成后会生成ModelAndView交由中央控制器DispatcherServlet处理,中央控制器DispatcherServlet处理接收到ModelAndView之后,会调用视图解析器ViewResolver,视图解析器处理完成后会返回view视图对象给中央控制器DispatcherServlet,然后中央控制器DispatcherServlet将这个视图进行渲染,然后响应给用户。
Mybatis
Q1:MyBatis中实体类中的属性名和数据库表中字段名不一致解决方法
- 在Mapper.xml中编写sql语句时可以将要查找的字段名起别名,使别名与实体类中的属性名保持一致;
- 在MyBatis全局配置文件中开启驼峰命名规则,可以使数据库表中的下滑线去掉,并且将下划线后的字母大写,例如:last_name(数据库表中字段)------>lastName(实体类中的属性名),注意只能是这种格式可以使用。
<settings>
<!-- 开启驼峰命名 -->
<setting name="mapUnderscoreToCamelCase" value="true" />
</settings>
- 在Mapper映射文件中使用resultMap来定义映射规则
<resultMap type="indi.dsl.entry.Student" id="myMap">
<!--映射主键-->
<id column="stu_no" property="studentNo">
<!--映射其他类-->
<result column="stu_age" property="studentAge">
<result column="stu_name" property="studentName">
<result column="email" property="email">
</resultMap>
<select id="studentId" resultMap="myMap">
select * from student where stu_no = #{studentNo}
</select>
SpringBoot
Q1:Spring Boot自动装配原理是什么,解决了什么问题?
- 原理总结:springboot根据配置文件自动装配所属依赖的类,再用动态代理的方式注入到spring容器里面,做到即用即取。
- 解决问题或者作用总结:Spring Boot主要作用就是简化Spring应用的开发,开发者只需要通过少量代码就可以创建一个Spring应用,而达到这一目的最核心的思想就是约定优于配置。
Q2:Spring Boot自动装配原理实现
- 关键词:
1.BFPP:-----BeanFactoryPostProcessor-----Bean工厂后置增强器
作用:他会在扫描完项目将Class转换为BeanDefinition 之后在进行实例化之前进行接口的回调!
2.BPP:-----BeanPostProcessor-----Bean对象后置增强器
作用:他有两个方法,两个方法的调用时机也不相同,他会在实例化之后,调用初始化方法之前进行第一次方法回调(postProcessBeforeInitialization),在执行完初始化方法之后又会进行一次回调(postProcessAfterInitialization),每次回调该类都会将当前创建好的bean传递到方法内部,从而让开发者能够自定义的修改当前bean的一些定义!
3.BDRPP:----BeanDefinitionRegistryPostProcessor----Bean对象属性注册后置增强器
作用:动态注册Bean到Spring容器
自动装配原理实现细节:
- 当启动 springboot 应用程序的时候,会先创建 SpringApplication 的对象,在对象的构造方法中会进行某些参数的初始化工作,最主要的是判断当前应用程序的类型以及加载所有初始化器和监听器,在这个过程中会加载整个应用程序中的 spring . factories 文件(存在spring-boot和autoconfigure包中),将文件的内容放到缓存对象中,方便后续获取。
- SpringApplication 对象创建完成之后,开始执行 run 方法,来启动整个应用程序,启动过程中最主要的有两个方法,第一个叫做 prepareContext ,第二个叫做 refreshContext ,在这两个方法中完成自动装配的核心功能。prepareContext的处理逻辑包含了上下文对象的创建, banner 的打印,异常报告等等一些准备工作,方便后续来进行调用。
- 在 prepareContext 方法中主要完成的是对上下文对象的初始化操作,包括了属性值的设置,比如环境对象, 在整个过程中有一个非常重要的方法,叫做 load , load 主要进行的操作,就是将当前启动类做为一个beanDefinition 注册到 registry 中,方便后续在进行 BeanFactoryPostProcessor 调用执行的时候,找到对应的主类,并且完成@ SpringBootApplicaiton ,@ EnableAutoConfiguration 等注解的解析工作。
- 在 refreshContext 方法中会进行整个容器刷新,会调用spring 中的 refresh 方法,refresh 中有13个非常关键的方法,来完成整个 spring 应用程序的启动, 在自动装配过程中,会调用invokeBeanFactoryPostProcessor 方法,在此方法中主要是对 ConfigurationClassPostProcessor 类的处理,然后调用postProcessBeanDefinitionRegistry方法,将@ PropertySource ,@ componentScan ,@ ComponentScans ,@ Bean ,@ Import 等注解进行解析处理,其中最主要的是对@ Import 注解的解析。 这个是 BFPP 的子类也是 BDRPP 的子类,在调用的时候会先调用 BDRPP 中的 postProcessBeanDefinitionRegistry 方法,然后调用 postProcessBeanFactory 方法,在执行 postProcesseanDefinitionRegistry 的时候会解析处理各种注解,包含@ PropertySource ,@ componentScan ,@ ComponentScans ,@ Bean ,@ Import 等注解,最主要的是@ Import 注解的解析。
- 在解析@ Import 注解的时候,会有使用 getlmports 方法,从主类开始递归解析注解,把整个程序中所有的@ Import注解都解析到,然后在 processImport 方法中对 Import 的类进行分类,此处主要是识别 AutoConfigurationlmportSelect 归属于 ImportSelect 的子类,在后续过程中会最后再调用
deferredlmportSelectorHandler 中的 process 方法,来完整 EnableAutoConfiguration 的加载。
Q2:Spring Boot启动过程流程
- 1.当启动 springboot 应用程序的时候,会先创建 SpringApplication 的对象,在对象的构造方法中会进行某些参数的初始化工作,最主要的是判断当前应用程序的类型以及加载所有初始化器和监听器,在这个过程中会加载整个应用程序中的 spring . factories 文件(存在spring-boot和autoconfigure包中),将文件的内容放到缓存对象中,方便后续获取。
- 2.SpringApplication 对象创建完成之后,开始执行 run 方法,来启动整个应用程序。
- 3.通过stopwatch对象开启计时器,设置当前的任务ID和启动时间,方便后续计时操作。
- 4.注册所有监听器,得到一个EventPublishingRunListenters对象,此对象会贯穿整个应用程序的启动过程,每次在进行监听的操作的时候,都会从中获取具体的监听器。
- 5.然后通过prepareEnvironment方法准备当前应用程序环境。
- 6.然后通过printBanner方法,将banner进行打印,即控制台中输出的spring标志。
- 7.然后通过createApplicationContext方法,准备上下文应用对象,根据当前的应用类型来判断使用那种格式的应用上下文,获取完上下文应用对象之后,通过refreshContext方法刷新上下文环境。
- 8.然后通过stopwatch对象中的stop方法,进行计时结束操作,并打印启动程序的运行时长。
- 9.最后通过listeners.run()方法,运行所有的监听器对象,完成最后的启动执行。
Mysql和Oracle
Q1:Mysql什么时候键索引?
- 适合创建索引的情况:
1.主键自动建立唯一索引
2.频繁作为查询条件的字段应该创建索引
3.查询中与其他表关联查询的字段,存在外键关系的需要建立索引
4.单键/组合索引的选择问题,组合索引性价比更高
5.查询中排序的字段,排序的字段若通过索引去访问将大大提高排序速度
6.查询中统计或者分组字段
- 哪些情况不要创建索引
1.表中记录太少
2.经常增删改的表或者字段
3.where条件里用不到的字段不要创建索引
4.过滤性不好的字段不要创建索引,例如性别字段,一般过滤出来的只有三种情况:男、女、位置性别,过滤性不好,所以不建立索引
Q2:mybatis #和$的区别
- #{}
在填充属性时,是通过?占位符的方式,能够避免SQL注入
只能获取跟数据库表相关的值
- ${}
在填充属性时,使用的是直接进行字符串拼接的方式,会有SQL注入的风险
使用其可以操作非数据库表列的取值
Q3:oracle和mysql区别
区别一:分页方式不同
1.mysql使用limit关键字实现分页
2.oracle使用rownum(伪列)关键字实现分页
区别二:求集合方式不同
1.mysql只有并集
2.oracle数据库有并集、交集(intersect)、差集(minus)
- 交集举例:
select * from t1 where t1.name in (‘张三’,‘李四’)
intersect
select * from t1 where t1.name in (‘张三’,‘王五’)
执行结果:张三
- 差集(minus关键字上面集合有的元素,下面集合没有的元素)举例:
select * from t1 where t1.name in (‘张三’,‘李四’)
minus
select * from t1 where t1.name in (‘张三’,‘王五’)
执行结果:李四
- 并集举例:
union(去除重复元素)、union all(不去除重复元素)
select * from t1 where t1.name in (‘张三’,‘李四’)
union
select * from t1 where t1.name in (‘张三’,‘王五’)
执行结果:张三、李四、王五
select * from t1 where t1.name in (‘张三’,‘李四’)
union all
select * from t1 where t1.name in (‘张三’,‘王五’)
执行结果:张三、李四、张三、王五
区别三:集合连接方式不同
mysql存在左连接、右连接、内连接
oracle存在左连接、右连接、内连接、全连接
- 举例建表
- 左连接
- 右连接
- 全连接(Oracle特有)
- 内连接
设计模式
Q1:手写单例模式
- 单例模式:某一个类在整个系统中只能有一个实例对象可被获取和使用的代码模式。
- 单例饿汉模式:直接创建对象,无论是否需要,不存在线程安全问题
直接实例化饿汉式(简洁直观)
枚举类(最简单)
静态代码块饿汉式(适合复杂实例化) - 饿汉模式步骤:
构造器私有化
自行创建,并用静态变量保存
向外提供这个实例
强调这是一个单例,我们可以用final修改 - 三种饿汉模式代码展示
//直接实例化饿汉式(简洁直观)
public class Singleton1 {
public static final Singleton1 INSTANCE = new Singleton1();
private Singleton1(){
}
}
--------------------------------------------------------------------
//枚举类(最简单)
public enum Singleton2 {
INSTANCE;
}
---------------------------------------------------------------------
//静态代码块饿汉式(适合复杂实例化)
package indi.dsl.singleton;
import java.io.IOException;
import java.util.Properties;
public class Singleton3 {
public static Singleton3 INSTANCE;
private String info;
static {
Properties ps = new Properties();
try {
//获取配置文件 ps.load(Singleton3.class.getClassLoader().getResourceAsStream("singleton.properties"));
INSTANCE = new Singleton3(ps.getProperty("info"));
} catch (IOException e) {
e.printStackTrace();
}
}
private Singleton3(String info) {
this.info = info;
}
}
- 单例懒汉式:延迟创建对象
线程不安全(适合单线程)
线程安全(适合于多线程)
静态内部类形式(适合于多线程) - 懒汉模式步骤:
构造器私有化
用一个静态变量保存这个唯一实例
提供一个静态方法,获取这个实例对象,并且判断是否需要实例化
//线程不安全(适合单线程)
package indi.dsl.singleton;
public class Singleton4 {
private static Singleton4 instance;
private Singleton4(){
}
public static Singleton4 getInstance(){
if (instance==null){
instance = new Singleton4();
}
return instance;
}
}
------------------------------------------------------------------------
//线程安全(适合于多线程)
package indi.dsl.singleton;
public class Singleton4 {
private static Singleton4 instance;
private Singleton4(){
}
public static Singleton4 getInstance(){
synchronized (Singleton4.class){
if (instance==null){
instance = new Singleton4();
}
}
return instance;
}
}
-----------------------------------------------------------------------------
//静态内部类形式(适合于多线程)
package indi.dsl.singleton;
public class Singleton5 {
private Singleton5(){
}
private static class inner{
private static final Singleton5 instance = new Singleton5();
}
public static Singleton5 getInstance(){
return inner.instance;
}
}
HTTP状态码
- 参考
链接: 点击跳转.
排序算法
- 冒泡排序
package indi.dsl.sorttest;
public class SortTest {
public static void sortMassage(int[] a) {
int[] b = a;
//比较几轮(a.length-1轮)
for (int i = 0; i < b.length - 1; i++) {
//每轮比较,找出最大数向后放
for (int j = 0; j < b.length - 1 - i; j++) {
int temp = 0;
if (b[j] > b[j + 1]) {
temp = b[j];
b[j] = b[j + 1];
b[j + 1] = temp;
}
}
}
for (int args : b){
System.out.print(args);
}
}
public static void main(String[] args) {
int[] a = {10, 4, 5, 1, 8, 6, 3, 7};
SortTest.sortMassage(a);
}
}
- 快速排序
package indi.dsl.sorttest;
public class QuickTest {
public static void quick(int[] a,int left,int right){
if (left > right) {
return;
}
//定义基准值
int base = a[left];
//定义左索引
int l = left;
//定义右索引
int r = right;
//进行第一轮排序
while(l!=r){
//找到比基准数小的停止,大的数进行索引下标-1,向左移动
while(a[r]>=base && l<r){
r--;
}
//找到比基准数大的的停止,小的数进行索引下标+1,向右移动
while(a[l]<=base && l<r){
l++;
}
//右面找到比base小的,左面找到比base大的,双方进行交换
int temp = 0;
temp = a[l];
a[l] = a[r];
a[r] = temp;
}
//左右相遇,需要把相遇位置的值给基准数,基准数的值给相遇位置的值
a[left] = a[l];
a[l] = base;
//递归调用,将第一次排好顺序的基准数左面的元素排序
quick(a,left,l-1);
//递归调用,将第一次排好顺序的基准数右面的元素排序
quick(a,r+1,right);
}
public static void main(String[] args) {
int [] a = {7,5,1,8,6,9,4,3,0};
QuickTest.quick(a,0,a.length-1);
for (int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
}
}
中间件
消息中间件ActiveMQ
Q1:ActiveMQ宕机了怎么处理?
- ActiveMQ主从集群方案:ZooKeeper集群+持久化数据(一般采用RePlicate LevelDB)+ActiveMQ集群;
- 一般真正生产中会采用 ActiveMQ主从集群方案,在生产环境中存在3个以上的ActiveMQ服务器,并且存在主从关系,当正常情况下是ActiveMQ的主服务器工作,其他ActiveMQ服务器做备份,如果其中一台ActiveMQ服务器宕机,可以选择其他正常的服务器正常工作。
Q2:如何防止消费方消息的重复消费?
- 如果因为网络延迟等原因,MQ无法及时接收到消费方的应答,导致MQ重试。在重试的过程中造成重复消费的问题。
- 解决方案:
1.如果消费方是做数据库操作,那么可以把消息的ID作为表的唯一主键,这样在重试的情况下,会触发主键冲突,从而避免数据出现脏数据。
2.如果消费方不是做数据库操作,那么可以借助第三方的应用,例如Redis,来记录消费记录,每次消息被消费完成的时候,把当前的消息的ID作为key存入Redis中,每次消费前,先到Redis中查询有没有该消息的消费记录,如果存在不做消费处理。
Q3:如何防止消息丢失
- 使用以下手段防止消息丢失:
- 在消息生产者和消费者使用事务
- 在消费方采用手动消息确认(acknowledge)
- 消息持久化,例如JDBC或日志
Q4:死信队列相关问题
- 本质:死信队列是ActiveMQ产品,保证处理失败的消息或者过期的消息,不会丢失的一种处理机制。
- 概念:DLQ-Dead Letter Queue,死信队列,用来保存处理失败或者过期的消息。
- 什么情况下消息会进入死信队列:
当一个消息被重发超过6次(默认为6次,可以调整次数),borker会将这个消息发送到死信队列中,以便后续处理。
- 什么情况下消息会被重新发送:
1.开启事务时,手动调用rollback()方法时,消息会被重新发送;
2.开启事务时,消息一直没有被提交时,消息会被重新发送;
3.消费端是手动确认消息模式时,如果调用session.recover()方法时,消息会被重新发送。
- 特别注意:
1.持久化的消息过期后,会被送到死信队列中;
2.默认的死信队列为ActiveMQ.DLQ,可以为每个队列设置唯一的死信队列与之对应。