【我在拉勾训练营学技术】绝对值得你收藏的 spring 笔记

2021-03-03 13:17:52 浏览数 (1)

前言

文章内容输出来源:拉勾教育Java高薪训练营;

spring概述

spring 优势

1、低耦合高内聚

2、AOP编程支持

3、声明式事物支持

4、支持测试

5、方便继承其他优秀框架

核心结构

image-20200530103942091

spring的核心容器 就是最下面的core,context,Bean 。为上层架构提供服务。

AOP 是利用动态代理实现的面向切面编程,用来抽取不同业务中相同的代码,减少代码重复和降低程序见的耦合度。

JDBC 封装的对数据库的操作,和可以轻易整合ORM框架,并且提供对数据库事物的声明试管理。

WEB 容器主要是 封装Servlet,并且可以轻易的整合SpringMVC 框架,实现传统的MVC 架构。开发web 应用项目。

TEST 是方便我们对各个模块进行单元测试。

核心思想

IOC

IOC :inversion of control(控制翻转)。也就是说我们找对象的时候,不在是原来需要new 一个对象出来,而是告诉IOC 控制器,我想要这个对象,那么IOC 控制器就会给你分配这个对象,你直接是用就好了。就好比我们找媳妇,我们告诉婚介所我想要一个怎样的对象,婚介所中有大量的资源,就会根据你的条件,找到你想要的对象。然后安排你们见面。你这用负责和他给你找到的对象谈恋爱就完了,其他不用管,婚介所会为你擦屁股料后事。

DI

DI:dependancy injection (依赖注入),依赖注入和IOC描述的是同一件事情,表示IOC 容器根据这个类需要的依赖对象,给这个类注入对应的对象。可以看到依赖注入是站在IOC 容器的角度。而IIOC 控制翻转是站在类或者Bean 的角度,将控制创建对象的权利交给了IOC容器。

AOP

AOP Aspect oridented programming(面向切面编程)。就是为了将不同业务间的相同业务带来抽离出来做成一个切面。然后通过动态代理的的方式将这部分相同的业务代码注入到原业务逻辑中,从而实现原业务逻辑的增强。降低了代码见的重复和代码间的耦合度。

手写 IOC AOP

设计模式

工厂模式

工厂模式就是通过工厂来创建想要的对象,而不是自己去创建对象。这样降低了代码间的耦合度。比如一个服饰工厂可以生产衣服,裤子,鞋子,袜子,帽子等等。我们想要用衣服,不用自己来造了,而是和工厂说你想要什么,那工厂就给你生产什么。这就是工厂模式。主要是将创建对象的实例交给工厂类来完成,并进行管理。

工厂分为简单工厂模式、工厂方法模式和抽象工厂模式。

简单工厂模式:是工厂模式最简单的一种,实例有工厂直接创建。也就是上面的例子中,你想要衣服,那这个服饰工厂就给你做一件衣服给你。

工厂方法模式 :就是工厂本身不进行实体对象的创建,而是通过对应的下游工厂来创建然后在返回,有点总工厂子工厂的意思,总工厂管理着所有的子工厂,每个字工厂只生产指定的商品,当通知总部想要什么东西时,总部就通知对应的子工厂生产,拿到产品后再返回客户。

抽象工厂:就是一个抽象工厂类,里面只是声明可以实现哪些对象,但是具体的实现就交给具体的工厂完成。抽象工厂就好比包皮公司,它告诉你他可生产服饰,也就可以生产食品。那比如你想要衣服,它就给你一个服饰工厂的联系方式,你通过这个服饰工厂来获取到衣服。想要辣条,那他就给你推一个食品公司的联系方式,让这个食品公司给你做。

工厂方法模式和抽象工厂模式的区别:在于工厂方法模式主要是生产某一类商品。而抽象工厂,我不关你什么实现,只要你说你能做这个商品,我就可以为你代言。

单例模式

单例模式表示这个对象只会被创建一次。所以要下面特性:

1、私有的构造方法

2、静态的成员变量

3、共有的get 方法,可以让其他对象获取到唯一的实例。

单例模式分饿汉懒汉双重检验,线程安全的懒汉式,静态内部流等。其实都是实现方式不同罢了,一样的满足上面特性。

饿汉式

代码语言:javascript复制
peublic class Singleton{
 //静态变量
 private static Singleton sing=new Singleton();
 //构造方法
 private Singleton(){
 }
 //外部调用方法
 public  static Singleton getInstance(){
  return sing;
 }
}

优点:这种方式在类加载时就完成初始化了,获取对象但速度快。避免多线程但同步问题。
缺点:类加载较慢。没有达到懒加载的效果,如果从始至终都未使用果这个实例,这会造成内存的浪费。

懒汉式

代码语言:javascript复制
public class Singleton{
 private static Singleton sing;
 private Singleton(){
 } 
 private static Singleton getInstance(){
  if(sing==null){
   sing= new Singleton();
  }
  return sing;
 }
}

优点:相对于饿汉,节省资源。
缺点:第一次加载比较慢,且多线程会出现问题。

线程安全的懒汉式

代码语言:javascript复制
public class Singleton{
 private Singleton(){
 }
 
 private Static Singleton sing;
 
 public static synchronized Singleton getInstance(){
  if(sing==null){
   sing=new Singleton();
  }
  return sing;
 }
 
}
优点:多线程能够很好的使用
缺点:每次代用getInstance方法都会进行同步操作,造成不必要的同步开销。毕竟大部分时候还是用不到同步的。

双重校验锁

代码语言:javascript复制
public class Singleton(){
 private Singleton(){}
 
 private volalite static Singleton sing;

 public Singleton getInstance(){
  if(sing==null){
   synchronized(Singleton.calss){
    if(sing==null){
     sing= new Singleton();
    }
   }
  }
  return sing;
 }

}

优点:线程安全,避免多余的线程同步,节省资源
缺点:第一次实例比较慢,并且在高并发的情况下可能出现失效

静态内部类

代码语言:javascript复制
public calss Singleton(){
 private Singleton(){}
 private static class SingletonInner(){
  private static final Singleton sing=new Singleton();
 }
 
 public Singleton getInstance(){
  return SingletonInner.sing;
 }
}

第一次加载 Singleton 类时并不会初始化 sInstance。 只有在第一次调用 getInstance 方法时虚拟机才会加载 SingletonHolder 并初始化 sInstance。

优点:资源利用率高,线程安全,避免多余同步。

代理模式

代理模式就是我们想要执行的方法通过代理对象来完成,就好比我们要抢票回家,我们不想自己盯着抢,所以就找代理帮我们抢从而达到目的。代理模式分为静态代理和动态代理。

静态代理:就是具体的代理类,在编译阶段就知道这个代理能做什么事情。就好比抢票的代理一样,它只能做抢票这个代理,而不能代你抢钱哈哈。

动态代理:动态代理就不同了,它在编译阶段也没有具体的实现。而是在程序运行期间根据JVM的反射机制动态生成的。比较出名的就JDK 动态代理 和cglib 动态代理。

JDK 动态代理:主要实现InvocationHandle 接口并实现其 invoke 方法。

代码语言:javascript复制
Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object result = null;

                        // 前置增强
                       ....
                        // 调用原有业务逻辑
                        result = method.invoke(obj,args);
                        //后置增强
                        ...

                        return result;
                    }
                })

cglib 动态代理:需要引入其依赖,使用方法和JDK动态代理差不多。实现MethodInterceptor 接口并实现 intercept 方法。

代码语言:javascript复制
public Object getCglibProxy(Object obj) {
        return  Enhancer.create(obj.getClass(), new MethodInterceptor() {
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                Object result = null;
                //前置增强
                ...
                result = method.invoke(obj,objects);
                //后置增强
                ...
                return result;
            }
        });
    }

Spring IOC 应用

