Bean创建步骤(线程不安全),boot装配机制,类加载机制和双亲委派模型,MVC步骤,servlet生命周期

目录

前言

1. Bean创建步骤(线程不安全)

1.1Bean创建步骤

1.2Spring的Bean不是线程安全的

2. boot装配机制

3. 类加载机制和双亲委派模型

3.1类加载机制

3.2双亲委派模型

4. MVC步骤

4.1什么是Spring MVC?MVC模式的优点?

4.2DispatcherServlet/Controlle类与Servlet的区别和联系

4.3请描述Spring MVC / DispatcherServlet的工作流程?

5.Servlet生命周期

3.1加载和实例化

3.2初始化

3.3服务

3.4销毁


前言

​​​​@Test
void timedTaskMethod() {
    String a = "hqw";
    String b = "hqw";
    String c = new String("hqw");
    System.out.println(a==b);//true
    System.out.println(a==c);//false
//
字符串常量池在方法区中,
//静态变量也在方法区中,静态对象还放在堆,
//堆和方法区是多线程共享的
}

1. Bean创建步骤(线程不安全)

1.1Bean创建步骤

@Bean 需要在配置类中使用,即类上需要加上@Configuration注解

那为什么有了@Component,还需要@Bean呢?
如果你想要将第三方库中的组件装配到你的应用中,在这种情况下,是没有办法在它的类上添加@Component注解的,因此就不能使用自动化装配的方案了,但是我们可以使用@Bean,当然也可以使用XML配置

Spring Bean的生命周期分为四个阶段:实例化 Instantiation --> 属性赋值 Populate --> 初始化 Initialization --> 销毁 Destruction

D:\12PROGRAMMER34\2kc-projectSets\kaytune32-selfStudyOfProfessionalCourse\003-Bean\01-\theExampleProject

【步骤1】程序运行时, 通过加载spring配置文件,实例化bean对象

ClassPathXmlApplicationContext contetx = new ClassPathXmlApplicationContext("spring.xml");

【步骤2】实现BeanNameAware接口,执行setBeanName方法,beanName值:

对应spring.xml配置文件配置的<bean id="userService123">

