锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

Spring 夺命 35 问!

时间:2023-04-22 18:37:00 a1101a1t变送器

有人说,“Java程序员都是Spring可见程序员Spring在Java世界起着重要的作用。

基础

1.Spring是什么特点?模块有哪些?

19fbf9cfc873cb64a5c6a8680cfb0c21.png
Spring Logo

一句话总结:Spring 轻量级、非入侵控制反转 (IoC) 和面向切面 (AOP) 的框架。

2003年,一位音乐家Rod Johnson决定发展轻量级Java开发框架,Spring作为Java战场上的龙骑兵逐渐崛起并被淘汰EJB传统的重装骑兵。

Spring重要版本

到目前为止,企业级发展的标准基本上是 Spring5 Spring Boot 2 JDK 8

Spring有哪些特点?

Spring有许多优点:

Spring特性
  1. IOCDI 的支持

Spring 其核心是大型工厂容器,可以维持所有对象的创建和依赖,Spring 用于生产的工厂 Bean,并且管理 Bean 实现生命周期高内聚低耦合设计理念。

  1. AOP 编程的支持

Spring 提供了面向切面编程,权限拦截、操作监控等切面功能可以轻松实现。

  1. 支持声明事务

支持通过配置完成事务管理,而不需要硬编码,以前重复的事务提交和回滚JDBC不用自己写代码。

  1. 支持快速测试

Spring 对 Junit 可通过提供支持注解快捷地测试 Spring 程序。

  1. 快速集成功能

方便集成各种优秀框架,Spring 不排斥各种优秀的开源框架,内部提供各种优秀框架(如:Struts、Hibernate、MyBatis、Quartz 等)直接支持。

  1. 复杂API模板封装

Spring 对 JavaEE 在开发中很难使用一些 API(JDBC、JavaMail、远程调用等。)提供模板包装,这些包装 API 应用难度大大降低。

2.Spring哪些模块?

Spring 除了核心,框架是分模块存在的Spring Core Container除了必要的模块,其他模块都是可选,大约有 20 多个模块。

Spring模块划分

七个主要模块:

  1. Spring Core:Spring 核心是框架最基本的部分,提供 IOC 和依赖注入 DI 特性。

  2. Spring Context:Spring 上下文容器,它是 BeanFactory 功能增强的子接口。

  3. Spring Web:它提供 Web 支持应用开发。

  4. Spring MVC:它针对 Web 应用中 MVC 实现思想。

  5. Spring DAO:提供对 JDBC 抽象层,简化 JDBC 同时,编码更健壮。

  6. Spring ORM:它支持流行 ORM 例如:Spring Hibernate、Spring iBatis、Spring JDO 的整合等。

  7. Spring AOP:也就是说,它提供切面编程 AOP 实现联盟兼容的编程。

3.Spring常用的注释有哪些?

Spring有很多模块,甚至是广义的SpringBoot、SpringCloud也算是Spring我们来分模块,根据功能看一些常用的注释:

Spring常用注解

Web:

  • @Controller:组合注释(组合)@Component注释),应用于MVC层(控制层)。

  • @RestController:相当于组合注释@Controller和@ResponseBody类中注释的组合意味着Controller默认添加了所有方法@ResponseBody。

  • @RequestMapping:用于映射Web请求,包括访问路径和参数。如果是Restful也可根据要求类型使用不同的注释:

    • @GetMapping

    • @PostMapping

    • @PutMapping

    • @DeleteMapping

  • @ResponseBody:支持将返回值放response内部,而不是页面,用户通常会返回json数据。

  • @RequestBody:允许request的参数在request身体,而不是直接连接到地址后面。

  • @PathVariable:用于接收路径参数,如@RequestMapping(“/hello/{name})申报的路径可以通过在参数中注释来获得,通常用作Restful接口实现方法。

  • @RestController:相当于组合注释@Controller和@ResponseBody类中注释的组合意味着Controller默认添加了所有方法@ResponseBody。

容器:

  • @Component:表示注释类是一个组件Spring管理的Bean。当使用基于注释的配置和类路径扫描时,这些类被视为自动检测的候选人。@Component还是元注解。

  • @Service:组合注释(组合)@Component注释),应用于service层(业务逻辑层)。

  • @Repository:组合注释(组合)@Component注释),应用于dao层(数据访问层)。

  • @Autowired:Spring提供的工具(由Spring依赖注入工具(BeanPostProcessor、BeanFactoryPostProcessor)自动注入)。

  • @Qualifier:通常跟着这个注释 @Autowired 一起使用,当想对注入的过程做更多的控制,@Qualifier 有助于配置,如两种以上相同类型的配置 Bean 时 Spring 不能选择,用这个注释

  • @Configuration:声明当前类是一个配置类(相当于一个Spring配置的xml文件)

  • @Value:可用于字段、构造器参数和方法参数,指定默认值 #{} 跟 ${} 两种方种方式 SpringbBoot 中的 application.properties 配置的属性值赋值变量。

  • @Bean:在方法上注明当前方法的返回值Bean。返回的Bean可以在相应的类别中定义init()方法和destroy()方法,然后在@Bean(initMethod=”init”,destroyMethod=”destroy)定义,构造后执行init,在销毁前执行destroy。

  • @Scope:定义我们使用什么模式来创建Bean(方法上,必须有@Bean) 设置类型包括:Singleton 、Prototype、Request 、 Session、GlobalSession。