BeanFactory 和ApplicationContext 的区别

beanFactory 是spring 的顶级接口,提供一些基础的功能和规范,而ApplicationContext 是集成 beanFactory 的接口的接口,增加了一些功能,spring 中个更多的功能都是通过applicationContext 接口来实现的。

IOC的启动方式

java 环境:

ClassPathXmlApplicationContext :从类的根路径加载配置文件(相对路径)。

FileSystemXmlApplicationContext: 从磁盘路径加载配置文件(绝对路径)。

AnnotationConfigApplicationContext:纯注解模式下启动spring 容器。

web 环境:

配置web.xml 启动。指定启动需要加载的配置文件。

代码语言:javascript复制
<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>classpath:applicationContext.xml</param-value>
 </context-param>
 <!--使⽤监听器启动Spring的IOC容器-->
 <listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener
  </listener-class>
 </listener>

配置web.xml 启动。指定启动时需要加载的配置类。

代码语言:javascript复制
<!--告诉ContextloaderListener知道我们使⽤注解的⽅式启动ioc容器-->
 <context-param>
  <param-name>contextClass</param-name>
  <param-value>org.springframework.web.context.support.AnnotationConfigWebAppli
cationContext
 </param-value>
 </context-param>
 <!--配置启动类的全限定类名-->
 <context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>com.lagou.edu.SpringConfig</param-value>
 </context-param>
 <listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener
  </listener-class>
 </listener>
 

实例化 Bean 的三种方式

1、使用无参的构造方法。在默认情况下通过反射调用对象的无参构造函数来实例化。

2、使用静态方法注入。

代码语言:javascript复制
<!--使⽤静态⽅法创建对象的配置⽅式-->
<bean id="userService" class="com.lagou.factory.BeanFactory"
 factory-method="getTransferService"></bean>

3、使用实例对象注入。

代码语言:javascript复制
<!--使⽤实例⽅法创建对象的配置⽅式-->
<bean id="beanFactory"
class="com.lagou.factory.instancemethod.BeanFactory"></bean> <bean id="transferService" factory-bean="beanFactory" factorymethod="getTransferService"></bean>

Bean 的作用范围已经生命周期

Bean 在创建的时候可以通过scope 来选择作用范围,模式是单例的。可以选择多例模式。

单例模式(singleton): 在创建容器时,也就是项目启动初始化阶段,对象就会被创建。只有当容器销毁时,对象才会被销毁,也就是说和IOC容器的生命周期是一样的。

多例模式(prototype): 在需要使用这个对象时,就会创建新的对象实例。ioc 容器只负责创建 对象,不负责销毁。当对象一直被使用时,就会存活,当对象没有使用时,就会等待JVM 垃圾回收销毁。

依赖注入的三种方式

1、setter 注入

2、构造函数注入

3、注解注入:@Autowired

Autowired 和Resource 注解的区别

1、autowired 是spring 提供的注解,@Resource是javaee 提供的注解。

2、Autowired 采用的是按类型注入。当一个类有多个Bean的时候需要配合@Qualifier 来指定唯一的Bean。而@Resource 默认安装byName 自动注入。

3、@Resource 可以执行 name 和type .可以通过type 注入,也可以通过name 来注入。

延迟加载

Bean 对象的创建的延时加载。对于singleton 的单例Bean,默认为立即加载。

代码语言:javascript复制
单个Bean:xml :配置lazy-init=true 为开启延迟加载,默认为false.
注解:@lazy

整体Bean:在beans 标签中增加: default-lazy-init 

延迟加载,在要使用这个Bean的时候才开始创建。可以在applicationContext --beanFactory --singletonObjects 用来存放单例bean.使用的存储结构为currentHashMap.

FactoryBean 和BeanFactory

BeanFactory 接口是容器的顶级接口,定义容器的基础行为。负责创建和管理Bean的一个工厂。Beanfactory 有两种,一种是普通的Bean,一种是工厂Bean(factoryBean)。

factoryBean 可以生成一个Bean 实例给我们 ,我们可以借助factoryBean 来自定义Bean的创建过程。

后置处理器

spring 提供了两种后置处理器的接口。BeanPostProcessor 和BeanFactoryPostProcessor.

在BeanFactory 初始化之后,可以实现BeanFactoryPostProcessor 进行后置处理做一些事情。

在Bean对象实例化但是没有走完加载的整个流程,可以使用BeanPostProcessor 进行后置处理。

BeanPostProcessor 是针对Bean的,可以获取到当前Bean,并做处理。

代码语言:javascript复制
public interface BeanPostProcessor {
 @Nullable
 default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
  return bean;
 }
 @Nullable
 default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
  return bean;
 }

}

该接⼝提供了两个⽅法,分别在Bean的初始化⽅法前和初始化⽅法后执⾏,具体这个初始化⽅法指的是什么⽅法,类似我们在定义bean时,定义了init-method所指定的⽅法定义⼀个类实现了BeanPostProcessor,默认是会对整个Spring容器中所有的bean进⾏处理。如果要对具体的某个bean处理,可以通过⽅法参数判断,两个类型参数分别为Object和String,第⼀个参数是每个bean的实例,第⼆个参数是每个bean的name或者id属性的值。所以我们可以通过第⼆个参数,来判断我们将要处理的具体的bean。

BeanFactoryPostProcessor 是针对 beanFactory 的典型应⽤:PropertyPlaceholderConfifigurer。用来替换配置文件中外部文件的引用值替换,比如applicationContext.xml 引用jdbc.properties 获取其实的数据库配置信息。

代码语言:javascript复制
@FunctionalInterface
public interface BeanFactoryPostProcessor {
 void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;

}

//需要实现它的方法
@Override
 public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
 System.out.println(beanFactory);
 BeanDefinition beanDefinition = beanFactory.getBeanDefinition("lagouBean");
  String beanClassName = beanDefinition.getBeanClassName();
 }

可以获取到beanfactory ,从而通过beanFactory.getBeanDefinition()获取到BeanDefinition对象,从而获取到bean 对Bean属性进行修改。

注意:调⽤ BeanFactoryPostProcessor ⽅法时,这时候bean还没有实例化,此时 bean 刚被解析成BeanDefifinition对象

Spring IOC 源码深度剖析

  • 好处:提⾼培养代码架构思维、深⼊理解框架
  • 原则
    • 定焦原则:抓主线
    • 宏观原则:站在上帝视⻆,关注源码结构和业务流程(淡化具体某⾏代码的编写细节)
  • 读源码的⽅法和技巧
    • 断点(观察调⽤栈)
    • 反调(Find Usages)
    • 经验(spring框架中doXXX,做具体处理的地⽅)
  • Spring源码构建
    • ⼯程—>tasks—>compileTestJava
    • 下载源码(github)
    • 安装gradle 5.6.3(类似于maven) Idea 2019.1 Jdk 11.0.5
    • 导⼊(耗费⼀定时间)
    • 编译⼯程(顺序:core-oxm-context-beans-aspects-aop)

IOC 容器初始化 主体流程

  • prepareRefresh()刷新前预处理。主要是设置启动时间,以及初始化配置文件中的占位符,以及校验配置信息是否正确。
  • 获取Beanfactory。
  • Beanfactory 的准备工作,对BeanFactory 的一些属性进行配置。比如context 的类加载器
  • BeanFactory 准备工作完成后,进行的后置处理工作。是一个钩子函数。
  • 注册BeanPostProcessor(Bean的后置处理器),在创建Bean的前后执行
  • 初始化MessageSource 组件,并将信息加入allpication.singletonObjects 中。
  • 初始化事件派发器,并加入到singletonObjects 中
  • onRefresh().子类重写这个方法,在容器刷新时可以自定义逻辑。
  • 注册应用监听器。ApplicationListener 接口监听器
  • 初始化所有没有设置延时加载的Bean
  • 完成context 的刷新,发布事件。