@Override
public void setBeanName(String name) {

【步骤3】实现BeanFactoryAware接口,执行setBeanFactory()方法,该方法作用是让bean类知道自己所在的BeanFactory的属性。

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {

【步骤4】实现ApplicationContextAware接口,执行setApplicationContext()方法,将当前Bean引入到应用上下文中,将来可以通过ApplicationContext获取到当前Bean(在启动Spring Boot项目时,需要调用SpringApplication.run()方法,而run()方法的返回值就是ApplicationContext)

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    String deanDefinitionNames = Arrays.toString(applicationContext.getBeanDefinitionNames());

【步骤5】实现BeanPostProcessor接口,执行postProcessBeforeInitialization(Object bean, String beanName)前置初始化方法

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

【步骤6】如果通过@PostConstruct注解自定义了初始化方法,则会在前置初始化方法后执行后执行@PostConstruct注解标注的方法

//通过注解@PostConstruct自定义初始化方法
@PostConstruct
public void initPostConstruct() {

【步骤7】如果实现了InitializingBean初始化接口,会执行重写的afterPropertiesSet()方法进行初始化

@Override
public void afterPropertiesSet() throws Exception {

【步骤8】执行spring.xml配置文件中通过bean标签的init-method属性指定的初始化方法

//通过<bean>的init-method属性指定的自定义初始化方法
public void initMethod() throws Exception {

【步骤9】实现BeanPostProcessor接口,在postProcessAfterInitialization(Object bean, String beanName)方法中进行后置初始化处理

//After后置初始化方法
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

初始化完毕,现在可以通过contetx.getBean(,)方法获取到容器中的bean:
UserService user = contetx.getBean("userService123", UserService.class);
contetx.close();
System.out.println("【步骤13】关闭spring bean容器contetx.close()");

销毁Bean:

【步骤10】如果通过@PreDestroy注解自定义了销毁方法preDestroy(),则会先执行此销毁方法

//通过注解@PreDestroy自定义销毁方法
@PreDestroy
public void preDestroy() {

【步骤11】实现DisposableBean接口,执行destroy()销毁方法

//销毁bean对象
@Override
public void destroy() throws Exception {

【步骤12】执行spring.xml配置文件中通过bean标签的destroy-method属性指定的销毁方法

//通过<bean>的destroy-method属性指定的自定义销毁方法
public void destroyMethod() throws Exception {

调用AbstractApplicationContext.close()方法:
contetx.close();

【步骤13】最后关闭spring bean容器可以调用contetx.close()方法

1.2Spring的Bean不是线程安全的

Spring 不保证 bean 的线程安全,因为spring 容器中的 bean 的作用域默认是单例的,当存在竞态条件时就可能会产生线程安全问题。Bean 的线程安全性主要取决于 Bean 的作用域(scope)和Bean是否有状态(状态即数据),Spring 提供了多种作用域:

  • 单例(Singleton)    无状态不依赖非线程安全的外部资源线程安全!否则线程不安全!
  • 原型(Prototype)    线程安全!
  • 请求(Request)    线程安全!
  • 会话(Session)    无状态不依赖非线程安全的外部资源线程安全!否则如果多个线程(请求)同时访问同一个会话Scoped Bean并且该Bean内部状态被修改线程不安全!
  • 全局会话(Global Session)作用域:仅适用于基于portlet的分布式Web应用的上下文中,在整个集群的用户会话期间共享一个Bean实例。如果多个线程同时访问同一个全局会话Scoped Bean,并且该Bean内部状态被修改,那么可能会出现竞争条件。线程安全!/ 线程不安全!

补充:web中的四个域对象::

  • page域指pageContext(jsp有效)
  • request域指HttpServletRequest(一次请求)
  • session域指HTTPSession(一次会话)
  • application域指ServletContext(当前web应用)

他们之所以是域对象,原因是他们都内置了map集合,都有setAttribute和getAttribute方法。而且他们的name都是String类型,而value都是Object类型。

  • page:jsp页面被执行,生命周期开始,jsp页面执行完毕,生命周期结束。
  • request:服务器收到一个请求生命周期开始,服务器返回响应请求结束,生命周期结束。
  • seesion:用户访问浏览器创建session生命周期开始,session超时或被声明失效该对象生命周期结束。
  • application:web应用加载的时候创建。Web应用被移除或服务器关闭,对象销毁。

spring配置文件配置单例Bean--Counter默认scope="Singleton"

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans 
		http://www.springframework.org/schema/beans/spring-beans.xsd
	    http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
        
	<bean id="counter" class="constxiong.interview.threadsafe.Counter" />
	
</beans>

Counter有状态单例Bean--有数据

package constxiong.interview.threadsafe;
 
/**
 * 计数类
 */
public class Counter {
 
	private int count = 0;
	
	public void addAndPrint() {
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(++count);
	}
	
}

测试类,10个线程

package constxiong.interview.threadsafe;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class CounterTest {
	
	public static void main(String[] args) {
		final ApplicationContext context = new ClassPathXmlApplicationContext("spring_safe.xml");
 
		for (int i = 0; i < 10; i++) {
			new Thread(){
				@Override
				public void run() {
					Counter counter = (Counter)context.getBean("counter");
					for (int j = 0; j < 1000; j++) {
						counter.addAndPrint();
					}
				}
			}.start();
		}
		
	}
	
}

期望打印出的最大值应该是 10000,并不是,所以单例Bean存在线程安全问题

1
5
7
4
2
6
3
8
9
.
.
.
9818
9819
9820
9821
9822
9823
9824
9825

修改 spring 配置文件,把 bean 的scope作用域改为 prototype

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans 
		http://www.springframework.org/schema/beans/spring-beans.xsd
	    http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
        
	<bean id="counter" class="constxiong.interview.threadsafe.Counter" scope="prototype"/>
	
</beans>

测试结果输出10个 1000,即每个线程都创建了一个 Counter 对象,线程内独自计数,不存在线程安全问题。

下面来分别介绍一下Bean不同作用域的线程安全性。

单例(Singleton)
  在Spring中,单例作用域是默认的作用域,容器中只会存在一个该类型的实例。如果Bean的实现没有状态并且没有依赖非线程安全的外部资源,那么该Bean就是线程安全的。因为所有线程都共享同一个实例,不会有多个线程同时修改同一个实例的状态。但是,如果Bean的实现具有状态,或者它依赖于非线程安全的外部资源,那么该Bean就不是线程安全的

原型(Prototype)
  在Spring中,原型(Prototype)作用域是指每次获取Bean时都会创建一个新的Bean实例。每个原型作用域的Bean实例都是独立的,之间互不影响,也不会共享任何状态信息。因此,原型作用域的Bean是线程安全的

请求(Request)
        在Web应用中有效,每次HTTP请求都会创建一个新的bean实例,在同一个请求中,不同的组件都会共享同一个实例。在不同的请求中,会创建不同的实例,由于每个请求都拥有自己独立的实例,因此请求作用域可以看做是线程安全的。可以通过@Scope(“request”)注解来设置作用域为请求作用域。

会话(Session)
  在Web应用中有效,在整个用户会话期间共享一个Bean实例(通过浏览器访问应用程序时创建一个实例直到浏览器关闭)。不同用户的会话会创建不同的实例,但对于同一用户的多个请求,会使用同一个实例。可以通过@Scope(“session”)注解来设置作用域为会话作用域。如果多个线程同时访问同一个会话Scoped Bean,并且该Bean内部状态被修改,那么可能会出现竞争条件,线程不安全,如果不出现上述状况则是线程安全的

2. boot装配机制

面试可以这样简洁回答:
启动类的@SpringBootApplication注解主要由@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan三个注解组成,三个注解共同完成自动装配,@EnableAutoConfiguration注解是实现自动装配的核心注解。

@SpringBootConfiguration 注解标记启动类为配置类
@ComponentScan 注解扫描启动类所在的包及其子包下所有被标记为bean的类,并由IOC容器注册为Bean
@EnableAutoConfiguration通过 @Import 注解导入 AutoConfigurationImportSelector类,然后通过该类的 selectImports() 方法去读取spring.factories文件配置的需要被自动装配的组件的全类名,并按照一定的规则过滤掉不符合要求的组件的全类名,最后将读取到的组件的全类名集合返回给IOC容器并将这些组件注册为Bean。

3. 类加载机制和双亲委派模型

3.1类加载机制

简述java虚拟机class文件/类加载机制

.java文件通过编译器可以生成.class文件,.class文件是一种平台无关性的二进制文,需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是通过类的全限定名把二进制的class文件从硬盘读取到jvm内存中并对数据进行校验,解析和初始化。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的。除非我们有特殊的用法,像是反射,就需要显式的加载类。

类装载方式有两种 :

1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。

2.显式装载, 通过Class.forName("com.kaytune.User")等方法,显式加载需要的类。

Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。

主要有一下四种类加载器:

启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的目录中的类库。
扩展类加载器(Extensions Class Loader):它用来加载java扩展库。负责加载\lib\ext目录或java.ext.dirs系统变量指定的目录中的所有类库;
系统类加载器/应用程序类加载器(System Class Loader):一般来说,Java 应用程序的类都是由它来完成加载的,它根据java应用程序的用户自定义类的类路径(classpath)来加载 Java 类。可以通过 ClassLoader.getSystemClassLoader() 来获取系统类加载器。
自定义类加载器:通过继承 java.lang.ClassLoader 类的方式实现。

说一下类装载的执行过程?类装载分为以下 5 个步骤:

加载:根据全路径找到相应的 class 文件然后导入;
验证:验证加载的 class 文件的正确性,确保文件不会导致 JVM 出现异常;
准备:给类中的静态变量即类变量分配内存空间和初始化零值的过程;

注意这时候分配的是类变量的内存,这些内存会在方法区中分配。此时不会分配实例变量的内存,因为实例变量是在实例化对象时一起创建在Java 堆中的。而且此时类变量是赋值为零值,即 int 类型初值为 0,引用类型初值为 null,而不是代码中显式赋值的数值。
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而直接引用是直接指向内存中的地址,可以通过直接引用在内存中查找到目标;
初始化:执行 java 静态代码块,对静态变量赋值,这里的赋值才是代码里面的赋值(准备阶段只是设置初始值)。

3.2双亲委派模型

如果一个类加载器收到了类加载的请求,它不会自己先去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,如果父类不能加载(它的搜索范围中没找到所需的类),反馈给子类,由子类去完成类的加载。

4. MVC步骤

4.1什么是Spring MVC?MVC模式的优点?

Spring MVC是一个基于java Servlet的实现了MVC设计模式的轻量级的Web框架。通过把模型M-视图V-控制器C分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。

使用servlet需要每个请求路径都要去web.xml中配置一个servlet或者添加@WebServlet@WebFilter注解。而SpringMVC中的DispatcherServlet他会拦截所有的请求,进一步去查找有没有合适的处理器,一个前端控制器就可以。

Servlet性能最好,是处理Http请求的标准。

SpringMVC开发效率高(好多共性的东西都封装好了,是对Servlet的封装,核心的DispatcherServlet最终继承自HttpServlet)。

4.2DispatcherServlet/Controlle类与Servlet的区别和联系

Spring MVC是基于servlet 的,它有一个DispatherServlet,然后它负责处理请求,并且调用了你的controller。有人在使用controller的时候会认为它是一个servlet,其实并不是的!DisPatcherServlet是Spring中唯一的Servlet,Servlet容器将所有请求都交给DisPatcherServlet处理,然后DisPatcherServlet分发请求通过HandlerMapping(处理器映射器)和HandlerAdapter(处理器适配器)找到具体的Controller。因此可见,Controller只是一个普通的JavaBean不是Servlet,默认Controller 和 Servlet 都是单例的DisPatcherServlet的生命周期和Servlet的生命周期是相同的,都是在第一次被访问时创建,servlet容器(tomcat等)关闭时销毁

DispatcherServlet继承了FrameworkServlet,最终继承自HttpServlet

public class DispatcherServlet extends FrameworkServlet { 

4.3请描述Spring MVC / DispatcherServlet的工作流程?

(1)用户发送请求,Tomcat等Servlet容器收到请求以后,通过Java的反射机制(根据Servlet类的全类名)创建DispatcherServlet并将所有请求都交给DispatcherServlet前端控制器来处理。
(2)前端控制器DispatcherServlet收到请求后,调用HandlerMapping处理器映射器。
(3)HandlerMapping根据请求url找到具体的处理器,把请求映射为HandlerExecutionChain对象返回给DispatcherServlet,HandlerExecutionChain对象包含一个Handler处理器及该Handler处理器对应的多个HandlerInterceptor处理器拦截器(如果有则生成)组成的数组。
(4)DispatcherServlet根据获得的Handler,从所有HandlerAdapter 中找到可以处理该 Handler 的 HandlerAdapter 处理器适配器;成功获得HandlerAdapter后,将执行 HandlerExecutionChain 中所有拦截器的 preHandler() 方法;

(HandlerInterceptor接口的三个方法preHandle(),postHandle()和afterCompletion()都是defalult修饰的,其实现类即拦截器不需要全部实现)。过滤器、拦截器の用法和区别

  1. preHandle():在请求被处理之前进行操作,预处理。
  2. postHandle():在请求被处理之后,但结果还没有响应前进行操作,可以改变响应结果,后处理。
  3. afterCompletion():所有的请求响应结束后执行善后工作,清理对象、关闭资源,最终处理。

如果想设置响应头,在postHandle中进行,当afterCompletion被调用时响应已经被提交了。

(5)然后 HandlerAdapter 通过反射调用具体的处理器Handler(Handler,也叫后端控制器即Controller);Handler执行完成得到 ModelAndView返回给DispatcherServlet,再依次调用拦截器的postHandler() 方法;

(6)DispatcherServlet调用ViewResolver视图解析器,视图解析器根据ModelAndView或是Exception(可解析成 ModelAndView)解析出View返回给DispatcherServlet。

(7)DispatcherServlet调用AbstractView的渲染视图的render() 方法,将ModelAndView中的模型数据渲染到页面。

public abstract class AbstractView extends WebApplicationObjectSupport implements View, BeanNameAware {

(8)DispatcherServlet响应结果(jsp页面、json数据等)给用户/浏览器;然后再依次调用拦截器的 afterCompletion() 方法。

5.Servlet生命周期

Servlet 是实现了 javax.servlet.Servlet 接口的用于服务 HTTP 请求的 Java 类。

Servlet作用大致过程

  1. 由客户端向服务器(servlet容器如tomcat)发送一个请求
  2. 再由服务器(servlet容器如tomcat)将请求信息发送到Servlet
  3. Servlet生成响应的内容发送给服务器(servlet容器如tomcat)
  4. 最后由服务器(servlet容器如tomcat)将响应内容发送给客户端

servlet的三种常见的实现方式:

  1. 实现接口javax.servlet.Servlet
  2. 继承抽象类GenericServlet。public abstract class GenericServlet implements Servlet, ServletConfig, Serializable { 
  3. 继承抽象类HttpServlet(常用)public abstract class HttpServlet extends GenericServlet

tomcat服务器作为servlet容器,是利用Java的反射机制根据Servlet类的全类名来自动创建servlet对象并调用servlet的init()、service()等方法的。tomcat在创建servlet对象之前,先在以map为数据结构的缓存池中,判断是否已经存在servlet对象:

— —存在,直接使用缓存池的servlet对象,所以servlet是单例的。

— —不存在,创建一个servlet对象,然后添加进servlet的缓存池。

Servlet的生命周期分为4个阶段:实例化初始化服务销毁

3.1加载和实例化

        Servlet 容器负责加载和实例化 Servlet。当容器启动或首次请求某个 Servlet 时,容器会读取 web.xml 或 @WebServlet 中的配置信息通过反射对 Servlet 进行实例化,而反射的本质是调用它的构造方法(无参)

3.2初始化

        实例化之后,容器会调用servlet的init()方法去初始化servlet。初始化的目的是让Servlet实例在处理请求之前完成一些工作,例如:建立数据库的连接、获取配置信息等

        在整个Servlet的生命周期内,init()方法只能被调用一次。初始化期间,Servlet 实例可以通过 ServletConfig 对象获取在 web.xml 或者 @WebServlet注解中配置的初始化参数

3.3服务

        初始化之后,Servlet容器为来自客户端的请求创建 ServletRequst 对象和 ServletResponse 对象,将它们以参数的形式传入 service() 方法内,并调用该方法对请求进行处理。

        在 service() 方法中,Servlet 可以通过 ServletRequst 对象获取客户端的请求信息。在请求处理完成后,可以通过 ServletResponse 对象将响应信息进行包装,返回给客户端。当 Servlet 容器将响应信息返回给客户端后,ServletRequst 对象与 ServletResponse 对象就会被销毁。        

        在 Servlet 的整个生命周期内,对于 Servlet 的每一次请求,Servlet 容器都会调用一次 service() 方法,并创建新的 ServletRequest 和 ServletResponse 对象。即 service() 方法在 Servlet 的整个生命周期中会被调用多次。

3.4销毁

        当 Servlet 容器关闭、重启时,就表示Servlet的生命周期结束,容器就会调用 destory() 方法,释放该实例使用的资源,随后该实例就会被 Java 的垃圾收集器GC所回收。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值