AOP:

  • @Aspect:声明一个切面(类) 使用@After、@Before、@Around定义建言(advice),拦截规则(切点)可以直接作为参数。

    • @After :执行方法后(方法)。

    • @Before:执行方法前(方法)。

    • @Around:在方法执行之前与之后执行(方法上)。

    • @PointCut:声明切点 在java配置类中使用@EnableAspectJAutoProxy注解开启Spring对AspectJ代理的支持(类上)。

事务:

  • @Transactional:在要开启事务的方法上使用@Transactional注解,即可声明式开启事务。

4.Spring 中应用了哪些设计模式呢?

Spring 框架中广泛使用了不同类型的设计模式,下面我们来看看到底有哪些设计模式?

Spring中用到的设计模式
  1. 工厂模式 : Spring 容器本质是一个大工厂,使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。

  2. 代理模式 : Spring AOP 功能功能就是通过代理模式来实现的,分为动态代理和静态代理。

  3. 单例模式 : Spring 中的 Bean 默认都是单例的,这样有利于容器对Bean的管理。

  4. 模板模式 : Spring 中 JdbcTemplate、RestTemplate 等以 Template结尾的对数据库、网络等等进行操作的模板类,就使用到了模板模式。

  5. 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。

  6. 适配器模式 :Spring AOP 的增强或通知 (Advice) 使用到了适配器模式、Spring MVC 中也是用到了适配器模式适配 Controller。

  7. 策略模式:Spring中有一个Resource接口,它的不同实现类,会根据不同的策略去访问资源。

IOC

5.说一说什么是IOC?什么是DI?

Java 是面向对象的编程语言,一个个实例对象相互合作组成了业务逻辑,原来,我们都是在代码里创建对象和对象的依赖。

所谓的IOC(控制反转):就是由容器来负责控制对象的生命周期和对象间的关系。以前是我们想要什么,就自己创建什么,现在是我们需要什么,容器就给我们送来什么。

引入IOC之前和引入IOC之后

也就是说,控制对象生命周期的不再是引用它的对象,而是容器。对具体对象,以前是它控制其它对象,现在所有对象都被容器控制,所以这就叫控制反转

控制反转示意图

DI(依赖注入):指的是容器在实例化对象的时候把它依赖的类注入给它。有的说法IOC和DI是一回事,有的说法是IOC是思想,DI是IOC的实现。

为什么要使用IOC呢?

最主要的是两个字解耦,硬编码会造成对象间的过度耦合,使用IOC之后,我们可以不用关心对象间的依赖,专心开发应用就行。

6.能简单说一下Spring IOC的实现机制吗?

PS:这道题老三在面试中被问到过,问法是“你有自己实现过简单的Spring吗?

Spring的IOC本质就是一个大工厂,我们想想一个工厂是怎么运行的呢?

工厂运行
  • 生产产品:一个工厂最核心的功能就是生产产品。在Spring里,不用Bean自己来实例化,而是交给Spring,应该怎么实现呢?——答案毫无疑问,反射

    那么这个厂子的生产管理是怎么做的?你应该也知道——工厂模式

  • 库存产品:工厂一般都是有库房的,用来库存产品,毕竟生产的产品不能立马就拉走。Spring我们都知道是一个容器,这个容器里存的就是对象,不能每次来取对象,都得现场来反射创建对象,得把创建出的对象存起来。

  • 订单处理:还有最重要的一点,工厂根据什么来提供产品呢?订单。这些订单可能五花八门,有线上签签的、有到工厂签的、还有工厂销售上门签的……最后经过处理,指导工厂的出货。

    在Spring里,也有这样的订单,它就是我们bean的定义和依赖关系,可以是xml形式,也可以是我们最熟悉的注解形式。

我们简单地实现一个mini版的Spring IOC:

mini版本Spring IOC

Bean定义:

Bean通过一个配置文件定义,把它解析成一个类型。

  • beans.properties

    偷懒,这里直接用了最方便解析的properties,这里直接用一个类型的配置来代表Bean的定义,其中key是beanName,value是class

    userDao:cn.fighter3.bean.UserDao
  • BeanDefinition.java

    bean定义类,配置文件中bean定义对应的实体

    public class BeanDefinition {
    
        private String beanName;
    
        private Class beanClass;
         //省略getter、setter  
     }
  • ResourceLoader.java

    资源加载器,用来完成配置文件中配置的加载

    public class ResourceLoader {
    
        public static Map getResource() {
            Map beanDefinitionMap = new HashMap<>(16);
            Properties properties = new Properties();
            try {
                InputStream inputStream = ResourceLoader.class.getResourceAsStream("/beans.properties");
                properties.load(inputStream);
                Iterator it = properties.stringPropertyNames().iterator();
                while (it.hasNext()) {
                    String key = it.next();
                    String className = properties.getProperty(key);
                    BeanDefinition beanDefinition = new BeanDefinition();
                    beanDefinition.setBeanName(key);
                    Class clazz = Class.forName(className);
                    beanDefinition.setBeanClass(clazz);
                    beanDefinitionMap.put(key, beanDefinition);
                }
                inputStream.close();
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
            return beanDefinitionMap;
        }
    
    }
  • BeanRegister.java

    对象注册器,这里用于单例bean的缓存,我们大幅简化,默认所有bean都是单例的。可以看到所谓单例注册,也很简单,不过是往HashMap里存对象。

    public class BeanRegister {
    
        //单例Bean缓存
        private Map singletonMap = new HashMap<>(32);
    
        /**
         * 获取单例Bean
         *
         * @param beanName bean名称
         * @return
         */
        public Object getSingletonBean(String beanName) {
            return singletonMap.get(beanName);
        }
    
        /**
         * 注册单例bean
         *
         * @param beanName
         * @param bean
         */
        public void registerSingletonBean(String beanName, Object bean) {
            if (singletonMap.containsKey(beanName)) {
                return;
            }
            singletonMap.put(beanName, bean);
        }
    
    }
  • BeanFactory.java

    BeanFactory
    • 对象工厂,我们最核心的一个类,在它初始化的时候,创建了bean注册器,完成了资源的加载。

    • 获取bean的时候,先从单例缓存中取,如果没有取到,就创建并注册一个bean

      public class BeanFactory {
      
          private Map beanDefinitionMap = new HashMap<>();
      
          private BeanRegister beanRegister;
      
          public BeanFactory() {
              //创建bean注册器
              beanRegister = new BeanRegister();
              //加载资源
              this.beanDefinitionMap = new ResourceLoader().getResource();
          }
      
          /**
           * 获取bean
           *
           * @param beanName bean名称
           * @return
           */
          public Object getBean(String beanName) {
              //从bean缓存中取
              Object bean = beanRegister.getSingletonBean(beanName);
              if (bean != null) {
                  return bean;
              }
              //根据bean定义,创建bean
              return createBean(beanDefinitionMap.get(beanName));
          }
      
          /**
           * 创建Bean
           *
           * @param beanDefinition bean定义
           * @return
           */
          private Object createBean(BeanDefinition beanDefinition) {
              try {
                  Object bean = beanDefinition.getBeanClass().newInstance();
                  //缓存bean
                  beanRegister.registerSingletonBean(beanDefinition.getBeanName(), bean);
                  return bean;
              } catch (InstantiationException | IllegalAccessException e) {
                  e.printStackTrace();
              }
              return null;
          }
      }
  • 测试

    • UserDao.java

      我们的Bean类,很简单

      public class UserDao {
      
          public void queryUserInfo(){
              System.out.println("A good man.");
          }
      }
    • 单元测试

      public class ApiTest {
          @Test
          public void test_BeanFactory() {
              //1.创建bean工厂(同时完成了加载资源、创建注册单例bean注册器的操作)
              BeanFactory beanFactory = new BeanFactory();
      
              //2.第一次获取bean(通过反射创建bean,缓存bean)
              UserDao userDao1 = (UserDao) beanFactory.getBean("userDao");
              userDao1.queryUserInfo();
      
              //3.第二次获取bean(从缓存中获取bean)
              UserDao userDao2 = (UserDao) beanFactory.getBean("userDao");
              userDao2.queryUserInfo();
          }
      }
    • 运行结果

      A good man.
      A good man.

至此,我们一个乞丐+破船版的Spring就完成了,代码也比较完整,有条件的可以跑一下。

PS:因为时间+篇幅的限制,这个demo比较简陋,没有面向接口、没有解耦、边界检查、异常处理……健壮性、扩展性都有很大的不足,感兴趣可以学习参考[15]。

7.说说BeanFactory和ApplicantContext?

可以这么形容,BeanFactory是Spring的“心脏”,ApplicantContext是完整的“身躯”。

BeanFactory和ApplicantContext的比喻
  • BeanFactory(Bean工厂)是Spring框架的基础设施,面向Spring本身。

  • ApplicantContext(应用上下文)建立在BeanFactoty基础上,面向使用Spring框架的开发者。

BeanFactory 接口

BeanFactory是类的通用工厂,可以创建并管理各种类的对象。

Spring为BeanFactory提供了很多种实现,最常用的是XmlBeanFactory,但在Spring 3.2中已被废弃,建议使用XmlBeanDefinitionReader、DefaultListableBeanFactory。

Spring5 BeanFactory继承体系

BeanFactory接口位于类结构树的顶端,它最主要的方法就是getBean(String var1),这个方法从容器中返回特定名称的Bean。

BeanFactory的功能通过其它的接口得到了不断的扩展,比如AbstractAutowireCapableBeanFactory定义了将容器中的Bean按照某种规则(比如按名字匹配、按类型匹配等)进行自动装配的方法。

这里看一个 XMLBeanFactory(已过期)  获取bean 的例子:

public class HelloWorldApp{ 
   public static void main(String[] args) { 
      BeanFactory factory = new XmlBeanFactory (new ClassPathResource("beans.xml")); 
      HelloWorld obj = (HelloWorld) factory.getBean("helloWorld");    
      obj.getMessage();    
   }
}
ApplicationContext 接口

ApplicationContext由BeanFactory派生而来,提供了更多面向实际应用的功能。可以这么说,使用BeanFactory就是手动档,使用ApplicationContext就是自动档。

Spring5 ApplicationContext部分体系类图

ApplicationContext 继承了HierachicalBeanFactory和ListableBeanFactory接口,在此基础上,还通过其他的接口扩展了BeanFactory的功能,包括:

  • Bean instantiation/wiring

  • Bean 的实例化/串联

  • 自动的 BeanPostProcessor 注册

  • 自动的 BeanFactoryPostProcessor 注册

  • 方便的 MessageSource 访问(i18n)

  • ApplicationEvent 的发布与 BeanFactory 懒加载的方式不同,它是预加载,所以,每一个 bean 都在 ApplicationContext 启动之后实例化

这是 ApplicationContext 的使用例子:

public class HelloWorldApp{ 
   public static void main(String[] args) { 
      ApplicationContext context=new ClassPathXmlApplicationContext("beans.xml"); 
      HelloWorld obj = (HelloWorld) context.getBean("helloWorld");    
      obj.getMessage();    
   }
}

ApplicationContext 包含 BeanFactory 的所有特性,通常推荐使用前者。

8.你知道Spring容器启动阶段会干什么吗?

PS:这道题老三面试被问到过

Spring的IOC容器工作的过程,其实可以划分为两个阶段:容器启动阶段Bean实例化阶段

其中容器启动阶段主要做的工作是加载和解析配置文件,保存到对应的Bean定义中。

容器启动和Bean实例化阶段

容器启动开始,首先会通过某种途径加载Congiguration MetaData,在大部分情况下,容器需要依赖某些工具类(BeanDefinitionReader)对加载的Congiguration MetaData进行解析和分析,并将分析后的信息组为相应的BeanDefinition。

xml配置信息映射注册过程

最后把这些保存了Bean定义必要信息的BeanDefinition,注册到相应的BeanDefinitionRegistry,这样容器启动就完成了。

9.能说一下Spring Bean生命周期吗?

可以看看:Spring Bean生命周期,好像人的一生。。

在Spring中,基本容器BeanFactory和扩展容器ApplicationContext的实例化时机不太一样,BeanFactory采用的是延迟初始化的方式,也就是只有在第一次getBean()的时候,才会实例化Bean;ApplicationContext启动之后会实例化所有的Bean定义。

Spring IOC 中Bean的生命周期大致分为四个阶段:实例化(Instantiation)、属性赋值(Populate)、初始化(Initialization)、销毁(Destruction)。

Bean生命周期四个阶段

我们再来看一个稍微详细一些的过程:

  • 实例化:第 1 步,实例化一个 Bean 对象

  • 属性赋值:第 2 步,为 Bean 设置相关属性和依赖

  • 初始化:初始化的阶段的步骤比较多,5、6步是真正的初始化,第 3、4 步为在初始化前执行,第 7 步在初始化后执行,初始化完成之后,Bean就可以被使用了

  • 销毁:第 8~10步,第8步其实也可以算到销毁阶段,但不是真正意义上的销毁,而是先在使用前注册了销毁的相关调用接口,为了后面第9、10步真正销毁 Bean 时再执行相应的方法

简单总结一下,Bean生命周期里初始化的过程相对步骤会多一些,比如前置、后置的处理。

最后通过一个实例来看一下具体的细节:

  • 定义一个PersonBean类,实现DisposableBean, InitializingBean, BeanFactoryAware, BeanNameAware这4个接口,同时还有自定义init-methoddestroy-method

public class PersonBean implements InitializingBean, BeanFactoryAware, BeanNameAware, DisposableBean {

    /**
     * 身份证号
     */
    private Integer no;

    /**
     * 姓名
     */
    private String name;

    public PersonBean() {
        System.out.println("1.调用构造方法:我出生了!");
    }

    public Integer getNo() {
        return no;
    }

    public void setNo(Integer no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        System.out.println("2.设置属性:我的名字叫"+name);
    }

    @Override
    public void setBeanName(String s) {
        System.out.println("3.调用BeanNameAware#setBeanName方法:我要上学了,起了个学名");
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println("4.调用BeanFactoryAware#setBeanFactory方法:选好学校了");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("6.InitializingBean#afterPropertiesSet方法:入学登记");
    }

    public void init() {
        System.out.println("7.自定义init方法:努力上学ing");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("9.DisposableBean#destroy方法:平淡的一生落幕了");
    }

    public void destroyMethod() {
        System.out.println("10.自定义destroy方法:睡了,别想叫醒我");
    }

    public void work(){
        System.out.println("Bean使用中:工作,只有对社会没有用的人才放假。。");
    }

}
  • 定义一个MyBeanPostProcessor实现BeanPostProcessor接口。

public class MyBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("5.BeanPostProcessor.postProcessBeforeInitialization方法:到学校报名啦");
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("8.BeanPostProcessor#postProcessAfterInitialization方法:终于毕业,拿到毕业证啦!");
        return bean;
    }
}
  • 配置文件,指定init-methoddestroy-method属性




    
    
        
        
    

  • 测试