AbstractApplicationContext 中主要的refresh() 方法:

代码语言:javascript复制
public void refresh() throws BeansException, IllegalStateException {
  //加对象锁,避免refresh 和close 同时调用
  synchronized (this.startupShutdownMonitor) {
   /**
    * 第⼀步:刷新前的预处理
    * 刷新的预处理
    * 设置系统启动时间
    * 验证环境信息是否包含必要属性
    */
   prepareRefresh();

   /**
    * 第⼆步:
    *  获取BeanFactory;默认实现是DefaultListableBeanFactory
    *  加载BeanDefinition 并注册到 BeanDefitionRegistry
    */
   ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

   /**
    * 第三步:BeanFactory的预准备⼯作(BeanFactory进⾏⼀些设置,⽐如context的类加
    * 载器等)
    */
   prepareBeanFactory(beanFactory);

   try {
   
    /**
     * 第四步:BeanFactory准备⼯作完成后进⾏的后置处理⼯作
     */
    postProcessBeanFactory(beanFactory);

    /**
     * 第五步:实例化并调⽤实现了 BeanFactoryPostProcessor 接⼝的Bean
     */
    invokeBeanFactoryPostProcessors(beanFactory);

    /**
     *  第六步:注册BeanPostProcessor(Bean的后置处理器),在创建bean的前后等执
     */
    registerBeanPostProcessors(beanFactory);

    /**
     * 第七步:初始化MessageSource组件(做国际化功能;消息绑定,消息解析);
     */
    initMessageSource();

    /**
     *  第⼋步:初始化事件派发器
     */
    initApplicationEventMulticaster();

    /**
     * 第九步:⼦类重写这个⽅法,在容器刷新的时候可以⾃定义逻辑
     */
    onRefresh();

    /**
     *  第⼗步:注册应⽤的监听器。就是注册实现了ApplicationListener接⼝的监听器
     * bean
     */
    registerListeners();

    /**
     * 第⼗⼀步:
     *  初始化所有剩下的⾮懒加载的单例bean
     *  初始化创建⾮懒加载⽅式的单例Bean实例(未设置属性)
     *  填充属性
     *  初始化⽅法调⽤(⽐如调⽤afterPropertiesSet⽅法、init-method⽅法)
     *  调⽤BeanPostProcessor(后置处理器)对实例bean进⾏后置处
     */
    finishBeanFactoryInitialization(beanFactory);
    
    /**
     * 第⼗⼆步:
     *  完成context的刷新。主要是调⽤LifecycleProcessor的onRefresh()⽅法,并且发布事
     * 件 (ContextRefreshedEvent)
     */
    finishRefresh();
   }
   catch (BeansException ex) {
    ...
   }
   finally {
    resetCommonCaches();
   }
  }
 }

创建 BeanFactory 的流程

通过整体流程,在refresh() 方法中第二步,获取beanFactory。获取的子流程发生在obtainFreshBeanFactory() 方法中。

代码语言:javascript复制
AbstractRefreshableApplicationContextprotected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
  //刷新Beanfactory,调用AbstractRefreshableApplicationContext
  refreshBeanFactory();
  //获取Beanfactory
  return getBeanFactory();
 }
代码语言:javascript复制
//AbstractRefreshableApplicationContext.java
protected final void refreshBeanFactory() throws BeansException {
  //判断BeanFactory 中是否存在Bean。存在就销毁Bean 并关闭beanfactory
  if (hasBeanFactory()) {
   destroyBeans();
   closeBeanFactory();
  }
  try {
   // 创建一个DefaultListableBeanFactory
   DefaultListableBeanFactory beanFactory = createBeanFactory();
   //序列化beanFactory的id
   beanFactory.setSerializationId(getId());

   //自定义Bean工厂的一些属性
   customizeBeanFactory(beanFactory);
   //加载应用的beanDefitions
   loadBeanDefinitions(beanFactory);
   synchronized (this.beanFactoryMonitor) {
    this.beanFactory = beanFactory;
   }
  }
  catch (IOException ex) {
   throw new ApplicationContextException("I/O error parsing bean definition source for "   getDisplayName(), ex);
  }
 }

image-20200601180122219

BeanDefifinition 加载解析注册 流程

Resource定位:指对BeanDefifinition的资源定位过程。通俗讲就是找到定义 Javabean 信息的XML⽂

件,并将其封装成Resource对象。

BeanDefifinition载⼊ :把⽤户定义好的Javabean表示为IoC容器内部的数据结构,这个容器内部的数

据结构就是BeanDefifinition

过程

  • 判断BeanFactory 中是否存在Bean。存在就销毁Bean 并关闭beanfactory
  • 创建一个DefaultListableBeanFactory
  • 序列化beanFactory的id
  • 自定义beanFactory的一些属性
  • 加载应用的beanDefinitions
    • 获取已有的BeanDefinition对象的个数
    • 注册BeanDefinition
    • 返回新注册的BeanDefinition的数量
    • 读取XML 信息 ,将 XML 信息保存到Document对象中。
    • 解析document 对象,封装BeanDefinition 对象并进行注册

所谓的注册就是把封装的 XML 中定义的 Bean信息封装为 BeanDefifinition 对象之后放⼊⼀个CurrentHashMap中,BeanFactory 是以 CurrentHashMap的结构组织这些 BeanDefifinition的。

image-20200602200851638

Bean创建流程

Bean的创建流程发生在AbstractApplicationContext.refresh()⽅法 finishBeanFactoryInitialization(beanFactory) 处。

做了如下操作:

代码语言:javascript复制
*  初始化所有剩下的⾮懒加载的单例bean
*  初始化创建⾮懒加载⽅式的单例Bean实例(未设置属性)
*  填充属性
*  初始化⽅法调⽤(⽐如调⽤afterPropertiesSet⽅法、init-method⽅法)
*  调⽤BeanPostProcessor(后置处理器)对实例bean进⾏后置处

springBean 的生命周期

1、根据配置情况调用 Bean的构造方法或者工厂方法实例化Bean

2、利用依赖注入完成Bean的所以属性值的配置注入

3、如果Bean 实现了BeanNameAware 接口,则spring 调用Bean的setBeanName() 传入当前Bean的id

代码语言:javascript复制
@Override
 public void setBeanName(String name) {
  System.out.println(name);
 }

4、如果Bean实现了BeanFactoryAware 接口,调用setBeanFactory() 方法传入当前工厂实例的引用

代码语言:javascript复制
@Override
 public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
  System.out.println(beanFactory);
 }

5、如果Bean 实现了ApplicationContextAware 接口,通过调用setApplicationContext 传入当前applicationContext 实例的引用。

代码语言:javascript复制
@Override
 public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
  System.out.println(applicationContext);
 }

6、如果BeanPostProcessor 和Bean 关联,则Spring 将调用改接口的预初始化方法。postProcessBeforeInitialization() 是前置处理的方法,Spring的AOP就是利用它实现的。

7、如果Bean 实现了InitializingBean 接口,需要实现afterPropertiesSet 方法

代码语言:javascript复制
@Override
 public void afterPropertiesSet() throws Exception {
  System.out.println("afterPropertiesSet");
 }

8、如果在配置文件中通过init-method 属性指定了初始化方法。则的调用该方法。

9、如果BeanPostProcessor 和Bean 关联,则Spring 将调用该接口的初始化方法postProcessAfterInitialization().此时Bean可以被应用系统使用。

10、如果在Bean标签中指定了Bean的作用范围的scope="singleton" 则将改Bean 方法singletonObjects的缓存池中

11、如果Bean 实现了DisposableBean 接口,则spring会调用destory() 方法 将Spring中的Bean销毁。

image-20200603092052274

延时加载

