面试题
Spring、SpringMVC、SpringBoot的区别
Spring是轻量级的开发框架,主要提供了IOC依赖注入容器和AOP面向切面编程的功能。
SpringMVC是基于Spring的一个用来解决Web开发的问题,主要处理web开发中路径映射和视图渲染等
SpringBoot是融合了Spring和SpringMVC等框架以及自带有Web服务器,采用约定大于配置来简化各种配置文件的配置
Spring中AOP是怎么实现的
Spring中的AOP是通过动态代理实现的,代理方式主要有两种:JDK动态代理、CGLIB动态代理
JDK动态代理是基于接口生成实现类并通过反射进行调用
CGLIB动态代理是基于继承实现的
依赖注入是什么
依赖注入是控制反转的一种实现方式,容器通过配置自动创建并注入对象来实现控制反转。
其优点是降低对象间的耦合度
Spring依赖注入主要有以下几种方式
- set注入
- 构造器注入
- 自动注入
Spring框架中使用到的设计模式
- 工厂模式:BeanFactory
- 代理模式:Spring的AOP就是基于动态代理实现的
- 单例模式:单例Bean的创建
- 适配器模式:HandlerAdpter
- 模板模式:提供了很多类似于jdbctemplate类
Spring启动流程
- ResourceLoader加载XML配置信息后,由BeanDefinitionReader读取配置信息文件,把每个解析成BeanDefinition对象保存在BeanDefinitionRegistry注册表中。这时的BeanDefinition可能只是个半成品,因为某些XML属性配置里会有占位符变量,这些变量此时不会被解析出来,需要继续优化
- 容器首先扫描注册表取出工厂后处理器,对注册表中的BeanDefinition进行加工处理,把占位符替换成真正的值,产生成品的BeanDefinition。
- 通过反射机制扫描BeanDefinitionRegistry所有属性编辑器的bean类,并把这些bean放到spring容器的属性编辑器注册表(PropertyEditorRegistry)中
- Spring容器从BeanDefinitionRegistry中取出加工后的BeanDefinition,并调用InstantiationStrategy着手对bean的实例化工作。(这里的实例化只是相当于new了一个新对象一样,也就是说,只是跑一个构造函数,不会具体的为属性设置值,当然如果构造函数里写了设置值的语句,那么也可以赋值。)
- 实例化的过程中,spring容器使用BeanWrapper对bean进行封装,BeanWrapper结合BeanDefinition和属性编辑器注册表中的属性编辑器完成bean的属性设置工作
- Bean后置处理器
Bean的生命周期
主要分为四个阶段:实例化、设置属性、初始化、销毁
- 利用反射创建一个Bean的实例
- 设置对象属性
- 检查是否实现Aware相关接口,如果实现了就调用对应的方法
- 执行BeanPostProcessor的前置处理
- 检查是否实现InitializingBean接口,如果实现则执行
afeterPropertiesSet()
方法。 - 检查是否配置又init-method方法,如果有则调用
- 执行BeanPostProcess的后置postProcessAfterInitialization()方法。
- 当要销毁Bean的时候,如果Bean实现了DisposableBean接口,
执行destroy()方法
。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VElwdRbW-1677676338030)(null)]
Spring中的IOC及应用场景
BeanFactory和ApplicationContext的区别
BeanFactory是Spring中最底层的容器接口,ApplicationContext继承了BeanFactory是更高级的容器接口。这两个接口都主要提供获取Bean的功能
- BeanFactory容器在启动时不会去初始化Bean,只有等真正调用getBean方法时才会初始化。ApplicationContext会在启动时将所有单例对象实例化
- 由于ApplicationFactory是BeanFactory的子接口,所以包含其所有的功能,并且功能更强大,一般情况下使用ApplicationContextFactory
SpringBoot自动装配的原理
- SpringBoot启动时会去扫描当前启动类的类路径,然后扫描这个路径下所有需要注入的信息
- 并且会扫描项目内所有包的META-INF下的SpringFactory文件,将路径下的Configuration加载进去并且根据条件注入对应的Bean
DispatcherServlet工作流程
- DispatcherServlet调用HandlerMapping根据URL来找到对应的Handler执行器链
- DispatcherServlet调用HandlerAdpter来执行请求
- 请求执行结束后返回一个ModelAndView
- DispatcherServlet调用视图解析器将ModelAndView解析成视图返回给用户
事务失效原因
-
对象没有被spring管理
-
方法不是public
-
同类的方法调用(此时没有经过代理)
解决办法
使用aspectj进行aop
-
数据库不支持事务
-
异常被捕获
-
异常类型和配置异常不同
-
有某个bean先于事务bean注入并且该bean引用这个bean导致事务bean提前注入没有被aop代理
Spring Bean的作用域
- 单例
- 原型
- request
- session
BeanFactory和FactoryBean
- BeanFactory是所有Spring容器的核心接口,给容器实现提供规范。用于获取所有的bean
- FactoryBean是针对实际的某个Bean的接口,Bean通过实现FactoryBean接口来配置Bean实例化时的操作
@Autowired和@Resources有什么区别
Spring
Spring框架本身是一个轻量级的IOC的容器框架。Spring的作用主要是帮我们创建了对象和管理对象的生命周期。我们在代码中可以不用再考虑创建对象时需要哪些参数,只需要从Spring的容器中取出来用即可。Spring是使用依赖注入的方式实现控制反转的,我们只需要通过简单的XML配置或者注解配置。Spring启动时就会将对象和它所依赖的对象创建出来。并且Spring还提供了AOP,单元测试、数据库、Web模块等框架,可以让我们轻松的将这些框架集合在一起使用。
IOC
IOC:控制反转,主要是指将对象创建的权力交给框架,并且由框架来管理。对象间复杂的依赖关系交给IOC容器来管理,对象的创建和属性的注入都由Spring来提供,我们只需要通过XML/注解来配置一下即可。
Spring容器中的对象与普通Java对象创建的区别
普通的Java对象,创建时虚拟机将对应的class文件加载进来,再将对象初始化。Class做为对象的模板进行创建的,并且创建对象是由我们来决定的,需要在代码中显式的创建对象才能使用。而Spring容器中的Bean创建是由Spring IOC容器来实现的,Bean对象的生命周期、依赖关系也都是容器来控制。我们只需要在XML文件、注解、JavaConfig中对Bean进行描述。Spring启动后会读取这些信息生成对应的BeanDefinition
。
Bean
Bean的作用域
主要分四种
- 单例:全局只保留唯一的一个对象,Spring中默认采用的单例
- 原型:每次获取该对象时都会创建一个全新的对象
- Request:bean的存活期限时一次请求
- Session:bean的存活时间是一次会话
Bean的生命周期
- 利用反射创建一个Bean的实例
- 设置对象属性
- 检查是否实现Aware相关接口,如果实现了就调用对应的方法
- 执行BeanPostProcessor的前置处理
- 检查是否实现InitializingBean接口,如果实现则执行
afeterPropertiesSet()
方法。 - 检查是否配置又init-method方法,如果有则调用
- 执行BeanPostProcess的后置postProcessAfterInitialization()方法。
- 当要销毁Bean的时候,如果Bean实现了DisposableBean接口,
执行destroy()方法
。
Bean是线程安全的吗
Bean是否是线程安全是根据Bean的作用域来区分的
- Bean如果是单例的,那就存在线程安全问题,但如果是像Controller这种只关注方法调用的无状态Bean(无状态Bean:Bean中不保存数据)一般也是不存在线程安全问题的
- Bean如果是原型的,也不存在线程安全问题,因为每次获取都是一个全新的Bean。
解决方法:使用同步机制(时间换空间),或者将单例改为原型(空间换时间)
依赖自动注入六层筛选
循环依赖-三层缓存
构造器注入中出现循环依赖时,Spring在启动时就会抛出异常。解决循环依赖的办法是使用延迟加载,
Set方法的循环依赖解决办法
主要分三级缓存
一级缓存:放置实例化好的单例对象
二级缓存:存放提前曝光的单例对象(没有进行属性注入)
三级缓存:存放实例化对象的对象工厂
Tips:其实二级缓存就能解决循环依赖的问题,之所以采用三级循环是因为AOP
流程
- 获取A时,先在一级缓存中获取
- 一级缓存中没有,则进入二级缓存中查找
- 二级缓存中也没有则会进入三级缓存中查找
- 三级缓存中也没有则会将对象A进行实例化后放入三级缓存
- 再对A进行注入,注入时发现A依赖于B,则去实例化B(放入三级缓存),因为B又依赖于A则将三级缓存中的A放入B中(因为三级缓存存放的是A的工厂对象,B是可以通过工厂对象获取到真正的代理对象的)
- 将三级缓存中的A转如二级缓存中
- 对B进行实例化后放入一级缓存
- A再获取到B进行注入
AOP
AOP:面向切面编程,主要用于具有横切性质的系统级服务
如日志收集、事务管理、安全检查等,比如我们代码中经常会记录操作日志或者权限校验,不使用AOP时我们需要在每个地方加上对应的代码。非常冗余也使代码不宜维护。AOP的作用就是将这些公共部分的代码抽离出来写成一个公共的方法。这样就减少了代码的冗余并且利于维护。
优点:降低代码耦合、提高代码复用性、易于扩展
AOP的实现
Spring AOP是怎么实现的
Spring中使用动态代理来实现AOP,动态代理的实现方式有两种,一种是JDK动态代理,一种是CGLIB动态代理。
JDK动态代理
JDK动态代理是基于接口实现的,它是利用反射生成接口的一个匿名实现类,生成的这个类在调用方法前会先调用InvokeHandler来处理。JDK代理的限制是必须要求代理类实现了某个接口才能被代理
优点
- 生产代理类的速度更快
- JDK原生不需要依赖
缺点
- 只能代理接口实现类
- JDK动态代理是通过反射调用方法,速度更慢
CGLIB动态代理
CGLIB使用了字节码生成技术,在运行时通过继承被代理的类来实现代理。CGLIB不需要考虑代理对象是否实现接口,不过需要考虑代理对象是否被final修饰(final修饰的类不能被继承自然不能被CGLIB代理)
Spring对接口的实现类都采用JDK动态代理,对于没有实现接口的类采用CGLIB实现
优点
- CGLIB代理的类不需要实现接口
- 因为CGLIB生成的类是通过继承实现的,所以调用方法时速度更快
缺点
- 不能代理final和private方法
- 不能代理final类
- 生产代理类的速度慢
AspectJ和SpringAOP的区别
SpringAOP采用的是JDK动态代理和CGLIB动态代理,是一种运行时增强,是在运行时对动态的生成类来实现的。并且SpringAOP只能对Spring容器中的Bean进行代理,不能对普通对象使用。
AspectJ是采用的静态代理,在编译时期对代码进行织入。因为AspectJ是在编译时期对代码进行增强,所以它在运行时是没有其他开销的,性能要比SpringAOP的性能更高,并且AspectJ还提供了很多织入方式和功能。AspectJ可以对任何对象进行代理
Advice通知的类型
- 前置通知:方法执行前调用通知
- 后置通知:方法执行后调用通知
- 返回通知:方法正常返回后调用通知
- 异常通知:方法返回异常后调用通知
- 环绕通知:方法执行前后都可以自定义行为
Around Around
Before Before
执行 执行
Around 抛出异常
After After
AfterReturning AfterThrowing
多个切面织入时执行顺序
多个切面织入时顺序是不固定的,可以通过指定order级别来实现固定顺序
Spring中的设计模式
工厂模式
Spring中的BeanFactory就是简单工厂的实现,根据传入的唯一标识来回去bean
单例模式
提供了全局的访问点BeanFactory
代理模式
Spring的AOP就是使用代理模式来增强
装饰器模式
依赖注入就需要使用BeanWrapper。
观察者模式
Spring中各种Listener
策略模式
AOP动态代理时采用何种方式来增强(CGLIB、JDK动态代理)
常用注解
@Component
将一个类注册为Bean,spring启动时会将加了这个注解的类注册为一个Bean放入容器中
@Controller
标识这个类是一个MVC的控制器,作用和Component相同
@Service
标识这个类是一个服务层的Service,作用和Component相同
@Resource
将依赖的对象自动注入进来,默认是按照Bean的名字来注入,名字不匹配时才会按照类型来注入
@Autowird
将依赖的对象自动注入进来,默认是根据Bean的类型进行自动装配Bean,并且要求这个对象必须存在,如果不存在会抛出异常
@Qualify
指定装入Bean的名字,与Autowird一起使用(@Autowird默认使用的按类型注入)
@Transactional实现原理
在类或者方法上使用,可以开启事务。开启事务的方法执行时抛出异常后,事务会执行回滚。
实现原理
Spring启动时扫描Bean,遇到@Transactional的注解时就会使用AOP对这个类进行动态代理(相当于以@Transactional注解为切点切入事务,类似于环绕型通知)。代理对象在执行真正方法时抛出异常,代理对象就会进行回滚,反之将事务提交
事务
事务的隔离级别(5种)和传播特性(7种)
5种事务隔离级别
Spring事务传播属性和隔离级别 - Eunice_Sun - 博客园 (cnblogs.com)
Spring中的事务隔离级别和数据库的隔离级别相同,只多一个默认隔离级别(使用数据库默认隔离级别)
如果数据库中设置的事务隔离级别和Spring设置的不同,以Spring为主
7种传播特性
REQUIRED
如果当前存在事务则沿用事务,否者创建新事务
REQUIRED-NEW
不论当前是否有事务都会创建新的事务,将原来的事务挂起
SUPPORT
如果当前有事务则沿用事务,如果没有则以无事务方式运行
NOT-SUPPORT
以非事务方式运行,如果当前存在事务则挂起事务
MANDATORY
使用当前事务,如果没有事务则抛出异常
NEVER
以非事务方式运行,如果当前存在事务则抛出异常
NESTED
嵌套事务(类似于REQUIRED),实现部分提交。子事务catch异常可以实现父事务不回滚。在Required中子事务发生异常不论是否catch都会导致整个事务回滚。
事务实现的原理
Spring中提供两种事务方式:编程式事务和声明式事务
编程式事务
编程式事务需要在代码中手动进行处理,一般不会使用这种方法
声明式事务
声明式事务主要是使用@Transactional注解
底层就是通过AOP来代理这个对象,如果调用方法后抛出异常则会进行回滚,反之则提交事务
SpringMVC
SpringMVC执行流程
- 请求URL由DispatcherServlet来进行分发
- 调用HandlerMapping处理器根据请求的URL找到对应的处理器
- 由HandlerAdapter(处理器适配)调用真正的Handler来进行处理
- Handler处理后返回一个ModelAndView对象(数据视图对象)
- 视图解析器对视图进行解析找到要返回的视图
- DispatcherServlet将Model渲染到视图上返回给浏览器
SpringBoot
SpringBoot相比SSM框架的优点
- SpringBoot采用的约定大于配置,它内置了很多基本配置,大大的减少了xml文件的配置。
- SpringBoot中内置服务器,项目可以独立运行,可以更快速的启动一个Web项目
- 引入第三方框架时只需要配置很少的信息
SpringBoot的常用注解
@SpringBootApplication
springboot项目的启动类,标识这是一个springboot项目,用来开启springboot的各项能力。
@springbootappliation其实是三个注解的组合(@Configuration,@EnableAutoConfiguration,@ComponentScan)
@Configuration
表明这是一个配置类,类中配置的bean都会被加载到spring容器中
@EnableAutoConfiguration
允许springboot开启自动注解配置,开启后springboot会扫描启动类下的包和类来配置spring bean。这个注解也是SpringBoot自动装配的关键。
自动装配的原理
Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到所有META-INF/spring.factories配置文件中的所有自动配置类,并对其进行加载,而这些自动配置类都是以AutoConfiguration结尾来命名的,它能从Properties结尾命名的类中取得在全局配置文件中配置的属性如:server.port,properties配置类会从application中读取它对应前缀的属性,如果没有配置一般也会有默认值。
@RestController
@ResponseBody+@Controller的结合体,标识这个Controller返回的信息直接写入response body,以json格式返回
@Bean
标注这个方法的返回值注册为一个Bean
手写SpringBootStarter
-
创建一个starter项目
-
创建一个ConfigurationProperties用来保存配置信息(一般要设置默认值)
@ConfigurationProperties(prefix = "http") // 自动获取配置文件中前缀为http的属性,把值传入对象参数 @Setter @Getter public class HttpProperties { // 如果配置文件中配置了http.url属性,则该默认属性会被覆盖 private String url = "http://www.baidu.com/"; }
-
创建一个AutoConfiguration,在 AutoConfiguration 中实现所有 starter 应该完成的操作,并且把这个类加入 spring.factories 配置文件中进行声明
- 创建一个业务类(Bean)
@Setter @Getter public class HttpClient { private String url; // 根据url获取网页数据 public String getHtml() { try { URL url = new URL(this.url); URLConnection urlConnection = url.openConnection(); BufferedReader br = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), "utf-8")); String line = null; StringBuilder sb = new StringBuilder(); while ((line = br.readLine()) != null) { sb.append(line).append("\n"); } return sb.toString(); } catch (Exception e) { e.printStackTrace(); } return "error"; } }
- 创建 AutoConfiguration
@Configuration @EnableConfigurationProperties(HttpProperties.class) public class HttpAutoConfiguration { @Resource private HttpProperties properties; // 使用配置 // 在Spring上下文中创建一个对象 @Bean @ConditionalOnMissingBean // 指这个bean在哪些情况下不加载 public HttpClient init() { HttpClient client = new HttpClient(); String url = properties.getUrl(); client.setUrl(url); return client; } }
-
在
resources
文件夹下新建目录META-INF
,在目录中新建spring.factories
文件,并且在spring.factories
中配置 AutoConfiguration:org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.nosuchfield.httpstarter.HttpAutoConfiguration
-
打包项目即可
CORS跨域问题
跨域问题
SpringBoot配置Cors解决跨域请求问题 - 袁老板 - 博客园 (cnblogs.com)
同源策略:同源策略是浏览器的一个安全功能,非同源的请求在没有明确授权下不能读取对方资源。同源是指请求的协议、ip地址、端口号完全相同。同源策略下,非同源的请求是不能发送的
CORS跨域就是为了解决同源策略不能访问的问题,在不破坏原有的同源策略下实现跨源通信。CORS将请求分为两类简单请求和非简单请求。
- 简单请求:服务器在接收到请求后判断该地址是否跨域访问,如果可以访问则在响应中设置Access-Control-Allow-Origin字段,不设置则表示不支持该IP地址访问
- 非简单请求:浏览器在发送请求时会发送一个OPTIONS的预检请求来检测服务器是否可访问。服务端在收到请求后设置对应的字段返回来表示是否支持访问。
SpringBoot设置CORS
1、@CrossOrigin
在Controller的方法和类上使用,表明该接口或一系列接口允许跨域访问
2、全局配置
添加全局配置
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
.maxAge(3600)
.allowCredentials(true);
}
}
3、使用Filter
@Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true); config.addAllowedOrigin("http://localhost:9000");
config.addAllowedOrigin("null");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config); // CORS 配置对所有接口都有效
FilterRegistrationBean bean = newFilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
主要作用就是判断请求是否为跨域请求,是否需要设置必要的跨域信息。