public class Main {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
        PersonBean personBean = (PersonBean) context.getBean("personBean");
        personBean.work();
        ((ClassPathXmlApplicationContext) context).destroy();
    }
}
  • 运行结果:

1.调用构造方法:我出生了!
2.设置属性:我的名字叫张铁钢
3.调用BeanNameAware#setBeanName方法:我要上学了,起了个学名
4.调用BeanFactoryAware#setBeanFactory方法:选好学校了
5.BeanPostProcessor#postProcessBeforeInitialization方法:到学校报名啦
6.InitializingBean#afterPropertiesSet方法:入学登记
7.自定义init方法:努力上学ing
8.BeanPostProcessor#postProcessAfterInitialization方法:终于毕业,拿到毕业证啦!
Bean使用中:工作,只有对社会没有用的人才放假。。
9.DisposableBean#destroy方法:平淡的一生落幕了
10.自定义destroy方法:睡了,别想叫醒我

关于源码,Bean创建过程可以查看AbstractBeanFactory#doGetBean方法,在这个方法里可以看到Bean的实例化,赋值、初始化的过程,至于最终的销毁,可以看看ConfigurableApplicationContext#close()

Bean生命周期源码追踪

10.Bean定义和依赖定义有哪些方式?

有三种方式:直接编码方式配置文件方式注解方式