在初始化阶段,会将所有的xml 中的Bean信息都解析到BeanDefinition 对象中保存到hashmap 中等待初始化。然后在refresh() 中的finishBeanFactoryInitialization。

image-20200603092653388

然后到preInstantiateSingletons() 方法。

image-20200603092735506

也就是说在Bean实例化阶段只会实例化非抽象的,单例的,非延迟加载的Bean。并将Bean放到缓存池singletonObjects 中,然后在取用的时候直接从singletonObjects 中取用。

而设置了延时加载的Bean,在调用的时候。在getBean() 时才会初始化,最终方法缓存中。

循环依赖

循环依赖,就是在A 对象实例化时依赖B 对象,在实例化B 对象时依赖A对象。那么Spring是怎么解决这个问题的,只有当通过setter 方式注入或者注解方式注入的时候,才可以解决循环依赖的问题,而通过构造器方式注入,不能解决循环依赖。因为通过构造器实例化一个对象,就直接实例化完成了,没有办法在实例化的过程中做到处理。

image-20200603094132061

spring 解决循环依赖的方法:

利用三级缓存:

代码语言:javascript复制
一级:singletonObjects
二级:earlySingletonObjects
三级:singletonFactories

image-20200603145753189

流程:

1、先触发所以非延时加载的Bean实例化。

image-20200603133419108

2、是 getBean 的时候会先从一级缓存池中检查是否存在,并且判断时候正在创建。这个时候 aBean 还没有开始创建,所以是 false,所以直接返回 null.

3、返回的是 null 就说明没有创建,所以就开始创建。调用 createBean() 来创建Bean

image-20200603134318256

4、createBean 实际调用 doCreateBean() 来创建 Bean

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3h5Pp0EM-1591430719224)(http://zlf.quellanan.xyz/image-20200603135004874.png)]

5、在 doCreateBean() 方法中,会先通过构造方法创建 Bean 实例,但是这个时候Bean还没有设置属性。

image-20200603135307089

6、判断是否需要提前暴露,需要的话就将 Bean 放入三级缓存中。

image-20200603135558110

image-20200603135825084

7、开始为 Abean 对象添加属性。调用 populateBean() 方法。

image-20200603140101600

8、在 populateBean() 方法中调用 applyPropertyValues() 处理属性依赖。

image-20200603140515401

9、通过一系列的调用,在 resolveReference() 中要冲 beanfactory 中获取 bBean 的实例为 aBean 填充属性。

image-20200603140911230

10、在获取 bBean 的时候发现,发现 bBean,发现 bBean 没有创建,就开始创建。和 aBean 创建的步骤相同。只到为 bBean填充属性的时候,发现依赖 aBean 。所有就从 beanFactory.getBean() 中获取 aBean。

11、在获取 aBean 的时候从缓存中查找,一级缓存没有,但是发现 aBean正在创建,所以就从二级缓存中取,返现没有,接着就到三级缓存中取,取到了,做些处理,然后将 aBean 加入到了二级缓存,并在三级缓存中删除aBean。此时二级缓存 earlySingletonObjects 中只有 aBean。而三级缓存 singletonFactories 中只有 bBean.

12、获取到 aBean 后,直接返回 Bean

image-20200603142928573

13、那么就回到了为 bBean 填充属性 aBean 的时候,获取 aBean 的地方。

image-20200603143218814

14、这样 bBean 就完成了属性注入,后面的就是其他操作了,之到 bBean 完成实例化了。

image-20200603143446221

15、bBean 在完成实例化之后,会加入到一级缓存池中。并充二级和三级缓存中删除。

image-20200603144353082

16、这个时候就回到了 aBean 填充 bBean ,从 beanFactory.getBean() 中拿到了 bBean。所以接下来完成 aBean 的实例化流程。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6rr43IkF-1591430719245)(http://zlf.quellanan.xyz/image-20200603144729814.png)]

17、aBean 和 bBean 一样,将实例化完成的 aBean 加入到一级缓存,并从二级和三级缓存中删除掉。

到此循环依赖的问题就解决了。

类和方法的大致跳转流程:

  • DefaultListableBeanFactory#preInstantiateSingletons//入口
    • DefaultSingletonBeanRegistry#getSingleton()//判断是否创建,没有继续
    • DefaultSingletonBeanRegistry#getSingleton()调用createBean() 创建
    • DefaultSingletonBeanRegistry#addSingletonFactory()//加入三级缓存
    • AbstractAutowireCapableBeanFactory#populateBean()//填充属性
    • 完成实例化
    • 将Bean A 加入一级缓存,从二级和三级缓存中删除。
    • BeanDefinitionValueResolver#resolveValueIfNecessary
    • BeanDefinitionValueResolver#resolveReference//发现BeanA依赖BeanB
    • AbstractBeanFactory#getBean()//获取BeanB
    • AbstractBeanFactory#doGetBean()//创建Bean B
    • ......重复BeanA 的操作
    • DefaultSingletonBeanRegistry#getSingleton()//从三级缓存中取到了BeanA,加入二级缓存
    • Bean B 完成了实例化。
    • DefaultSingletonBeanRegistry#addSingleton//添加到一级缓存中。
    • AbstractAutowireCapableBeanFactory#applyPropertyValues
    • Bean A填充了 属性
    • AbstractAutowireCapableBeanFactory#createBean()
    • AbstractAutowireCapableBeanFactory#doCreateBean()
    • AbstractBeanFactory#getBean()//创建BeanA
    • AbstractBeanFactory#doGetBean()//创建Bean A

时序图:

循环依赖加载时序图

Spring AOP 应用

AOP的概念

连接点:方法开始时,结束时,正常运行完毕时,方法异常时等这些特殊的时机点,我们称之为连接点,项目中每个方法都有连接点,连接点是候选的点。

切入点:指定AOP 思想想要影响的具体方法有哪些。

Advice增强

1、指的是横切逻辑

2、方位点,在某一些连接点上加入横切逻辑,那么这些连接点就是方位点,描述具体的切入时机。

Aspect 切面 是切入点加增强

Aspect切⾯= 切⼊点 增强

= 切⼊点(锁定⽅法) ⽅位点(锁定⽅法中的特殊时机) 横切逻辑

这些定义主要是为了锁定在哪个地方插入横切逻辑代码

spring 实现AOP 思想主要是通过动态代理。

动态代理有JDK 动态代理和cglib 动态代理。

  • jdk 动态代理需要代理对象实现接口
  • cglib 动态代理不用实现代理接口

默认情况下,spring 会根据被代理的对象是否实现了接口来选择jdk动态代理还是cglib 动态代理。也可以通过配置强制spring 是用哪种代理。

AOP代理方式

有三种

  • 纯注解
  • XML 注解
  • 纯XML

XML

配置:

代码语言:javascript复制
<beans xmlns="
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd
">


<!--aspect   =    切入点(锁定方法)   方位点(锁定方法中的特殊时机)  横切逻辑 -->
    <aop:config>
        <aop:aspect id="logAspect" ref="logUtils">

            <!--切入点锁定我们感兴趣的方法,使用aspectj语法表达式-->
            <!--<aop:pointcut id="pt1" expression="execution(* *..*.*(..))"/>-->
            <aop:pointcut id="pt1" expression="execution(* com.lagou.edu.service.impl.TransferServiceImpl.*(..))"/>


            <!--方位信息,pointcut-ref关联切入点-->
            <!--aop:before前置通知/增强-->
            <aop:before method="beforeMethod" pointcut-ref="pt1"/>
            <!--aop:after,最终通知,无论如何都执行-->
            <!--aop:after-returnning,正常执行通知-->
            <aop:around method="arroundMethod" pointcut-ref="pt1"/>

        </aop:aspect>
    </aop:config>

代码:

代码语言:javascript复制
public class LogUtils {

    /**
     * 业务逻辑开始之前执行
     */
    public void beforeMethod(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i  ) {
            Object arg = args[i];
            System.out.println(arg);
        }
        System.out.println("业务逻辑开始执行之前执行.......");
    }

    /**
     * 业务逻辑结束时执行(无论异常与否)
     */
    public void afterMethod() {
        System.out.println("业务逻辑结束时执行,无论异常与否都执行.......");
    }

    /**
     * 异常时时执行
     */
    public void exceptionMethod() {
        System.out.println("异常时执行.......");
    }

    /**
     * 业务逻辑正常时执行
     */
    public void successMethod(Object retVal) {
        System.out.println("业务逻辑正常时执行.......");
    }

    /**
     * 环绕通知
     *
     */
    public Object arroundMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("环绕通知中的beforemethod....");

        Object result = null;
        try{
            // 控制原有业务逻辑是否执行
            // result = proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
        }catch(Exception e) {
            System.out.println("环绕通知中的exceptionmethod....");
        }finally {
            System.out.println("环绕通知中的after method....");
        }

        return result;
    }

}

XML 注解

xml 中增加注解驱动

代码语言:javascript复制
<!--开启aop注解驱动
    proxy-target-class:true强制使用cglib
-->
<aop:aspectj-autoproxy/>

代码:

代码语言:javascript复制
@Component
@Aspect
public class LogUtils {


    @Pointcut("execution(* com.lagou.edu.service.impl.TransferServiceImpl.*(..))")
    public void pt1(){

    }


    /**
     * 业务逻辑开始之前执行
     */
    @Before("pt1()")
    public void beforeMethod(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i  ) {
            Object arg = args[i];
            System.out.println(arg);
        }
        System.out.println("业务逻辑开始执行之前执行.......");
    }


    /**
     * 业务逻辑结束时执行(无论异常与否)
     */
    @After("pt1()")
    public void afterMethod() {
        System.out.println("业务逻辑结束时执行,无论异常与否都执行.......");
    }


    /**
     * 异常时时执行
     */
    @AfterThrowing("pt1()")
    public void exceptionMethod() {
        System.out.println("异常时执行.......");
    }


    /**
     * 业务逻辑正常时执行
     */
    @AfterReturning(value = "pt1()",returning = "retVal")
    public void successMethod(Object retVal) {
        System.out.println("业务逻辑正常时执行.......");
    }

    /**
     * 环绕通知
     *
     */
    @Around("pt1()")
    public Object arroundMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("环绕通知中的beforemethod....");

        Object result = null;
        try{
            // 控制原有业务逻辑是否执行
            // result = proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
        }catch(Exception e) {
            System.out.println("环绕通知中的exceptionmethod....");
        }finally {
            System.out.println("环绕通知中的after method....");
        }

        return result;
    }
}

纯注解

替换配置文件中开启的功能的配置。

所以在config 中增加配置:

代码语言:javascript复制
@EnableAspectJAutoProxy 

image-20200603194135236

web.xml

代码语言:javascript复制

<web-app>
  <display-name>Archetype Created Web Application</display-name>

  <!--配置Spring ioc容器的配置类-->
  <context-param>
    <param-name>contextClass</param-name>
    <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
  </context-param>

  <!--配置Spring ioc容器的配置文件-->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <!--<param-value>classpath:applicationContext.xml</param-value>-->
    <param-value>com.lagou.edu.config.SpringConfig</param-value>
  </context-param>
  <!--使用监听器启动Spring的IOC容器-->
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
</web-app>

Spring 声明式事物支持

事务

事务一般是指数据库事务,表示一组逻辑处理,这一组处理中所有逻辑单元要么都全部成功,要么都全部失败。确保数据的一致性和安全性。

事务的特性

事物的四大特性:ACID ,一致性,原子性,隔离性,持续性

  • 原⼦性(Atomicity):表示事务的操作是原子的,不可分割的最小单元。要么全部执行,要么全部不执行。
  • ⼀致性(Consistency):表示事务发生钱前后,数据库的状态是一致的,比如a 和账户减100,b,账户加100,确保数据的和是不变的。
  • 隔离性(Isolation):表示事务的执行是相互隔离的,一个事务的执行不能影响另一个事务。
  • 持久性(Durability):事务操作改变数据库的数据是永久性的,不可以回退的,也就是说事务一旦提交,那数据库的数据就会修改。

事务的隔离级别

事务有四大隔离级别,读未提交,读已提交,可重复读,串行化

  • 读未提交:一个事务可以读取到另个一事务没有提交操作的数据。会产生脏读。隔离级别最低
  • 读已提交:一个事务只能读取到其他事务提交后的数据。会存在不可重复读问题,隔离级别第二。不可重复读,主要是一个事务在前后两次查询期间,另一个事务提交了,导致前后读取的两次结果不一致。
  • 可重复读:一个事务在执行过程中,读取到的数据都是一样的。解决上面不可重复的读的问题。隔离级别第三。但是会产生幻读,幻读是前后两次读取获取到的数据条数不一致。在这期间,其他事务执行了删除获取增加数据的操作。
  • 串行化:隔离级别最高,线程安全,一个事务在读取的时候,会对表加锁,其他事务处于等待操作。

mysql 默认的隔离级别为可重复度,但是mysql 解决的幻读问题,主要也是利用锁机制,不过不是锁表,而是锁定行。

事务的传播行为

事务往往在service层进⾏控制,如果出现service层⽅法A调⽤了另外⼀个service层⽅法B,A和B⽅法本

身都已经被添加了事务控制,那么A调⽤B的时候,就需要进⾏事务的⼀些协商,这就叫做事务的传播⾏

为。

A调⽤B,我们站在B的⻆度来观察来定义事务的传播⾏为

PROPAGATION_REQUIRED

如果当前没有事务,就新建⼀个事务,如果已经存在⼀个事务中,加⼊到这个事务中。这是最常⻅的选择

PROPAGATION_SUPPORTS

⽀持当前事务,如果当前没有事务,就以⾮事务⽅式执⾏

PROPAGATION_MANDATORY

使⽤当前的事务,如果当前没有事务,就抛出异常。

PROPAGATION_REQUIRES_NEW

新建事务,如果当前存在事务,把当前事务挂起。

PROPAGATION_NOT_SUPPORTED

以⾮事务⽅式执⾏操作,如果当前存在事务,就把当前事务挂起。

PROPAGATION_NEVER

以⾮事务⽅式执⾏,如果当前存在事务,则抛出异常

PROPAGATION_NESTED

如果当前存在事务,则在嵌套事务内执⾏。如果当前没有事务,则执⾏与PROPAGATION_REQUIRED类似的操作。

代码

声明式事务,增加注解就可以了

代码语言:javascript复制
@EnableTransactionManagement 
@Transactional

Spring AOP 源码深度剖析

手写AOP和IOC 代码讲解

基于XML 模式

环境搭建

创建一个maven 项目,将webapp 目录导入到src/main 目录下。

数据库创建一张bank表以及初始化数据

代码语言:javascript复制
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
  `cardNo` varchar(20) NOT NULL,
  `money` int DEFAULT NULL,
  `name` varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of account
-- ----------------------------
INSERT INTO `account` VALUES ('6029621011000', '1000', '李大雷');
INSERT INTO `account` VALUES ('6029621011001', '1000', '韩梅梅');

代码编写

pojo 实体类

代码语言:javascript复制
public class Account {

    private String cardNo;
    private String name;
    private int money;
    //getter/setter 方法
}

public class Result {

    private String status;
    private String message;
    //getter/setter 方法
}

DAO层

接口:

代码语言:javascript复制
public interface AccountDao {

    Account queryAccountByCardNo(String cardNo) throws Exception;

    int updateAccountByCardNo(Account account) throws Exception;
}