Bean依赖配置方式
  • 直接编码方式:我们一般接触不到直接编码的方式,但其实其它的方式最终都要通过直接编码来实现。

  • 配置文件方式:通过xml、propreties类型的配置文件,配置相应的依赖关系,Spring读取配置文件,完成依赖关系的注入。

  • 注解方式:注解方式应该是我们用的最多的一种方式了,在相应的地方使用注解修饰,Spring会扫描注解,完成依赖关系的注入。

11.有哪些依赖注入的方法?

Spring支持构造方法注入属性注入工厂方法注入,其中工厂方法注入,又可以分为静态工厂方法注入非静态工厂方法注入

Spring依赖注入方法
  • 构造方法注入

    通过调用类的构造方法,将接口实现类通过构造方法变量传入

    public CatDaoImpl(String message){
       this. message = message;
     }
     
      
    
  • 属性注入

    通过Setter方法完成调用类所需依赖的注入

    public class Id {
        private int id;
    
        public int getId() { return id; }
     
        public void setId(int id) { this.id = id; }
    }
     
       
    
  • 工厂方法注入

    • 静态工厂注入

      静态工厂顾名思义,就是通过调用静态工厂的方法来获取自己需要的对象,为了让 Spring 管理所有对象,我们不能直接通过"工程类.静态方法()"来获取对象,而是依然通过 Spring 注入的形式获取:

      public class DaoFactory { //静态工厂
       
         public static final FactoryDao getStaticFactoryDaoImpl(){
            return new StaticFacotryDaoImpl();
         }
      }
       
      public class SpringAction {
       
       //注入对象
       private FactoryDao staticFactoryDao; 
       
       //注入对象的 set 方法
       public void setStaticFactoryDao(FactoryDao staticFactoryDao) {
           this.staticFactoryDao = staticFactoryDao;
       }
       
      }
      //factory-method="getStaticFactoryDaoImpl"指定调用哪个工厂方法
       
         
         
       
       
       
      
    • 非静态工厂注入

      非静态工厂,也叫实例工厂,意思是工厂方法不是静态的,所以我们需要首先 new 一个工厂实例,再调用普通的实例方法。

      //非静态工厂 
      public class DaoFactory { 
         public FactoryDao getFactoryDaoImpl(){
           return new FactoryDaoImpl();
         }
       }
       
      public class SpringAction {
        //注入对象
        private FactoryDao factoryDao; 
        
        public void setFactoryDao(FactoryDao factoryDao) {
          this.factoryDao = factoryDao;
        }
      }
      
         
         
       
       
       
       
       
      