实现类:就是一个转账一个查询 的操作。这里的connectionUtils是直接new 的后续通过set或者注解注入。

代码语言:javascript复制
public class AccountDaoImpl implements AccountDao {

    private ConnectionUtils connectionUtils=new ConnectionUtils() ;

    @Override
    public Account queryAccountByCardNo(String cardNo) throws Exception {
        Connection con = connectionUtils.getCurrentThreadConn();
        String sql = "select * from account where cardNo=?";
        PreparedStatement preparedStatement = con.prepareStatement(sql);
        preparedStatement.setString(1,cardNo);
        ResultSet resultSet = preparedStatement.executeQuery();

        Account account = new Account();
        while(resultSet.next()) {
            account.setCardNo(resultSet.getString("cardNo"));
            account.setName(resultSet.getString("name"));
            account.setMoney(resultSet.getInt("money"));
        }

        resultSet.close();
        preparedStatement.close();
        return account;
    }

    @Override
    public int updateAccountByCardNo(Account account) throws Exception {

        // 从连接池获取连接
        // 改造为:从当前线程当中获取绑定的connection连接
        Connection con = connectionUtils.getCurrentThreadConn();
        String sql = "update account set money=? where cardNo=?";
        PreparedStatement preparedStatement = con.prepareStatement(sql);
        preparedStatement.setInt(1,account.getMoney());
        preparedStatement.setString(2,account.getCardNo());
        int i = preparedStatement.executeUpdate();

        preparedStatement.close();
        return i;
    }
}

service 层

这里一样的调DAO 直接new 的。

代码语言:javascript复制
public interface TransferService {

    void transfer(String fromCardNo, String toCardNo, int money) throws Exception;
}



public class TransferServiceImpl implements TransferService{



    private AccountDao accountDao=new AccountDaoImpl();

    @Override
    public void transfer(String fromCardNo, String toCardNo, int money) throws Exception {

            Account from = accountDao.queryAccountByCardNo(fromCardNo);
            Account to = accountDao.queryAccountByCardNo(toCardNo);

            from.setMoney(from.getMoney()-money);
            to.setMoney(to.getMoney() money);

            accountDao.updateAccountByCardNo(to);
            int c = 1/0;
            accountDao.updateAccountByCardNo(from);
    }
}

servlet 层

这里没有写controller ,应为我们要手写ioc 和Aop 就没哟引入spring的任何东西,spring的controller 实际上就是一个经过封装的servlet 。所以我们这里直接用一个servlet 来代替。代码都很简单就是在 dopost 或者doget 方法中写我们想要执行的代码。

代码语言:javascript复制

@WebServlet(name="transferServlet",urlPatterns = "/transferServlet")
public class TransferServlet extends HttpServlet {


    private TransferService transferService;


    @Override
    public void init() throws ServletException {

        transferService = new TransferServiceImpl();
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req,resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        // 设置请求体的字符编码
        req.setCharacterEncoding("UTF-8");

        String fromCardNo = req.getParameter("fromCardNo");
        String toCardNo = req.getParameter("toCardNo");
        String moneyStr = req.getParameter("money");
        int money = Integer.parseInt(moneyStr);

        Result result = new Result();

        try {

            // 2. 调用service层方法
            transferService.transfer(fromCardNo,toCardNo,money);
            result.setStatus("200");
        } catch (Exception e) {
            e.printStackTrace();
            result.setStatus("201");
            result.setMessage(e.toString());
        }

        // 响应
        resp.setContentType("application/json;charset=utf-8");
        resp.getWriter().print(JsonUtils.object2Json(result));
    }
}

还又一个数据库连接的类

代码语言:javascript复制
public class ConnectionUtils {

    private ThreadLocal<Connection> threadLocal = new ThreadLocal<>(); // 存储当前线程的连接

    /**
     * 从当前线程获取连接
     */
    public Connection getCurrentThreadConn() throws SQLException {
        //判断当前线程中是否已经绑定连接,如果没有绑定,需要从连接池获取一个连接绑定到当前线程
        Connection connection = threadLocal.get();
        if(connection == null) {
            // 从连接池拿连接并绑定到线程
            connection = DruidUtils.getInstance().getConnection();
            // 绑定到当前线程
            threadLocal.set(connection);
        }
        return connection;

    }
}

xml 编写和代码调整

上面是基础代码,也可以说是业务代码。这里我们就不多说,我们主要来分析,我们主要考虑下面几个问题:

1、DAO 层调 ConnectionUtils 连接工具使用的是new .怎么实现IOC的功能实现依赖注入呢?

2、service 层一样的调用DAO 一样的实现IOC

3、转账一个用户扣钱一个用户加钱,怎确保事物一致性?

基于上面这三个问题,我们先用基于XML 的方式来处理。

创建一个beans.xml

内容如下:

代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8" ?>
<!--跟标签beans,里面配置一个又一个的bean子标签,每一个bean子标签都代表一个类的配置-->
<beans>
    <!--id标识对象,class是类的全限定类名-->
    <bean id="accountDao" class="cn.quellanan.dao.impl.AccountDaoImpl">
        <property name="ConnectionUtils" ref="connectionUtils"/>
    </bean>
    <bean id="transferService" class="cn.quellanan.service.impl.TransferServiceImpl">
        <!--set  name 之后锁定到传值的set方法了,通过反射技术可以调用该方法传入对应的值-->
        <property name="AccountDao" ref="accountDao"></property>
    </bean>


    <!--配置新增的三个Bean-->
    <bean id="connectionUtils" class="cn.quellanan.utils.ConnectionUtils"></bean>

    <!--事务管理器-->
    <bean id="transactionManager" class="cn.quellanan.utils.TransactionManager">
        <property name="ConnectionUtils" ref="connectionUtils"/>
    </bean>

    <!--代理对象工厂-->
    <bean id="proxyFactory" class="cn.quellanan.factory.ProxyFactory">
        <property name="TransactionManager" ref="transactionManager"/>
    </bean>
</beans>

可以看到和spring中配置的有点类似。如果要注入一个对象,那就给这个类添加一个Bean标签,这个对象需要注入的属性是用property 标签。

BeanFactory

我们有了这个配置文件,那怎么初始化他们呢?

所以我们来创建一个BeanFactory 来解析初始化他们

代码语言:javascript复制

public class BeanFactory {

/**
     * 任务一:读取解析xml,通过反射技术实例化对象并且存储待用(map集合)
     * 任务二:对外提供获取实例对象的接口(根据id获取)*/



    private static Map<String, Object> map = new HashMap<>();  // 存储对象


    static {
        // 加载xml
        InputStream resourceAsStream = BeanFactory.class.getClassLoader().getResourceAsStream("beans.xml");
        // 解析xml
        SAXReader saxReader = new SAXReader();
        try {
            Document document = saxReader.read(resourceAsStream);
            Element rootElement = document.getRootElement();
            List<Element> beanList = rootElement.selectNodes("//bean");
            for (int i = 0; i < beanList.size(); i  ) {
                Element element =  beanList.get(i);
                // 处理每个bean元素,获取到该元素的id 和 class 属性
                String id = element.attributeValue("id");        // accountDao
                String clazz = element.attributeValue("class");  // com.lagou.edu.dao.impl.JdbcAccountDaoImpl
                // 通过反射技术实例化对象
                Class<?> aClass = Class.forName(clazz);
                Object o = aClass.newInstance();  // 实例化之后的对象
                // 存储到map中待用
                map.put(id,o);

            }

            // 实例化完成之后维护对象的依赖关系,检查哪些对象需要传值进入,根据它的配置,我们传入相应的值
            // 有property子元素的bean就有传值需求
            List<Element> propertyList = rootElement.selectNodes("//property");
            // 解析property,获取父元素
            for (int i = 0; i < propertyList.size(); i  ) {
                Element element =  propertyList.get(i);   //<property name="AccountDao" ref="accountDao"></property>
                String name = element.attributeValue("name");
                String ref = element.attributeValue("ref");

                // 找到当前需要被处理依赖关系的bean
                Element parent = element.getParent();

                // 调用父元素对象的反射功能
                String parentId = parent.attributeValue("id");
                Object parentObject = map.get(parentId);
                // 遍历父对象中的所有方法,找到"set"   name
                Method[] methods = parentObject.getClass().getMethods();
                for (int j = 0; j < methods.length; j  ) {
                    Method method = methods[j];
                    if(method.getName().equalsIgnoreCase("set"   name)) {  // 该方法就是 setAccountDao(AccountDao accountDao)
                        method.invoke(parentObject,map.get(ref));
                    }
                }

                // 把处理之后的parentObject重新放到map中
                map.put(parentId,parentObject);

            }


        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

    }

    // 任务二:对外提供获取实例对象的接口(根据id获取)
    public static Object getBean(String id) {
        return map.get(id);
    }

}

然后将通过new 创建的,去掉,增加一个属性的set 方法。如这种格式:

代码语言:javascript复制
private ConnectionUtils connectionUtils;

public void setConnectionUtils(ConnectionUtils connectionUtils) {
 this.connectionUtils = connectionUtils;
}

事物一致性

创建一个代理对象,通过动态代理将事物逻辑封装起来,然后切入到业务逻辑中,也就是我们spring 的AOP实现。

我们先也一个事物控制的类。

代码语言:javascript复制
public class TransactionManager {

    @Autowired
    private ConnectionUtils connectionUtils;

    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }

    // 开启手动事务控制
    public void beginTransaction() throws SQLException {
        connectionUtils.getCurrentThreadConn().setAutoCommit(false);
        System.out.println("开启事务.....");
    }


    // 提交事务
    public void commit() throws SQLException {
        connectionUtils.getCurrentThreadConn().commit();
        System.out.println("提交事务.....");
    }

    // 回滚事务
    public void rollback() throws SQLException {
        connectionUtils.getCurrentThreadConn().rollback();
        System.out.println("回滚事务.....");
    }
}

主要就三个方法,开启事物,成功提交事物,失败回滚事物。

然后我们创建一个代理类,用来处理代理的对象,这里我们需要对TransferServiceImpl 这个类进行代理。

ProxyFactory

代码语言:javascript复制
public class ProxyFactory {


    @Autowired
    private TransactionManager transactionManager;


    /**
     * Jdk动态代理
     * @param obj  委托对象
     * @return   代理对象
     */
    public Object getJdkProxy(Object obj) {

        // 获取代理对象
        return  Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object result = null;

                        try{
                            // 开启事务(关闭事务的自动提交)
                            transactionManager.beginTransaction();

                            result = method.invoke(obj,args);
                            //提交
                            transactionManager.commit();

                        }catch (Exception e) {
                            e.printStackTrace();
                            // 回滚事务
                            transactionManager.rollback();

                            // 抛出异常便于上层servlet捕获
                            throw e;

                        }

                        return result;
                    }
                });

    }


    /**
     * 使用cglib动态代理生成代理对象
     * @param obj 委托对象
     * @return
     */
    public Object getCglibProxy(Object obj) {
        return  Enhancer.create(obj.getClass(), new MethodInterceptor() {
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                Object result = null;
                try{
                    // 开启事务(关闭事务的自动提交)
                    transactionManager.beginTransaction();

                    result = method.invoke(obj,objects);

                    // 提交事务

                    transactionManager.commit();
                }catch (Exception e) {
                    e.printStackTrace();
                    // 回滚事务
                    transactionManager.rollback();

                    // 抛出异常便于上层servlet捕获
                    throw e;

                }
                return result;
            }
        });
    }
}

有两种实现方式,jdk 动态代理和cglib 动态代理。逻辑都是一样的。到此为止整个就改糙完成了。只要在servlet 中获取到map 中的TransferServiceImpl对象就可以实现ioc 和AOP 的功能。

基于注解的方式

我们不能是用xml 配置了,而是使用注解,所以需要我们实现自定义注解,这里我们仿照spring 来实现几个自定义的注解。

自定义注解

创建一个annotation 包,创建一个自定义注解。

Autowired

代码语言:javascript复制
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
    boolean required() default true;
}

Component

可以设置value值,默认为“”;

代码语言:javascript复制
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Component {
    String value() default "";
}

Service

Component注解一样,主要是不同标识

代码语言:javascript复制
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Service {
    String value() default "";
}

Transactional

事务注解,这里设置一个枚举类型的type 可以指定使用哪种动态代理。

代码语言:javascript复制
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {

    ProxyType type() default ProxyType.DEFAULT;

}

public enum ProxyType {

    //jdk动态代理
    JDK,

    //cglib 动态代理
    CGLIB,

    //默认
    DEFAULT
}

代码修改

创建好我们自定义的注解后,我们需要将这些使用xml 注入的地方改下。

AccountDaoImpl

代码语言:javascript复制
@Service(value = "accountDao")
public class AccountDaoImpl implements AccountDao {

    @Autowired
    private ConnectionUtils connectionUtils;
    ......
}

image-20200605135346431

可以看到增加了一个自定的Autowired 注解和Service 注解,Service 注解可以设置value 值。

TransferServiceImpl

一样添加注解@Service、@Transactional、@Autowired

代码语言:javascript复制
@Service
@Transactional
public class TransferServiceImpl implements TransferService{


    @Autowired
    private AccountDao accountDao;
    ......
    
}

这里注意一点,我们在service层调用的是 AccountDao接口,而实例化应该实例化AccountDaoImpl 。那这个怎么控制呢?就可以通过上面的在AccountDaoImpl 类上的@Service 的value 值来指定。

核心代理

现在有一个问题,我们在这些类上添加了这些注解,但是我们还没有为这些注解赋予能力。所以他们现在还没有什么作用,那我们现在就来给他们赋予至高无上的权利。

核心思想:扫描项目指定包下的所有类,发现类上包含注解我们就对这个类进行对应的处理,然后将处理好呢的对象保存在map 中也就是对应spring 中的singletonObjects一级缓存池。

所以我们创建一个BeanFactoryByAnno来做这件事。

流程:

  • 提供的自定路径获取到包路径名
  • 将这些包路径名保存到set 结合中
  • 遍历set 集合,是否包含@Service,@Component 注解,包含的话,创建Bean 保存到map 集合中
  • 上一步完成后,再次遍历集合,判断是否包含Autowired注解,为属性赋值
  • 上一步完成后,再次遍历集合,判断是否包含Transactional注解,包含的话说明需要创建代理对象来实现。