12.Spring有哪些自动装配的方式?

什么是自动装配?

Spring IOC容器知道所有Bean的配置信息,此外,通过Java反射机制还可以获知实现类的结构信息,如构造方法的结构、属性等信息。掌握所有Bean的这些信息后,Spring IOC容器就可以按照某种规则对容器中的Bean进行自动装配,而无须通过显式的方式进行依赖配置。

Spring提供的这种方式,可以按照某些规则进行Bean的自动装配,元素提供了一个指定自动装配类型的属性:autowire="<自动装配类型>"

Spring提供了哪几种自动装配类型?

Spring提供了4种自动装配类型:

Spring四种自动装配类型
  • byName:根据名称进行自动匹配,假设Boss又一个名为car的属性,如果容器中刚好有一个名为car的bean,Spring就会自动将其装配给Boss的car属性

  • byType:根据类型进行自动匹配,假设Boss有一个Car类型的属性,如果容器中刚好有一个Car类型的Bean,Spring就会自动将其装配给Boss这个属性

  • constructor:与 byType类似, 只不过它是针对构造函数注入而言的。如果Boss有一个构造函数,构造函数包含一个Car类型的入参,如果容器中有一个Car类型的Bean,则Spring将自动把这个Bean作为Boss构造函数的入参;如果容器中没有找到和构造函数入参匹配类型的Bean,则Spring将抛出异常。

  • autodetect:根据Bean的自省机制决定采用byType还是constructor进行自动装配,如果Bean提供了默认的构造函数,则采用byType,否则采用constructor。

13.Spring 中的 Bean 的作用域有哪些?

Spring的Bean主要支持五种作用域:

Spring Bean支持作用域
  • singleton : 在Spring容器仅存在一个Bean实例,Bean以单实例的方式存在,是Bean默认的作用域。

  • prototype : 每次从容器重调用Bean时,都会返回一个新的实例。

以下三个作用域于只在Web应用中适用:

  • request : 每一次HTTP请求都会产生一个新的Bean,该Bean仅在当前HTTP Request内有效。

  • session : 同一个HTTP Session共享一个Bean,不同的HTTP Session使用不同的Bean。

  • globalSession:同一个全局Session共享一个Bean,只用于基于Protlet的Web应用,Spring5中已经不存在了。

14.Spring 中的单例 Bean 会存在线程安全问题吗?

首先结论在这:Spring中的单例Bean不是线程安全的

因为单例Bean,是全局只有一个Bean,所有线程共享。如果说单例Bean,是一个无状态的,也就是线程中的操作不会对Bean中的成员变量执行查询以外的操作,那么这个单例Bean是线程安全的。比如Spring mvc 的 Controller、Service、Dao等,这些Bean大多是无状态的,只关注于方法本身。

假如这个Bean是有状态的,也就是会对Bean中的成员变量进行写操作,那么可能就存在线程安全的问题。

Spring单例Bean线程安全问题

单例Bean线程安全问题怎么解决呢?

常见的有这么些解决办法:

  1. 将Bean定义为多例

    这样每一个线程请求过来都会创建一个新的Bean,但是这样容器就不好管理Bean,不能这么办。

  2. 在Bean对象中尽量避免定义可变的成员变量

    削足适履了属于是,也不能这么干。

  3. 将Bean中的成员变量保存在ThreadLocal中⭐

    我们知道ThredLoca能保证多线程下变量的隔离,可以在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal里,这是推荐的一种方式。

15.说说循环依赖?

什么是循环依赖?

Spring循环依赖

Spring 循环依赖:简单说就是自己依赖自己,或者和别的Bean相互依赖。

鸡和蛋

只有单例的Bean才存在循环依赖的情况,原型(Prototype)情况下,Spring会直接抛出异常。原因很简单,AB循环依赖,A实例化的时候,发现依赖B,创建B实例,创建B的时候发现需要A,创建A1实例……无限套娃,直接把系统干垮。

Spring可以解决哪些情况的循环依赖?

Spring不支持基于构造器注入的循环依赖,但是假如AB循环依赖,如果一个是构造器注入,一个是setter注入呢?

看看几种情形:

循环依赖的几种情形

第四种可以而第五种不可以的原因是 Spring 在创建 Bean 时默认会根据自然排序进行创建,所以 A 会先于 B 进行创建。

所以简单总结,当循环依赖的实例都采用setter方法注入的时候,Spring可以支持,都采用构造器注入的时候,不支持,构造器注入和setter注入同时存在的时候,看天。

16.那Spring怎么解决循环依赖的呢?

PS:其实正确答案是开发人员做好设计,别让Bean循环依赖,但是没办法,面试官不想听这个。

我们都知道,单例Bean初始化完成,要经历三步:

Bean初始化步骤

注入就发生在第二步,属性赋值,结合这个过程,Spring 通过三级缓存解决了循环依赖:

  1. 一级缓存 : Map singletonObjects,单例池,用于保存实例化、属性赋值(注入)、初始化完成的 bean 实例

  2. 二级缓存 : Map earlySingletonObjects,早期曝光对象,用于保存实例化完成的 bean 实例

  3. 三级缓存 : Map> singletonFactories,早期曝光对象工厂,用于保存 bean 创建工厂,以便于后面扩展有机会创建代理对象。

三级缓存

我们来看一下三级缓存解决循环依赖的过程:

当 A、B 两个类发生循环依赖时:

A实例的初始化过程:

  1. 创建A实例,实例化的时候把A对象⼯⼚放⼊三级缓存,表示A开始实例化了,虽然我这个对象还不完整,但是先曝光出来让大家知道

    1
  2. A注⼊属性时,发现依赖B,此时B还没有被创建出来,所以去实例化B

  3. 同样,B注⼊属性时发现依赖A,它就会从缓存里找A对象。依次从⼀级到三级缓存查询A,从三级缓存通过对象⼯⼚拿到A,发现A虽然不太完善,但是存在,把A放⼊⼆级缓存,同时删除三级缓存中的A,此时,B已经实例化并且初始化完成,把B放入⼀级缓存。

    2
  4. 接着A继续属性赋值,顺利从⼀级缓存拿到实例化且初始化完成的B对象,A对象创建也完成,删除⼆级缓存中的A,同时把A放⼊⼀级缓存

  5. 最后,⼀级缓存中保存着实例化、初始化都完成的A、B对象

5

所以,我们就知道为什么Spring能解决setter注入的循环依赖了,因为实例化和属性赋值是分开的,所以里面有操作的空间。如果都是构造器注入的化,那么都得在实例化这一步完成注入,所以自然是无法支持了。

17.为什么要三级缓存?⼆级不⾏吗?

不行,主要是为了⽣成代理对象。如果是没有代理的情况下,使用二级缓存解决循环依赖也是OK的。但是如果存在代理,三级没有问题,二级就不行了。

因为三级缓存中放的是⽣成具体对象的匿名内部类,获取Object的时候,它可以⽣成代理对象,也可以返回普通对象。使⽤三级缓存主要是为了保证不管什么时候使⽤的都是⼀个对象。

假设只有⼆级缓存的情况,往⼆级缓存中放的显示⼀个普通的Bean对象,Bean初始化过程中,通过 BeanPostProcessor 去⽣成代理对象之后,覆盖掉⼆级缓存中的普通Bean对象,那么可能就导致取到的Bean对象不一致了。

二级缓存不行的原因

18.@Autowired的实现原理?

实现@Autowired的关键是:AutowiredAnnotationBeanPostProcessor

在Bean的初始化阶段,会通过Bean后置处理器来进行一些前置和后置的处理。