代码语言:javascript复制
public static void searchClass(String basePack) throws Exception {
        Set<String> classPaths = new HashSet<>();
        //先把包名转换为路径,首先得到项目的classpath
        String classpath = BeanFactoryByAnno.class.getResource("/").getPath();
        //然后把我们的包名basPach转换为路径名
        basePack = basePack.replace(".", File.separator);
        //然后把classpath和basePack合并
        String searchPath = classpath   basePack;
        doPath(new File(searchPath),classPaths);
        //这个时候我们已经得到了指定包下所有的类的绝对路径了。我们现在利用这些绝对路径和java的反射机制得到他们的类对象
        for (String s : classPaths) {
            //把绝对路径转换为全类名
            s = s.replace(classpath.replace("/","\").replaceFirst("\\",""),"").replace("\",".").replace(".class","");
            //处理@Service 注解,实例化对象
            createBeans(s);
        }

        //同理处理@Autowired 注解,为这个属性注入实例化的Bean。
        for (String s : classPaths) {
            //把绝对路径转换为全类名
            s = s.replace(classpath.replace("/","\").replaceFirst("\\",""),"").replace("\",".").replace(".class","");
            //实例化对象
            populateBean(s);
        }

        //如果包含@Transactional注解,说明需要对这个类进行代理
        for (String s : classPaths) {
            //把绝对路径转换为全类名
            s = s.replace(classpath.replace("/","\").replaceFirst("\\",""),"").replace("\",".").replace(".class","");
            //代理对象
            ProxyBean(s);
        }

    }

该方法会得到所有的类,将类的绝对路径写入到classPaths中

代码语言:javascript复制
/**
     * 该方法会得到所有的类,将类的绝对路径写入到classPaths中
     * @param file
     */
    private static void doPath(File file,Set<String> classPaths) {
        if (file.isDirectory()) {//文件夹
            //文件夹我们就递归
            File[] files = file.listFiles();
            for (File f1 : files) {
                doPath(f1,classPaths);
            }
        } else {//标准文件
            //标准文件我们就判断是否是class文件
            if (file.getName().endsWith(".class")) {
                //如果是class文件我们就放入我们的集合中。
                classPaths.add(file.getPath());
            }
        }
    }

创建Bean

代码语言:javascript复制
private static void createBeans(String classPath) throws Exception{
        Class<?> aClass = Class.forName(classPath);
        if (aClass.isAnnotationPresent(Service.class)||aClass.isAnnotationPresent(Component.class) ) {
            // 实例化之后的对象
            Object o = aClass.newInstance();
            //获取key
            String key = getMapKey(aClass);
            // 存储到map中待用
            map.put(key,o);
        }
    }

上面就是为包含有Service或者Component的注解,创建一个实例并添加到map 集合中。这里key 我单独封装成一个方法。因为要考虑到这两个注解是可以设置value 的值的。如果设置了值那么就用设置的值做key ,如果没有设置值那就用当前类的类名来做key.为了最大化兼容,将key 全部转化成大写。

代码语言:javascript复制
private static String getMapKey(Class<?> aClass){
        String key = aClass.getSimpleName();

        //获取注解的属性值
        String value="";
        if (aClass.isAnnotationPresent(Service.class)) {
            value = aClass.getAnnotation(Service.class).value();
        }else if(aClass.isAnnotationPresent(Component.class)){
            value=aClass.getAnnotation(Component.class).value();
        }
        if(!value.isEmpty()){
            key=value;
        }
        key=key.toUpperCase();
        return key;
    }

创建完所有的含注解的Bean 后,需要为这些Bean中包含Autowired 注解填充上属性。也就是将Bean和Bean之间建立联系。

代码语言:javascript复制
private static void populateBean(String classPath) throws  Exception{
        Class<?> aClass = Class.forName(classPath);
        //获取类的属性
        Field[] declaredFields = aClass.getDeclaredFields();
        for (Field field : declaredFields) {
            //如果属性上有注解,则为这个属性填充Bean
            if(field.isAnnotationPresent(Autowired.class)){
                //获取到需要填充属性的类
                Object obj = map.get(getMapKey(aClass));
                //设置属性可以赋值
                field.setAccessible(true);
                //获取到填充的属性
                String name = field.getType().getSimpleName().toUpperCase();
                //通过key 从map 中获取到对象值
                Object value = getMapValue(name);
                //为属性赋值。
                if(value!=null && obj!=null){
                    field.set(obj,value);
                }
            }
        }

    }

遍历类,并获取类的属性,如果属性上包含Autowired 注解,我们就进行处理。我们先通过类从map中获取当前类的实例。然后通过属性的类型获取需要注入的类。最后通过field.set()为当前对象的属性赋值。

这里通过key 从map 中取值,我也做了封装处理。主要是如果Autowired 注解获取的是接口,具体通过子类实现的,我这里按照惯性添加了一个IMPL 后缀获取值。

代码语言:javascript复制
private static Object getMapValue(String key){
        key=key.toUpperCase();
        Object value = map.get(key);

        //通过子类子类实现
        if (value == null) {
            value=map.get(key "IMPL");
        }
        return value;
    }

然后我们接下再次遍历,处理包含Transactional注解的,有这个注解,说明需要对这个类是是用代理对象。

代码语言:javascript复制
private static void ProxyBean(String classPath)throws Exception{
        Class<?> aClass = Class.forName(classPath);
        //如果包含事务注解
        if (aClass.isAnnotationPresent(Transactional.class)) {

            //获取实际对象的Bean
            String mapKey = getMapKey(aClass);
            Object mapValue = getMapValue(mapKey);
            //获取代理对象
            ProxyFactory proxyFactory = (ProxyFactory) getMapValue("ProxyFactory");


            //获取 Transactional 注解指定的类型
            ProxyType type = aClass.getAnnotation(Transactional.class).type();
            //判断是否指定了某种代理
            switch (type){
                case JDK:useJdkProxy(proxyFactory,mapKey,mapValue);
                    break;
                case CGLIB:usecGLIBProxy(proxyFactory,mapKey,mapValue);
                    break;
                case DEFAULT:useDefaultProxy(aClass,proxyFactory,mapKey,mapValue);
                default:break;
            }
        }

    }

先通过类名获取到当前类的key 和实例vaule 。然后从map 中获取代理对象,判断注解指定的类型。

如果是JDK就是用JDK动态代理,如果是CGLIB 就是用cglib 动态代理。如果是默认的,那就再判断。我这里都抽离出了单独方法。

如果是默认,就判断当前类是否实现接口,这里排除实现业务接口。如果没有就是用cglib 动态代理,如果使用了就用jdk 动态代理。

代码语言:javascript复制
private static void useDefaultProxy(Class<?> aClass,ProxyFactory proxyFactory,String mapKey,Object mapValue){
        //判读是否包含接口
        Class<?>[] interfaces = aClass.getInterfaces();
        //包含除了自身实现接口外的接口接口
        if(interfaces.length>1){
            useJdkProxy(proxyFactory,mapKey,mapValue);
        }else {
            usecGLIBProxy(proxyFactory, mapKey, mapValue);
       
       }
    }

JDK 动态代理,通过当前对象获取到代理对象,代替原来的对象保存在map 中。cglib 一样。

代码语言:javascript复制
private static void useJdkProxy(ProxyFactory proxyFactory,String mapKey,Object mapValue){
        Object jdkProxy = proxyFactory.getJdkProxy(mapValue);
        map.put(mapKey,jdkProxy);
    }
    
private static void usecGLIBProxy(ProxyFactory proxyFactory,String mapKey,Object mapValue){
        Object cglibProxy = proxyFactory.getCglibProxy(mapValue);
        map.put(mapKey,cglibProxy);
    }
    

最后抛出一个通过key 来获取实例的方法接口

代码语言:javascript复制
public static Object getBean(String key) {
        return getMapValue(key);
    }

增加监听

我们添加一个监听,在项目启动的时候,就初始化核心代码。

创建一个监听类实现ServletContextListener

代码语言:javascript复制

public class ContextListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        String packageName = "cn.quellanan";
        try {
            BeanFactoryByAnno.searchClass(packageName);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("初始化异常。。。。");
        }

    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {

    }
}

最后在servlet 总增加修改.,主要是为了获取transferService 实例。

代码语言:javascript复制
private TransferService transferService;

    @Override
    public void init() throws ServletException {
        transferService = (TransferService) BeanFactoryByAnno.getBean("transferService");
    }

测试

初始数据库

image-20200605144516320

转账100

image-20200605144535673

界面报错

image-20200605144629695

控制台log

image-20200605144654679

image-20200605144711976

再次查看数据库,没有变化

image-20200605144745080

去掉异常。重启动项目

image-20200605144832833

转账100,显示成功

看控制台日志:

image-20200605145020798

看数据库:

image-20200605145035343

说明整个流程都可以了,实现了转账成功事物提交和转账失败事物回滚的操作。

0 人点赞