实现@Autowired的功能,也是通过后置处理器来完成的。这个后置处理器就是AutowiredAnnotationBeanPostProcessor。

  • Spring在创建bean的过程中,最终会调用到doCreateBean()方法,在doCreateBean()方法中会调用populateBean()方法,来为bean进行属性填充,完成自动装配等工作。

  • 在populateBean()方法中一共调用了两次后置处理器,第一次是为了判断是否需要属性填充,如果不需要进行属性填充,那么就会直接进行return,如果需要进行属性填充,那么方法就会继续向下执行,后面会进行第二次后置处理器的调用,这个时候,就会调用到AutowiredAnnotationBeanPostProcessor的postProcessPropertyValues()方法,在该方法中就会进行@Autowired注解的解析,然后实现自动装配。

    /**
    * 属性赋值
    **/
    protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
                //………… 
                if (hasInstAwareBpps) {
                    if (pvs == null) {
                        pvs = mbd.getPropertyValues();
                    }
    
                    PropertyValues pvsToUse;
                    for(Iterator var9 = this.getBeanPostProcessorCache().instantiationAware.iterator(); var9.hasNext(); pvs = pvsToUse) {
                        InstantiationAwareBeanPostProcessor bp = (InstantiationAwareBeanPostProcessor)var9.next();
                        pvsToUse = bp.postProcessProperties((PropertyValues)pvs, bw.getWrappedInstance(), beanName);
                        if (pvsToUse == null) {
                            if (filteredPds == null) {
                                filteredPds = this.filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
                            }
                            //执行后处理器,填充属性,完成自动装配
                            //调用InstantiationAwareBeanPostProcessor的postProcessPropertyValues()方法
                            pvsToUse = bp.postProcessPropertyValues((PropertyValues)pvs, filteredPds, bw.getWrappedInstance(), beanName);
                            if (pvsToUse == null) {
                                return;
                            }
                        }
                    }
                }
               //…………
        }
  • postProcessorPropertyValues()方法的源码如下,在该方法中,会先调用findAutowiringMetadata()方法解析出bean中带有@Autowired注解、@Inject和@Value注解的属性和方法。然后调用metadata.inject()方法,进行属性填充。

    public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
            //@Autowired注解、@Inject和@Value注解的属性和方法
            InjectionMetadata metadata = this.findAutowiringMetadata(beanName, bean.getClass(), pvs);
    
            try {
                //属性填充
                metadata.inject(bean, beanName, pvs);
                return pvs;
            } catch (BeanCreationException var6) {
                throw var6;
            } catch (Throwable var7) {
                throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", var7);
            }
        }

AOP

19.说说什么是AOP?

AOP:面向切面编程。简单说,就是把一些业务逻辑中的相同的代码抽取到一个独立的模块中,让业务逻辑更加清爽。

横向抽取

具体来说,假如我现在要crud写一堆业务,可是如何业务代码前后前后进行打印日志和参数的校验呢?

我们可以把日志记录数据校验可重用的功能模块分离出来,然后在程序的执行的合适的地方动态地植入这些代码并执行。这样就简化了代码的书写。

AOP应用示例

业务逻辑代码中没有参和通用逻辑的代码,业务模块更简洁,只包含核心业务代码。实现了业务逻辑和通用逻辑的代码分离,便于维护和升级,降低了业务逻辑和通用逻辑的耦合性。

AOP 可以将遍布应用各处的功能分离出来形成可重用的组件。在编译期间、装载期间或运行期间实现在不修改源代码的情况下给程序动态添加功能。从而实现对业务逻辑的隔离,提高代码的模块化能力。

Java语言执行过程

AOP 的核心其实就是动态代理,如果是实现了接口的话就会使用 JDK 动态代理,否则使用 CGLIB 代理,主要应用于处理一些具有横切性质的系统级服务,如日志收集、事务管理、安全检查、缓存、对象池管理等。

AOP有哪些核心概念?

  • 切面(Aspect):类是对物体特征的抽象,切面就是对横切关注点的抽象

  • 连接点(Joinpoint):被拦截到的点,因为 Spring 只支持方法类型的连接点,所以在 Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器

  • 切点(Pointcut):对连接点进行拦截的定位

  • 通知(Advice):所谓通知指的就是指拦截到连接点之后要执行的代码,也可以称作增强

  • 目标对象 (Target):代理的目标对象

  • 织入(Weabing):织入是将增强添加到目标类的具体连接点上的过程。

    • 编译期织入:切面在目标类编译时被织入

    • 类加载期织入:切面在目标类加载到JVM时被织入。需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码。

    • 运行期织入:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。SpringAOP就是以这种方式织入切面。

      Spring采用运行期织入,而AspectJ采用编译期织入和类加载器织入。

  • 引介(introduction):引介是一种特殊的增强,可以动态地为类添加一些属性和方法

AOP有哪些环绕方式?

AOP 一般有 5 种环绕方式:

  • 前置通知 (@Before)

  • 返回通知 (@AfterReturning)

  • 异常通知 (@AfterThrowing)

  • 后置通知 (@After)

  • 环绕通知 (@Around)

环绕方式

多个切面的情况下,可以通过 @Order 指定先后顺序,数字越小,优先级越高。

20.说说你平时有用到AOP吗?

PS:这道题老三的同事面试候选人的时候问到了,候选人说了一堆AOP原理,同事就势来一句,你能现场写一下AOP的应用吗?结果——场面一度很尴尬。虽然我对面试写这种百度就能出来的东西持保留意见,但是还是加上了这一问,毕竟招人最后都是要撸代码的。

这里给出一个小例子,SpringBoot项目中,利用AOP打印接口的入参和出参日志,以及执行时间,还是比较快捷的。

  • 引入依赖:引入AOP依赖

    
                org.springframework.boot
                spring-boot-starter-aop
            
  • 自定义注解:自定义一个注解作为切点

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD})
    @Documented
    public @interface WebLog {
    }
  • 配置AOP切

锐单商城拥有海量元器件数据手册IC替代型号,打造电子元器件IC百科大全!

相关文章