001-Spring框架
2022-03-15 18:21:37 0 举报
AI智能生成
001-Spring框架,加油骚年
作者其他创作
大纲/内容
1)Spring是一个开源框架,Spring是于2003 年兴起的一个轻量级的Java 开发框架,由Rod Johnson 在其著作Expert One-On-One J2EE Development and Design中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。
2)Spring是一个IOC(DI)和AOP容器框架3),Spring是一个分层的JavaSE/EE full-stack(一站式) 轻量级开源框架。
它是一个全面的、企业应用开发一站式的解决方案,贯穿表现层、业务层、持久层。但是Spring仍然可以和其他的框架无缝整合。
什么是Spring框架
Spring开发的应用中的对象可以不依赖于Spring的API
非侵入式
反转控制(IOC)最经典的实现
依赖注入
AOP功能,方便进行面向切面的编程
面向切面
Spring是一个容器,因为它包含并且管理应用对象的生命周期
容器
Spring 中可以使用XML和Java注解组合这些对象
组件化
通过配置就可以完成对事务的管理,不需要手动编程
支持声明式事务处理
Spring不排斥各种优秀的开源框架,内部提供了对各种优秀框架的支持
集成各种优秀框架
轻量级
Spring的优良特性,Spring框架的特性有哪些?Spring 特点
架构图
Spring 核心组件
Spring 常用模块
核心容器 Beans--管理javabean Core--核心 Context--上下文配置 expression language -- 表达式语言面向切面 AOP--面向切面编程 Aspects--切面,AOP的框架数据访问 jdbc--数据库开发 orm--整合hibernate,包括对mybatis持久化框架的支持 Transactions--事务管理,很重要其它 整合Junit单元测试 web--支持web开发,就是后面的MVC框架
组成模块
对现有的类结构没有影响
轻量级,非侵入式
可以提供众多服务,如事务管理,WS等
AOP的很好支持,方便面向切面编程,使得业务逻辑和系统服务分开
使用Spring的IOC容器,将对象之间的依赖关系交给Spring,降低组件之间的耦合性,让我们更专注于应用逻辑
Spring DI机制降低了业务对象替换的复杂性。
Spring的高度可开放性,并不强制依赖于Spring,开发者可以自由选择Spring部分或全部
Spring优点
缺少一个公用控制器
没有SpringBoot好用
Spring像一个胶水,将框架黏在一起,后面拆分的话就不容易拆分了
Spring缺点
在pom中增加依赖
maven项目
手动添加jar包到项目路径下
普通web项目
创建项目
applicationContext.xml
创建配置文件
使用spring框架创建对象,给属性赋值
使用Spring框架
搭建Spring运行时环境
主要jar包
Spring 主要JAR包
Spring概述
Spring 通过一个配置文件描述 Bean 及 Bean 之间的依赖关系,利用 Java 语言的反射功能实例化 Bean 并建立 Bean 之间的依赖关系。
Spring 的 IoC 容器在完成这些底层工作的基础上,还提供了 Bean 实例缓存、生命周期管理、 Bean 实例代理、事件发布、资源装载等高级服务。
由 Spring IOC 容器来负责对象的生命周期和对象之间的关系
IoC 全称为 Inversion of Control,翻译为 “控制反转”
控制反转的思想完全颠覆了应用程序组件获取资源的传统方式:反转了资源的获取方向
改由容器主动的将资源推送给需要的组件,开发人员不需要知道容器是如何创建资源对象的,只需要提供接收资源的方式即可,
极大的降低了学习成本,提高了开发的效率。
这种行为也称为查找的被动形式。
什么是IOC
依赖注入(DI)和控制反转(IOC)是从不同的角度的描述的同一件事情,
就是指通过引入IOC容器,利用依赖关系注入DI的方式,实现对象之间的解耦。
简单来说:IOC 描述的是一种思想,而DI 是对IOC思想的具体实现.
IOC和DI的关系
1)在通过IOC容器读取Bean的实例之前,需要先将IOC容器本身实例化。
IOC容器的基本实现,是Spring内部的基础设施,
是面向Spring本身的,不是提供给开发人员使用的。
BeanFactory 是 Spring 框架的基础设施,面向 Spring 本身;
图解
图解BeanFactory
Spring 配置文件中每一个节点元素在 Spring 容器里都通过一个 BeanDefinition 对象表示,它描述了 Bean 的配置信息。
而 BeanDefinitionRegistry 接口提供了向容器手工注册 BeanDefinition 对象的方法。
BeanDefinitionRegistry注册表
位于类结构树的顶端 ,
它最主要的方法就是 getBean(String beanName),
该方法从容器中返回特定名称的 Bean,BeanFactory 的功能通过其他的接口得到不断扩展:
BeanFactory 顶层接口
该接口定义了访问容器中 Bean 基本信息的若干方法,如查看Bean 的个数、获取某一类型 Bean 的配置名、查看容器中是否包括某一 Bean 等方法;
ListableBeanFactory
父子级联 IoC 容器的接口,子容器可以通过接口方法访问父容器; 通过 HierarchicalBeanFactory 接口, Spring 的 IoC 容器可以建立父子层级关联的容器体系,子容器可以访问父容器中的 Bean,但父容器不能访问子容器的 Bean。Spring 使用父子容器实现了很多功能,比如在 Spring MVC 中,展现层 Bean 位于一个子容器中,而业务层和持久层的 Bean 位于父容器中。这样,展现层 Bean 就可以引用业务层和持久层的 Bean,而业务层和持久层的 Bean 则看不到展现层的 Bean。
HierarchicalBeanFactory父子级联
是一个重要的接口,增强了 IoC 容器的可定制性,它定义了设置类装载器、属性编辑器、容器初始化后置处理器等方法;
ConfigurableBeanFactory
定义了将容器中的 Bean 按某种规则(如按名字匹配、按类型匹配等)进行自动装配的方法;
AutowireCapableBeanFactory自动装配
运行期间注册单例Bean
定义了允许在运行期间向容器注册单实例 Bean 的方法;对于单实例( singleton)的 Bean 来说,BeanFactory会缓存 Bean 实例,所以第二次使用 getBean() 获取 Bean 时将直接从 IoC 容器的缓存中获取 Bean 实例。Spring 在 DefaultSingletonBeanRegistry 类中提供了一个用于缓存单实例 Bean 的缓存器,它是一个用HashMap 实现的缓存器,单实例的 Bean 以 beanName 为键保存在这个HashMap 中。
SingletonBeanRegistry
BeanFactory 的功能扩展
在初始化 BeanFactory 时,必须为其提供一种日志框架,比如使用Log4J,
即在类路径下提供 Log4J 配置文件,这样启动 Spring 容器才不会报错。
依赖日志框框
BeanFactory-框架基础设施
BeanFactory的子接口,提供了更多高级特性。
ApplicationContext 面向使用 Spring 框架的开发者,几乎所有的应用场合都直接使用 ApplicationContext 而非底层的 BeanFactory。
ApplicationContext面向开发应用
面向Spring的使用者,几乎所有场合都使用ApplicationContext而不是底层的BeanFactory。
ApplicationContext 由 BeanFactory 派生而来,提供了更多面向实际应用的功能。
ApplicationContext 继承了 HierarchicalBeanFactory 和 ListableBeanFactory 接口,在此基础上,还通过多个其他的接口扩展了 BeanFactory 的功能:
对应类路径(src)下的XML格式的配置文件(类路径就是编译后的bin路径,可以看编译目录,看文件位置)
默认从类路径加载配置文件
ClassPathXmlApplicationContext
对应文件系统(磁盘中)中的XML格式的配置文件(了解)
默认从文件系统中装载配置文件
FileSystemXmlApplicationContext
1) 是ApplicationContext的子接口,包含一些扩展方法
2) refresh()和close()让ApplicationContext具有启动、关闭和刷新上下文的能力。
ConfigurableApplicationContext
1) 专门为WEB应用而准备的,它允许从相对于WEB根目录的路径中完成初始化工作
WebApplicationContext
WebXmlApplicationContext
ApplicationContext的主要实现类
让容器拥有发布应用上下文事件的功能,包括容器启动事件、关闭事件等。
ApplicationEventPublisher
为应用提供 i18n 国际化消息访问的功能
MessageSource
所 有 ApplicationContext 实现类都实现了类似于PathMatchingResourcePatternResolver 的功能,
可以通过带前缀的 Ant 风格的资源文件路径装载 Spring 的配置文件。
ResourcePatternResolver
该接口是 Spring 2.0 加入的,该接口提供了 start()和 stop()两个方法,
主要用于控制异步处理过程。
在具体使用时,该接口同时被 ApplicationContext 实现及具体 Bean 实现,
ApplicationContext 会将 start/stop 的信息传递给容器中所有实现了该接口的 Bean,以达到管理和控制 JMX、任务调度等目的。
LifeCycle
扩展于 ApplicationContext,它新增加了两个主要的方法: refresh()和 close(),
让 ApplicationContext 具有启动、刷新和关闭应用上下文的能力。
在应用上下文关闭的情况下调用 refresh()即可启动应用上下文,在已经启动的状态下,调用 refresh()则清除缓存并重新装载配置信息,而调用close()则可关闭应用上下文。
其他接口与功能
ApplicationContext
2)Spring提供了IOC容器的两种实现方式
WebApplicationContext 是专门为 Web 应用准备的,它允许从相对于 Web 根目录的路径中装载配置文件完成初始化工作。
从WebApplicationContext 中可以获得 ServletContext 的引用,
整个 Web 应用上下文对象将作为属性放置到 ServletContext 中,以便 Web 应用环境可以访问 Spring 应用上下文。
图解、
3)WebApplication体系架构
IOC容器在Spring框架的实现
Spring 启动时,读取应用程序提供的Bean配置信息,并在Spring容器中生成一份相应的Bean配置注册表,然后根据这张注册表实例化Bean,装配好Bean之间的依赖关系,为上层应用提供准备就绪的运行环境。其中Bean缓存池为HashMap实现
图解spring容器
Spring容器高层视图
未知目标类型,需要类型强转
ApplicationContext的getBean()方法,根据id(配置的id对应)来获取bean的实例
容器中只有一个对应类型bean,可以正常
容器中有多个对象类型的bean,异常
ApplicationContext的getBean()方法,bean的类型,来获取bean的实例
唯一匹配
ApplicationContext的getBean()方法,bean的id值和类型,来获取bean的实例
bean对象获取的方式
可以在<bean>元素的scope属性里设置bean的作用域,以决定这个bean是单实例的还是多实例的bean的作用域
Spring 3中为Bean定义了5中作用域,分别为singleton(单例)、prototype(原型)、request、session和global session
singleton
在整个IOC容器中只能存在一个bean的对象. 而且在IOC容器对象被创建时,就创建单例的bean的对象.
后续每次通过getBean()方法 获取bean对象时,返回的都是同一个对象.
Spring IoC容器中只会存在一个共享的Bean实例,无论有多少个Bean引用它,始终指向同一对象
单例模式
所有bean的默认作用域
Singleton作用域是Spring中的缺省作用域
也可以显示的将Bean定义为singleton模式
配置为:<bean id=\"userDao\" class=\"com.ioc.UserDaoImpl\" scope=\"singleton\"/>
Bean的默认作用域
该模式在多线程下是不安全的。
多线程下不安全
DefaultSingletonBeanRegistry类里的singletonObjects哈希表保存了单例对象。
Spring容器可以管理singleton作用域下bean的生命周期,在此作用域下,Spring能够精确地知道bean何时被创建,何时初始化完成,以及何时被销毁
默认情况下是容器初始化的时候创建,但也可设定运行时再初始化bean
单例(singleton)
多例:prototype
在整个IOC容器中,可有多个bean的对象。而singleton全局只有一个对象。
原型的/多例的
在IOC容器对象被创建时, 不会创建原型的bean的对象,而是等到每次通过getBean()方法获取bean对象时,才会创建一个新的bean对象返回
每次通过Spring容器获取prototype定义的bean时,容器都将创建一个新的Bean实例,每个Bean实例都有自己的属性和状态
原型模式每次使用时创建
根据经验,对有状态的bean使用prototype作用域,而对无状态的bean使用singleton作用域。
当容器创建了bean的实例后,bean的实例就交给了客户端的代码管理,
多例(prototype)
请求:request
在一次Http请求中,容器会返回该Bean的同一实例。而对不同的Http请求则会产生新的Bean,而且该bean仅在当前Http Request内有效。
请求对应一个bean对象,只在web环境中有效
在一次Http请求中,容器会返回该Bean的同一实例。
一次request一个实例
<bean id=\"loginAction\" class=\"com.cnblogs.Login\" scope=\"request\"/>
请求(request)
会话:session
在一次Http Session中,容器会返回该Bean的同一实例。而对不同的Session请求则会创建新的实例,该bean实例仅在当前Session内有效。
一次会话对应一个bean对象,只在web环境中有效
在一次Http Session中,容器会返回该Bean的同一实例
而对不同的Session请求则会创建新的实例,该bean实例仅在当前Session内有效。
同Http请求相同,每一次session请求创建新的实例,而不同的实例之间不共享属性,且实例仅在自己的session请求内有效,请求结束,则实例将被销毁。
<bean id=\"userPreference\" class=\"com.ioc.UserPreference\" scope=\"session\"/>
会话(session)
在一个全局的Http Session中,容器会返回该Bean的同一个实例,仅在使用portlet \tcontext时有效。
global Session
Bean作用域
Spring中有两种类型的bean,一种是普通bean,另一种是工厂bean,即FactoryBean。
工厂bean跟普通bean不同,其返回的对象不是指定类的一个实例,其返回的是该工厂bean的getObject方法所返回的对象。
工厂bean必须实现org.springframework.beans.factory.FactoryBean接口。
工厂bean的使用
Spring IOC容器可以管理bean的生命周期,Spring允许在bean生命周期内特定的时间点执行指定的任务
作用
实例化
实例化一个Bean,也就是常说的new。
1.实例化Bean
IOC依赖注入
按照Spring上下文对实例化的Bean进行配置
也就是IOC注入
2.设置Bean的属性
setBeanName实现
如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String)方法,
spring将bean的id传给setBeanName()方法
此处传递的就是Spring配置文件中Bean的id值
BeanNameAware
BeanFactoryAware实现
如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory,
setBeanFactory(BeanFactory)传递的是Spring工厂自身(可以用这个方式来获取其它Bean,只需在Spring配置文件中配置一个普通的Bean就可以)。
会调用它实现的setBeanFactory(setBeanFactory(BeanFactory)传递的是Spring工厂自身(可以用这个方式来获取其它Bean,只需在Spring配置文件中配置一个普通的Bean就可以)
BeanFactoryAware
ApplicationContextAware实现
如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法
会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文
因为ApplicationContext是BeanFactory的子接口,有更多的实现方法
传入Spring上下文(同样这个方式也可以实现BeanFactoryAware的内容,但比4更好,)
ApplicationContextAware
3.检查Aware相关接口并设置相关依赖
postProcessBeforeInitialization接口实现-初始化预处理
BeanPostProcessor经常被用作是Bean内容的更改,并且由于这个是在Bean初始化结束时调用那个的方法,也可以被应用于内存或缓存技术。
4.检查BeanPostProcessor接口并进行前置处理
init-method
如果Bean在Spring配置文件中配置了init-method属性会自动调用其配置的初始化方法。
由于这个是在Bean初始化结束时调用那个的方法,也可以被应用于内存或缓存技术;
5.检查Bean在Spring配置文件中配置的init-method属性并自动调用其配置的初始化方法。
postProcessAfterInitialization
注:以上工作完成以后就可以应用这个Bean了,那这个Bean是一个Singleton的,
所以一般情况下调用同一个id的Bean会是在内容地址相同的实例,当然在Spring配置文件中也可以配置非Singleton。
6.检查BeanPostProcessor接口并进行后置处理
Destroy过期自动清理阶段
当Bean不再需要时,会经过清理阶段,
如果Bean实现了DisposableBean这个接口,会调用那个其实现的destroy()方法
7.当Bean不再需要时,会经过清理阶段
destroy-method自配置清理
如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。
8. 最后,配置了destroy-method属性,会自动调用其配置的销毁方法
图解Bean的生命周期
Bean的生命周期
在配置bean时,通过init-method和destroy-method 属性为bean指定初始化和销毁方法
bean 标签有两个重要的属性(init-method和destroy-method)。
用它们你可以自己定制初始化和注销方法。它们也有相应的注解(@PostConstruct和@PreDestroy)。
<bean id=\"\" class=\"\" init-method=\"初始化方法\" destroy-method=\"销毁方法\">
Bean的生命周期使用
bean后置处理器允许在调用初始化方法前后对bean进行额外的处理
bean后置处理器对IOC容器里的所有bean实例逐一处理,而非单一实例。其典型应用是:检查bean属性的正确性或根据特定的标准更改bean的属性。
bean后置处理器时需要实现接口: org.springframework.beans.factory.config.BeanPostProcessor
①通过构造器或工厂方法创建bean实例
②为bean的属性设置值和对其他bean的引用
③将bean实例传递给bean后置处理器的postProcessBeforeInitialization()方法
④调用bean的初始化方法
⑤将bean实例传递给bean后置处理器的postProcessAfterInitialization()方法
⑥bean可以使用了
⑦当容器关闭时调用bean的销毁方法
添加bean后置处理器后bean的生命周期变为7个
bean的后置处理器
① 通过构造器或工厂方法创建bean实例
② 为bean的属性设置值和对其他bean的引用
③ 调用bean的初始化方法
④ bean可以使用了
⑤ 当容器关闭时,调用bean的销毁方法
过程
1.进入getBean()方法
如果是,则去对应缓存中查找,没有查找到的话则新建实例并保存。
如果不是单例,则直接新建实例(createBeanInstance)
2.判断当前bean的作用域是否是单例,
创建bean
找到@Autowired的对象
创建注入对象,并赋值
3.新建实例后再将注入属性(populateBean),并处理回调
bean的创建过程
invokeBeanFactoryPostProcessors
扫描类
封装beanDefinition对象 各种信息
放到map
遍历map
能不能实例化 需要实例化么 根据信息来
是否单例等等
判断是不是factory bean
单例池 只是一个ConcurrentHashMap而已
正在创建的 容器
验证
得到 class
根据注入模型
默认
推断构造方法
得到构造方法
反射 实例化这个对象
后置处理器合并beanDefinition
判断是否允许 循环依赖
提前暴露bean工厂对象
自动注入
填充属性
执行部分 aware 接口
继续执行部分 aware 接口 生命周期回调方法
完成代理AOP
beanProstprocessor 的前置方法
实例化为bean
放到单例池
销毁
Bean生命周期
Bean的生命周期
构造器注入
<bean id=\"text\" class=\"com.maven.Text\" /><bean id=\"hello\" class=\"com.maven.Hello\"><constructor-arg ref=\"text\" /></bean>
构造器依赖注入通过容器触发一个类的构造器来实现的,该类有一系列参数,每个参数代表一个对其他类的依赖。
1.保证依赖不可变(final关键字)
实例化Controller的时候,由于自己实现了有参数的构造函数,所以不会调用默认构造函数,那么就需要Spring容器传入所需要的参数,这样保证了依赖不为空
2.保证依赖不为空
在构造器注入传入参数时,比如在A中注入B,在B中注入A。先初始化A,那么需要传入B,这个时候发现需要B没有初始化,那么就要初始化B,这个时候就出现了问题,会排除循环依赖错误,而使用filed注入方式则只能在运行时才会遇到这个问题
3.避免循环依赖
使用构造器注入的好处
bean的实体类中,必须提供空参和带参构造方法
构造器方式注入(通过bean的构造器赋值)
Set注入
通过bean的setXxx()方法赋值
bean的实体类中,必须提供setter方法
例如:<bean id=\"hello\" class=\"com.maven.Hello\"><property name=\"text\" ref=\"text\" /></bean>
之所以叫setter方法注入,因为这是通过找到类的对应的setter方法,再进行相应的注入
Setter方法注入是容器通过调用无参构造器或无参static工厂方法实例化bean之后,调用该bean的setter方法,即实现了基于setter的依赖注入。
图解setter方法注入
setter方法注入
通过调用静态工厂的方法来获取自己需要的对象
为了让spring管理所有对象,不能直接通过\"工程类.静态方法()\"来获取对象,而是依然通过spring注入的形式获取
静态工厂注入(略)
接口注入(略)
实例工厂的意思是获取对象实例的方法不是静态的,
所以你需要首先new工厂类,再调用普通的实例方法
实例工厂(略)
对于直接量(基本数据类型、字符串)属性:p:属性名="属性值"
对于引用Bean的属性:p:属性名-ref="Bean的id"
Spring从2.5版本开始引入了一个新的p命名空间,可以通过<bean>元素属性的方式配置Bean\t的属性
p命名空间注入属性值(略)
用构造器参数实现强制依赖
setter方法实现可选依赖
最好的解决方案是
依赖注入的几种方式,Spring 依赖注入多种方式
以value或ref的方式明确指定属性值都是手动装配
基于XML配置的显式装配
基于xml装配
构造方法
setter方法
能够在编译时就发现错误
基于Java配置的显式装配
手动装配
相对于XML方式而言,通过注解的方式配置bean更加简洁和优雅,而且和MVC组件化开发的理念十分契合,是开发中常用的使用方式。
标识一个受Spring IOC容器管理的组件,比如有个类要交给spring的Ioc容器管理,只要在类上加这个注解就可以了,这个只是普通的组件,没有特殊含义
普通组件:@Component
标识一个受Spring IOC容器管理的持久化层组件
持久化层组件:@Repository
标识一个受Spring IOC容器管理的业务逻辑层组件
业务逻辑层组件:@Service
标识一个受Spring IOC容器管理的表述层控制器组件
控制层/表述层控制器组件:@Controller
分层注解
默认情况:使用组件的简单类名首字母小写后得到的字符串作为bean的id
使用组件注解的value属性指定bean的id
注意:事实上Spring并没有能力识别一个组件到底是不是它所标记的类型,即使将@Respository注解用在一个表述层控制器组件上面也不会产生任何错误,所以 @Respository、@Service、@Controller这几个注解仅仅是为了让开发人员自己明确当前的组件扮演的角色,在使用上基本是没有区别的。
组件命名规则
组件被上述注解标识后还需要通过Spring进行扫描才能够侦测到
核心配置文件增加配置:<context:component-scan base-package="com.kgc.spring.annotation"></context:component-scan>
扫描加了注解的类,并管理到IOC容器中 base-package: 基包. Spring会扫描指定包以及子包下所有的类,将带有注解的类管理到IOC容器中当需要扫描多个包时可以使用逗号分隔
组件扫描
标签将会开启Spring Beans的自动扫描,并可设置base-package属性,表示Spring将会扫描该目录以及子目录下所有被@Component标注修饰的类,对它们进行装配。
自动扫描(component-scan)
<context:include-filter>子节点表示要包含的目标类
通常需要与父标签的use-default-filters属性配合使用才能够达到“仅包含某些组件”这样的效果。即:通过将use-default-filters属性设置为false,禁用默认过滤器,然后扫描的就只是include-filter中的规则指定的组件了。否则不生效
指定扫描
<context:exclude-filter>子节点表示要排除在外的目标类
排除扫描
按照注解方式进行指定,注意type和expression(注解的全类名),推荐用法
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
根据注解
按照指定类进行指定,注意type和expression(具体类的全类名)
<context:include-filter type="assignable" expression="com.kgc.spring.annotation.controller.Controller"/>
根据类型
过滤扫描方式
指定扫描和排除扫描
Controller组件中往往需要用到Service组件的实例,
Service组件中往往需要用到 Repository组件的实例。
Spring可以通过注解的方式帮我们实现属性的装配。
需求:
原理:在指定要扫描的包时,<context:component-scan> 元素会自动注册一个bean的后置处\t理器:AutowiredAnnotationBeanPostProcessor的实例。该后置处理器可以 自动装配标记\t了@Autowired、@Resource或@Inject注解的属性。
1.优先按照byType方式进行查找
首先会使用byType的方式进行自动装配,
如果查询的结果为空,那么会抛出异常。可使用required=false解决
如果能唯一匹配,则装配成功,
如果能唯一确定,则装配成功
否则@Autowired会按照byName方式来查找。
如果查询的结果不止一个,而且没有设置名称的话,那么会报错。
如果不能唯一确定,则装配失败,抛出异常
工作机制
默认情况下, 使用@Autowired标注的属性必须被装配,如果装配不了,也会抛出异常
可以设置@Autowired注解的required属性为 false,实现允许不必须被装配
(容器中找不到的情况,如果多个匹配,不适用)
属性配置
如果匹配到多个兼容类型的bean,可以使用@Qualifier来进一步指定要装配的bean的id值
在自动装配的成员对象变量上,增加注解:@Qualifier("目标bean的idl")
@Qualifier注解
@Autowired注解
Resource注解,也可以实现自动装配,但是这个不是spring的注解,是jdk提供的,实现自动装配原理,正好跟Autowired注解是相反的(先根据名称,再根据类型)
提供一个bean名称的属性,若该属性为空,则自动采用标注处的变量或方法名作为bean的名称。
@Resource注解
1.默认按照byName方式进行装配,名称可以通过name属性进行指定
2. 如果没有指定name属性,当注解写在字段上时,默认取字段名进行按照名称查找,如果注解写在setter方法上默认取属性名进行装配。
3. 当找不到与名称匹配的bean时才通过byType进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。
4.只指定@Resource注解的type属性,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常。
@Resource
@Inject和@Autowired注解一样也是按类型注入匹配的bean,但没有required属性
了解即可
@Inject注解
组件自动装配
注解
实现方式
根据指定的装配规则,不需要明确指定,Spring自动将匹配的属性值注入bean中。
基于注解的自动装配
自动装配有五种自动装配的方式,可以用来指导Spring容器用自动装配方式来进行依赖注入。
默认的方式是不进行自动装配,
通过显式设置ref 属性来进行装配。
不进行自动装配(关闭)
1. no:
通过参数名 自动装配,
根据名称自动装配:byName
使用bean的属性名与IOC容器中<bean>的id值进行匹配. 匹配成功则装配成功.
Spring容器在配置文件中发现bean的autowire属性被设置成byname,之后容器试图匹配、装配和该bean的属性具有相同名字的bean。
2. byName:
通过参数类型自动装配,
根据类型自动装配:byType
使用bean的属性的类型与IOC容器中<bean>的class进行匹配。
Spring容器在配置文件中发现bean的autowire属性被设置成byType,之后容器试图匹配、装配和该bean的属性具有相同类型的bean。
如果唯一匹配,则装配成功
如果匹配到多个兼容类型的bean。则抛出异常
如果有多个bean符合条件,则抛出错误。
3. byType:
这个方式类似于byType, 但是要提供给构造器参数,
如果没有确定的带参数的构造器参数类型,将会抛出异常。
通过构造器进行自动装配
4. constructor:
首先尝试使用constructor来自动装配,如果无法工作,则使用byType方式。
5. autodetect:
使用父标签beans配置的装配方式
(default-autowire指定的装配方式)
default
5种不同方式的自动装配
使用bean的属性的类型与IOC容器中<bean>的class进行匹配。 如果唯一匹配则装配成功
constructor:通过构造器进行自动装配,no:不进行自动装配(关闭),default:使用父标签beans配置的装配方式(default-autowire指定的装配方式)
注解实现自动装配
Spring的装配方式(依赖注入的具体行为)
属性注入可以破解
三级缓存没自己 因二级之后去加载B了
构造器不行
情况
去单例池拿
判断是不是正在被创建的
判断是否 支持循环依赖
二级缓存 放到 三级缓存
GC
干掉二级缓存
下次再来直接 三级缓存拿 缓存
三级缓存
一级缓存 单例Bean
产生bean 复杂
二级缓存 工厂 产生baen
三级缓存 半成品
缓存 存放
因为加入singletonFactories三级缓存的前提是执行了构造器(因为要先构建出对象),所以构造器的循环依赖没法解决。
构造器方式无法解决,只能抛出异常
因为Spring容器不缓存\"prototype\"作用域的bean,因此无法提前暴露一个创建中的bean。
多例方式无法解决,只能抛出异常
通过三级缓存解决
在createBeanInstance()之后会调用addSingleton()方法将bean注册到singletonFactories中
通过提前暴露一个单例工厂方法,从而使其他bean能够引用到该bean/提前暴露一个正在创建中的bean
1.“A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象
2.A首先完成了初始化的第一步,并且将自己提前曝光到singletonFactories中(这步是关键)
3.A发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程
4.B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象
5.B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中
6.返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中
7.由于B拿到了A的对象引用,所以B类中的A对象完成了初始化。
举例
单例模式可以解决
循环依赖
1.首先根据配置文件找到对应的包,读取包中的类,,找到所有含有@bean,@service等注解的类,利用反射解析它们,包括解析构造器,方法,属性等等,然后封装成各种信息类放到container(其实是一个map)里(ioc容器初始化)
2.获取类时,首先从container中查找是否有这个类,如果没有,则报错,如果有,则通过构造器信息将这个类new出来
3.如果这个类含有其他需要注入的属性,则进行依赖注入,如果有则还是从container找对应的解析类,new出对象,并通过之前解析出来的信息类找到setter方法(setter方法注入),然后用该方法注入对象(这就是依赖注入)。如果其中有一个类container里没找到,则抛出异常
4.如果有嵌套bean的情况,则通过递归解析
5.如果bean的scope是singleton,则会重用这个bean不再重新创建,将这个bean放到一个map里,每次用都先从这个map里面找。如果scope是session,则该bean会放到session里面。
大致流程
依赖注入(Spring的装配方式)
1.)Resource定位;指对BeanDefinition的资源定位过程。通俗地讲,就是找到定义Javabean信息的XML文件,并将其封装成Resource对象。
2.)BeanDefinition的载入;把用户定义好的Javabean表示为IoC容器内部的数据结构,这个容器内部的数据结构就是BeanDefinition。
3.)向IoC容器注册这些BeanDefinition。
容器的初始化过程
内部bean的使用
外部已声明的bean
引用其他的bean
是什么?
控制反转的原理
原理
SpringBean的生命周期?
Spring的生命周期+1
Key-value?
Spring是否可以注入java集合?如何注入?
Spring IOC+4
IOC
常见问题
子主题
Spring 中的IoC 的实现原理就是工厂模式加反射机制。
上面写法的缺点是当我们再添加一个子类的时候,就需要修改工厂类了。
如果添加太多的子类时,改动就会很多。下面用反射机制实现工厂模式:
现在就算我们添加任意多个子类的时候,工厂类都不需要修改。使用反射机制实现的工厂模式可以通过反射取得接口的实例,但是需要传入完整的包和类名。而且用户也无法知道一个接口有多少个可以使用的子类,所以我们通过属性文件的形式配置所需要的子类。
apple=Reflect.Appleorange=Reflect.Orange
首先创建一个fruit.properties 的资源文件:
然后编写主类代码:
运行结果:Apple。
下面编写使用反射机制并结合属性文件的工厂模式(即Io C )。
反射与工厂模式实现IOC
通俗来讲就是根据给出的类名(字符串方式)来动态地生成对象,
这种编程方式可以让对象在生成时才被决定到底是哪一种对象。
只是在Spring 中要生产的对象都在配置文件中给出定义,目的就是提高灵活性和可维护性。
IOC 中最基本的技术就是“反射(Reflection)”编程
其中PHP5 的技术书籍中,有时候也被翻译成“映射”。
有关反射的概念和用法,大家应该都很清楚。
目前C#、Java 和PHP5 等语言均支持反射
比如像Java 中的Hibernate、Spring 框架,
.Net 中NHibernate、Spring.NET 框架
都是把”反射“做为最基本的技术手段。
反射的应用是很广泛的,很多的成熟的框架,都是把”反射“做为最基本的技术手段。
反射技术其实很早就出现了,但一直被忽略,没有被进一步的利用。
当时的反射编程方式相对于正常的对象生成方式要慢至少得10 倍。
现在的反射技术经过改良优化,已经非常成熟,
反射方式生成对象和通常对象生成方式,速度已经相差不大了,大约为1-2 倍的差距。
反射技术经过改良优化,已经非常成熟
我们可以把IOC 容器的工作模式看做是工厂模式的升华,可以把IOC 容器看作是一个工厂
这个工厂里要生产的对象都在配置文件中给出定义,然后利用编程语言提供的反射机制,根据配置文件中给出的类名生成相应的对象。
从实现来看,IOC 是把以前在工厂方法里写死的对象生成代码,改变为由配置文件来定义,
也就是把工厂和对象生成这两者独立分隔开来,目的就是提高灵活性和可维护性。
IOC 容器的工作模式
IOC 容器的技术剖析
使用IOC 框架产品能够给我们的开发过程带来很大的好处,但是也要充分认识引入IOC 框架的缺点,做到心中有数,杜绝滥用框架。
1) 软件系统中由于引入了第三方IOC 容器,生成对象的步骤变得有些复杂,本来是两者之间的事情,又凭空多出一道手续,所以,我们在刚开始使用IOC 框架的时候,会感觉系统变得不太直观。所以,引入了一个全新的框架,就会增加团队成员学习和认识的培训成本,并且在以后的运行维护中,还得让新加入者具备同样的知识体系。
2) 由于IOC 容器生成对象是通过反射方式,在运行效率上有一定的损耗。如果你要追求运行效率的话,就必须对此进行权衡。
3) 具体到IOC 框架产品(比如Spring)来讲,需要进行大量的配制工作,比较繁琐,对于一些小的项目而言,客观上也可能加大一些工作成本。
4) IOC 框架产品本身的成熟度需要进行评估,如果引入一个不成熟的IOC 框架产品,那么会影响到整个项目,所以这也是一个隐性的风险。
一些工作量不大的项目或者产品,不太适合使用IOC框架产品。
另外,如果团队成员的知识能力欠缺,对于IOC 框架产品缺乏深入的理解,也不要贸然引入。
最后,特别强调运行效率的项目或者产品,也不太适合引入IOC 框架产品,像WEB2.0 网站就是这种情况。
结论
使用IOC 框架应该注意什么
IOC控制反转/DI依赖注入
执行加减乘除运算
在程序执行期间追踪正在发生的活动(记录日志)
此种实现方式分析
数学计算器
前奏
手动添加jar包
普通项目
添加依赖
环境搭建
XML方式
注解方式
@Configuration 作用于类上,相当于一个xml配置文件;
@Bean 作用于方法上,相当于xml配置中的<bean>;
通过 @Configuration 和 @Bean 这两个注解实现的
基于Java类配置
配置方式
1) AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传统 OOP(Object-Oriented Programming,面向对象编程)的补充。
2) AOP编程操作的主要对象是切面(aspect),而切面模块化横切关注点(可以理解为功能)。 在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。这样一来横切关注点就被模块化到特殊的类里——这样的类我们通常称之为“切面”。
3) AOP的好处: ① 每个事物逻辑位于一个位置,代码不分散,便于维护和升级 ② 业务模块更简洁,只包含核心业务代码
AOP(Aspect-Oriented Programming,面向切面编程):
是一种新的方法论,是对传统 OOP(Object-Oriented Programming,面向对象编程)的补充。
核心业务功能和切面功能分别独立进行开发 ,然后把切面功能和核心业务功能 \"编织\" 在一起,这就叫AOP
让关注点代码与业务代码分离
面向切面编程就是指: 对很多功能都有的重复的代码抽取,再在运行的时候往业务方法上动态植入“切面类代码”。
\"横切\"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为\"Aspect\",即切面。所谓\"切面\",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
使用\"横切\"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。
AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
AOP概述、基本概念
使用动态代理
代理设计模式的原理:使用一个代理对象将原始对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。
实现模式
封装了各个抽离的功能点的类,Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中
类是对物体特征的抽象,切面就是对横切关注点的抽象
切面(Aspect)
对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点。
横切关注点
增强包含了用于添加到目标连接点上的一段执行逻辑,又包含了用于定位连接点的方位信息也就是需要执行的具体操作,
简单理解为切面中的一个方法
也被翻译成通知,
通知(advice)
所谓通知指的就是指拦截到连接点之后要执行的代码,
通知分为前置、后置、异常、最终、环绕通知五类。
增强、通知(Adivce)
定义义了“故事”发生的地点。例如某个类或者方法名,Spring中允许我们使用正则来指定
对连接点进行拦截的定义
切入点(Pointcut)
切入点匹配的执行点称作连接点。
如果说切入点是查询条件,那连接点就是被选中的具体的查询结果(具体的数据记录)。
简单记忆,就是业务类中的各个方法中执行增强的位置。
被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器。
连接点(Joinpoint)
AOP框架创建的对象。一个类被AOP织入增强之后,就产生了一个结果类,它是融合了原类和增强逻辑的代理类。
代理(Proxy)
增强逻辑的织入的目标类
代理的目标对象
目标对象(Target)
将切面应用到目标对象并导致代理对象创建的过程
织入(weave)
在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段。
引入(introduction)
切面可以理解为由增强处理和切入点组成,既包含了横切逻辑的定义,也包含了连接点的定义。
面向切面编程主要关心两个问题,即:在什么位置,执行什么功能。
说明
术语、AOP核心概念
<!-- 组件扫描 --><context:component-scan base-package="com.kgc.spring.aspectj"></context:component-scan>
<aop:aspectj-autoproxy/>
基于注解使用AspectJ: 主要的作用是当Spring IOC容器侦测到bean配置文件中的<aop:aspectj-autoproxy>元素时,会自动为 与AspectJ切面匹配的bean创建代理
引入aop名称空间,增加配置
要在Spring中声明AspectJ切面,只需要在IOC容器中将切面声明为bean实例。使用Component
当在Spring IOC容器中初始化AspectJ切面之后,Spring IOC容器就会为那些与 AspectJ切面相匹配的bean创建代理。
在AspectJ注解中,切面只是一个带有@Aspect注解的Java类,它往往要包含很多通知。
通知是标注有某种注解的简单的Java方法
① @Before:前置通知,在方法执行之前执行
不管目标方法有没有抛出异常
无法获取返回结果
② @After:后置通知,在方法执行之后执行
可以获取到方法的返回值
通过returning属性 来指定一个名字, 必须要与当前方法的一个形参名一致.
③ @AfterRunning:返回通知,在方法返回结果之后执行
通过throwing属性来指定一个名字, 必须要与当前方法的一个形参名一致.
④ @AfterThrowing:异常通知,在方法抛出异常之后执行
可以理解是 前置 后置 返回 异常 通知的结合体
⑤ @Around:环绕通知,围绕着方法执行
AspectJ支持5种类型的通知注解
@Aspect注解
execution([权限修饰符] [返回值类型] [简单类名/全类名] [方法名]([参数列表]))
语法
execution(* com.kgc.spring.aspectJ.annotation.*.*(..))
第一个 * : 任意修饰符 任意返回值
第二个 * : 任意类
第三个 * : 任意方法
.. : 任意参数列表
通知要作用的目标包下的所有类中的所有类的所有方法
含义
同一个切点表达式可能会在多个通知中重复出现
通过@Pointcut注解将一个切入点声明成简单的方法
@Pointcut("execution(* com.kgc.spring.aspectJ.annotation.*.*(..))")public void declarePointCut() {}
目标通知直接指定方法名就可以了,如:@After("declarePointCut()")
重用切入点表达式
切入点表达式
通过注解@Order(值)改变优先级
默认int类型最大值 2147483647 值越小优先级越高
切面优先级
使用AspectJ注解声明切面
记录访问日志
性能监控后做SQL优化
统一后端返回值格式
Spring Boot AOP 应用场景
Spring 最重要的两个功能,就是依赖注入(DI)和面向切面编程 (AOP)。
AOP 为我们提供了处理问题的全局化视角,使用得当可以极大提高编程效率。
AOP 之所以如此重要,在于它提供了解决问题的新视角。
通过将业务逻辑抽象出切面,功能代码可以切入指定位置,从而消除重复的模板代码。
使用 AOP 有一种掌握全局的快感,发现业务逻辑中的切面颇有一番趣味
Spring Boot 中使用 AOP 与 Spring 中使用 AOP 几乎没有什么区别,只是建议尽量使用 Java 配置,代替 XML 配置。
本节就来演示下 Spring Boot 中使用 AOP 的常见应用场景。
AOP介绍
首先需要构建一个 Spring Boot 项目并引入 AOP 依赖,后续场景演示均是在这个项目上实现的。
构建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-aop
生成项目后导入 Eclipse 开发环境
使用 Spring Initializr 创建项目
引入 Web 项目依赖与 AOP 依赖。
<!-- Web项目依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- AOP --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
引入项目依赖
为了便于后续的演示,依次新建控制类、服务类、数据访问类,并将其放入对应的包中,项目结构如下:
项目结构
各个类代码如下,注意此处仅仅是为了演示 AOP 的使用,并未真实访问数据库,而是直接返回了测试数据。
@RestControllerpublic class GoodsController { @Autowired private GoodsService goodsService; //获取商品列表 @GetMapping(\"/goods\") public List getList() { return goodsService.getList(); }}
//商品控制器类
@Servicepublic class GoodsService { @Autowired private GoodsDao goodsDao; //获取商品信息列表 public List getList() { return goodsDao.getList(); }}
//商品服务类
@Repository // 标注数据访问类public class GoodsDao { //查询商品列表 public List getList() { return new ArrayList(); }}
// 商品数据库访问类
新建控制层、服务层、数据访问层
如果要记录对控制器接口的访问日志,可以定义一个切面,切入点即为控制器中的接口方法,然后通过前置通知来打印日志。
//日志切面 @Component@Aspect // 标注为切面public class LogAspect { private Logger logger = LoggerFactory.getLogger(this.getClass()); // 切入点表达式,表示切入点为控制器包中的所有方法 @Pointcut(\"within(com.imooc.springbootaop.controller..*)\") public void LogAspect() { } // 切入点之前执行 @Before(\"LogAspect()\") public void doBefore(JoinPoint joinPoint) { logger.info(\"访问时间:{}--访问接口:{}\
启动项目后,访问控制器中的方法之前会先执行 doBefore 方法。
2020-05-25 22:14:12.317 INFO 9992 --- [nio-8080-exec-2] com.imooc.springbootaop.LogAspect : 访问时间:Mon May 25 22:14:12 CST 2020--访问接口:List com.imooc.springbootaop.controller.GoodsController.getList()
控制台打印如下:
(1)使用 AOP ,记录日志(访问控制器方法前执行 doBefore 方法)
在研发项目的性能测试阶段,或者项目部署后,希望查看服务层方法执行的时间。
以便精准的了解项目中,哪些服务方法,执行速度慢,后续,可以针对性的进行性能优化。
此时就可以使用 AOP 的环绕通知,监控服务方法的执行时间。
//服务层方法切面 @Component@Aspect // 标注为切面public class ServiceAspect { private Logger logger = LoggerFactory.getLogger(this.getClass()); // 切入点表达式,表示切入点为服务层包中的所有方法 @Pointcut(\"within(com.imooc.springbootaop.service..*)\") public void ServiceAspect() { } @Around(\"ServiceAspect()\") // 环绕通知 public Object deAround(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis();// 记录开始时间 Object result = joinPoint.proceed(); logger.info(\"服务层方法:{}--执行时间:{}毫秒\
2020-05-25 22:25:56.830 INFO 4800 --- [nio-8080-exec-1] com.imooc.springbootaop.ServiceAspect : 服务层方法:List com.imooc.springbootaop.service.GoodsService.getList()--执行时间:3毫秒
当服务层方法被调用时,控制台输入日志如下:
(2)使用 AOP ,监控性能(查看服务层方法执行的时间)
正常情况下,用户查看页面或进行更新操作时,耗时超过 1.5 秒,就会感觉到明显的迟滞感。
由于前后端交互也需要耗时,按正态分布的话,大部分交互耗时在 0.4秒 左右。
所以在参与的项目中,会对耗时超过 1.1 秒的服务层方法,进行跟踪分析,通过优化 SQL 语句、优化算法、添加缓存等方式,缩短方法执行时间。
上面的数值均为我个人的经验参考值,还要视乎具体的服务器、网络、应用场景来确定合理的监控临界值。
通过使用AOP监控性能对SQL进行优化
前后端分离的项目结构中,前端通过 Ajax 请求后端接口,此时最好使用统一的返回值格式供前端处理。
此处就可以借助 AOP 来实现正常情况、异常情况返回值的格式统一。
首先定义返回值类,它属于业务逻辑对象 (Bussiness Object),所以此处命名为 ResultBo ,代码如下:
public class ResultBo<T> { //错误码 0表示没有错误(异常) 其他数字代表具体错误码 private int code; //后端返回消息 private String msg; //后端返回的数据 private T data; //无参数构造函数 public ResultBo() { this.code = 0; this.msg = \"操作成功\"; } //带数据data构造函数 public ResultBo(T data) { this(); this.data = data; } //存在异常的构造函数 public ResultBo(Exception ex) { this.code = 99999;// 其他未定义异常 this.msg = ex.getMessage(); } // 省略 get set}
定义返回值类
对所有的控制层方法进行修改,保证返回值均通过 ResultBo 包装,
另外再定义一个方法,模拟抛出异常的控制层方法。
// 获取商品列表 @GetMapping(\"/goods\") public ResultBo getList() { return new ResultBo(goodsService.getList()); } // 模拟抛出异常的方法 @GetMapping(\"/test\") public ResultBo test() { int a = 1 / 0; return new ResultBo(goodsService.getList()); }
修改控制层返回值类型
正常控制层方法都返回 ResultBo 类型对象,然后我们需要定义切面,处理控制层抛出的异常。
当发生异常时,同样返回 ResultBo 类型的对象,并且对象中包含异常信息。
//返回值切面 @Component@Aspectpublic class ResultAspect { // 切入点表达式,表示切入点为返回类型ResultBo的所有方法 @Pointcut(\"execution(public com.imooc.springbootaop.ResultBo *(..))\") public void ResultAspect() { } // 环绕通知 @Around(\"ResultAspect()\") public Object deAround(ProceedingJoinPoint joinPoint) throws Throwable { try { return joinPoint.proceed();// 返回正常结果 } catch (Exception ex) { return new ResultBo<>(ex);// 被切入的方法执行异常时,返回ResultBo } }}
定义切面,处理异常返回值
启动项目,访问 http://127.0.0.1:8080/goods 返回数据如下:
{\"code\
然后访问 http://127.0.0.1:8080/test ,返回数据如下:
这样,前端可以根据返回值的 code, 来判断后端是否正常响应。
如果 code 为 0 ,则进行正常业务逻辑操作;如果 code 非 0 ,则可以弹窗显示 msg 提示信息。
测试
(3)使用 AOP, 统一后端返回值格式
Spring Boot整合AOP应用场景
权限控制
1. Authentication 权限
2. Caching 缓存
Controller层的参数校验
信息过滤,页面转发等等功能
3. Context passing 内容传递
4. Error handling 错误处理
5. Lazy loading 懒加载
6. Debugging 调试
日志
8. Performance optimization 性能优化
9. Persistence 持久化
10. Resource pooling 资源池
11. Synchronization 同步
Spring声明式事务管理配置
事务管理
12. Transactions 事务
能做哪些事/应用场景
小明想购买法国某个牌子的香水送给女朋友,但是在国内没有货源售卖,亲自去法国又大费周章了
而小红现在正在法国玩耍,她和小明是好朋友,可以帮小明实到这个牌子的香水
于是小明就找到小红,答应给她多加5%的辛苦费,小红答应了,小明成功在中国买到了法国的香水。
之后小红开启了狂的代购模式,赚到了很多手续费。
生活中的代理模式
在故事中,小明是一个客户,它让小红帮忙购买香水,小红就成了一个代理对象,而香水提供商是一个真实的对象,可以售卖香水
小明通过代理商小红,购买到法国的香水,这就是一个代购的例子。
小明通过代理商小红,购买到法国的香水
一幅图帮助理解这个故事的整个结构
详解代理过程
这个故事是最典型的代理模式,代购从供应商购买货物后返回给调用者,也就是需要代理的小明。
生活中的代理模式:代购
定义好一个售卖香水的接口
定义好一个售卖香水的接口,定义好售卖香水的方法井传入该香水的价格。
定义香奈儿(Chanel) 香水提供商
定义香奈儿(Chanel) 香水提供商, 实现接口.
定义小红代理类
定义小红代理类,她需要代购去售卖香奈儿香水,所以她是香奈儿香水提供商的代理对象,同样实现接口,并在内部保存对目标对象(香亲儿提供商)的引用,控制其它对象对目标对象的访问。
定义需求者
小明是一个需求者,需要去购买香水,只能通过小红去购买,所以去找小红购买1999.99的香水。
看看运行结果,
看看运行结果,小红在向小明售卖香水前可以执行额外的其它操作,如果良心点的代购就会打折、包邮…,如果黑心点的代购就会加手续费、售出不退还…,是不是很刺激。
4个类组成的类图关系结构
来看看上面4个类组成的类图关系结构,可以发现小红和香奈儿提供商都实现了售卖香水这一接口,而小红内部增加了对提供商的引用,用于调用提供商的售卖香水功能。
代码案例:代购香奈儿(Chanel) 香水
定义真实对象和代理对象的公共接口(售卖香水接口)
代理对象内部保存对真实目标对象的引用(小红引用提供商)
访问者仅能通过代理对象访问真实目标对象,不可直接访问目标对象(小明只能通过小红去购买香水,不能直接到香奈儿提供商购买)
实现代理模式,需要走以下几个步骤
小红并不是真正卖香水的
代理对象并不是真正提供服务的一个对象,它只是替访问者访问目标对象的一个中间人
卖香水的还是香奈儿提供商
真正提供服务的还是目标对象
而小红只不过是在让香奈儿卖香水之前和之后执行了一些自己额外加上去的操作。
而代理对象的作用就是在目标对象提供服务之前和之后能够执行额外的逻辑
代理模式很容易产生错误思维的点
目前来看,代理对象小红已经能够代理购买香水了,但有一天,小红的另外一个朋友小何来了,他想购买最纯正的法国红酒,国内没有这样的购买渠道,小红刚巧也在法国,于是小何就想找小红帮他买红酒啦,这和小明找小红是一个道理的,都是想让小红做代理。
创建售卖红酒的接口
售卖红酒提供商和代理对象小红都需要实现该接口
小何访问小红,让小红卖给他红酒
但问题是:在程序中,小红只能代理购买香水,如果要代理购买红酒,要怎么做呢?
新的需求
图解代购法国红酒
OK,事已至此,代码就不重复写了,我们来探讨一下,面对这种新增的场景,上面的这种实现方法有没有什么缺陷呢?
在编写程序的过程中,软件的所有对象应该是对扩展是开放的,而对修改是关闭的
软件工程中的开闭原则
面对新的需求时,需要修改代理类,增加实现新的接口和方法,导致代理类越来越庞大,变得难以维护,
虽然说目前代理类只是实现了2个接口,如果日后小红不只是代理售卖红酒,还需要代理售卖电影票、代购日本寿司…
实现的接口会变得越来越多,内部的结构变得越来越复杂,整个类显得愈发瘾肿,变得不可维护,
之后的扩展也会成问题,只要任意一个接口有改动,就会牵扯到这个代理类,维护的代价很高。
为什么静态代理违反了开闭原则
为了提高类的可扩展性和可维护性, 满足开闭原则, Java提供了动态代理机制
代码案例:代购法国红酒
代理模式
给目标对象提供一个代理对象,代理对象包含该目标对象,并控制对该目标对象的访问。
代理的定义
在编译的时候就直接生成代理类
代理类是由程序员自己编写的,在编译期就确定好了的。
代理模式中的所有角色(代理对象、目标对象、目标对象的接口)等都是在编译期就确定好的。
定义
代码结构简单,较容易实现
优点
无法适配所有代理场景,如果有新的需求,需要修改代理类,不符合软件工程的开闭原则
如果要代理一个接口的多个实现的话需要定义不同的代理类
一旦需要代理的类中方法比较多,或者需要同时代理多个对象时,这无疑会增加很大的复杂度。
需要程序员手写很多代码,这个过程是比较浪费时间和精力的
小红现在只是代理香水,如果小明需要找小红买法国红酒,那小红就需要代理法国红酒了,
但是静态代理去扩展代理功能必须修改小红内部的逻辑,这会让小红内部代码越来越臃肿
以生活中的例子
缺点
而静态代理的代理对象, 在程序编译时已经写好Java文件了, 直接new一个代理对象即可
区别点
上面的代码比较简单,定义了一个接口和其实现类。这就是代理模式中的目标对象和目标对象的接口。
定义了一个接口和其实现类
接下类定义代理对象。上面就是一个代理类,他也实现了目标对象的接口,并且扩展了say 方法
定义代理对象
下面是一个测试类:
测试类
代码说明:一个简单的静态的代理模式的实现
通过代理对象控制对真实对象的使用权限
控制真实对象的访问权限
通过使用一个代理小对象来代表一个真实的大对象
可以减少系统资源的消耗,对系统进行优化并提高运行速度。
避免创建大对象
这个比较简单,通过代理可以在调用真实对象的方法的前后增加额外功能。
增强真实对象的功能
静态代理的用途
静态代理
是面对新的需求时,不需要修改代理对象的代码,只需要新增接口和真实对象,在客户端调用即可完成新的代理,
解决的问题
满足软件工程的开闭原则,提高类的可维护性和可扩展性.
目的
动态代理中的代理类并不要求在编译期就确定,而是可以在运行期动态生成,从而实现对目标对象的代理功能。
程序在运行时动态生成代理类对象,拦截调用方法,在调用方法前后扩展额外的功能
动态代理的精髓
反射机制
生成动态代理对象的原理
反射是动态代理的一种实现方式。
和反射的关系
动态代理产生代理对象的时机是运行时动态生成, 它没有Java源文件, 直接生成字节码文件实例化代理对象:
不需要程序员自己手写代理类
动态代理在静态代理的基础上做了改进,极大地提高了程序的可维护性和可扩展性。
能够动态适配特定的代理场景,扩展性较好,符合软件工程的开闭原则
动态代理比静态代理更加稳健,对程序的可维护性和可扩展性更加友好
如果小明需要找小红代理红酒,无需修改代理类小红的内部逻辑,只需要关注扩展的功能点:代理红酒,实例化新的类,通过一些转换即可让小红既能够代理香水也能够代理红酒了。
动态代理需要利用到反射机制和动态生成字节码,导致其性能会比静态代理稍差一些
但是相比于优点,这些劣势几乎可以忽路不计
传统的OOP编程符合从上往下的编码关系, 却不符合从左往右的编码关系, 如果你看不懂, 可以参考下面的动图
OOP满足一个方法一个方法从上往下地执行, 但是却不能从左往右嵌入代码,
而AOP的出现很好地弥补了这一点
它允许将重复代码逻辑抽取出来形成一个单独覆盖层, 在执行代码时,可以将该覆盖层,毫无知觉的嵌入到原代码逻辑里面去
method 1和method 2都需要在方法执行前后记录日志, 实际上会有更多的方法需要记录日志
如图所示
只能在每个方法前后手动记录日志
大量的Log.info存在于方法内部,导致代码阅读性下降
方法内部无法专注于自己的逻辑。
传统的OOP的痛点
可以将这些重复性的代码包装到额外的一层, 监听方法的执行
当方法被调用时, 通用的日志记录层会拦截掉该方法,在该方法调用前后记录日志
这样让方法专注于自己的业务逻辑而无需关注其它不必要的信息。
AOP的解决方法
以记录日志为实例
AOP解决OOP的痛点
提供缓存
提供日志环绕
事务处理
Spring AOP的功能
Spring的事务涉及到一个核心注解@Transactional
加上这个注解之后,在执行方法时如果发生异常,该方法内所有的事务都回滚,否则全部提交生效,这是最宏观的表现
每个有关数据库的操作都要保证一个事务内的所有操作,要么全部执行成功,要么全部执行失败
传统的事务代码
传统的事务失败回滚和成功提交是使用try...catch代码块完成的
如果多个方法都需要写这一段逻辑非常冗余, 所以Spring封装了一个注解@Transactional
使用它后,调用方法时,会监视方法,如果方法上含有该注解,就会自动把数据库相关操作的代码包裹起来,最终形成类似于上面的一段代码原理
当然这里并不准确,只是一个大概的总览,了解Spring AOP的本质在干什么
内部是如何实现的呢?
以事务作为例,讲解Spring底层是如何使用动态代理的
Spring AOP
动态代理的实际应用
JDK Proxy(JDK提供的动态代理)
Cglib 动态代理
Spring AOP 中的动态代理主要有两种方式
动态代理
代理可以分为两大类
都能够实现代理模式
代理对象和目标对象都需要实现一个公共接口
共同点
通过代理对象的隔离,可以在对目标对象访问前后增加额外的业务逻辑,实现功能增强。
通过代理对象访问目标对象,可以防止系统大量地直接对目标对象进行不正确地访问,出现不可预测的后果
代理模式的目的
使用一个代理对象将原始对象包装起来,然后用该代理对象取代原始对象。
任何对原始对象的调用都要通过代理。
代理对象决定是否以及何时将方法调用转到原始对象上。
代理设计模式的原理
JDK Proxy(JDK动态代理)是JDK提供的一个动态代理机制
JDK 动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。
java.lang.reflect 包中的Proxy 类和InvocationHandler 接口提供了生成动态代理类的能力。
JDK 动态代理的核心是InvocationHandler 接口和Proxy 类。
java.lang.reflect.InvocationHandler
代理工厂需要实现InvocationHandler接口, 调用代理方法时,会转向执行invoke() 方法
动态生成的代理类需要完成的具体内容需要自己定义一个类,而这个类必须实现InvocationHandler 接口。
实现InvocationHandler接口,并实现其invoke()方法
又称\"调用处理器\",他是一个接口
InvocationHandler接口是什么?
Object proxy:代理对象
Method method:真正执行的方法
Object agrs:调用第二个参数method时,传入的参数列表值
invoke() 方法有3个参数
是一个代理方法
也就是说,最后客户端请求代理时, 执行的就是该方法
invoke()方法是什么?
代理工厂类
代理工厂类到这里为止已经结束了
样例代码
如何实现一个代理工厂 ? InvocationHandler接口
java.lang.reflect.Proxy
这是生成代理类的主类
通过Proxy 类生成的代理类都继承了Proxy 类,即DynamicProxyClass extends Proxy。
生成代理对象需要用到Proxy类, 它可以帮助我们生成任意一个代理对象, 里面提供一个静态方法new Proxy Instance。
生成代理对象,需要使用Proxy对象中的newProxyInstance()方法, 返回对象可强转成传入的其中一个接口,然后调用接口方法即可实现代理
Proxy类是什么?
静态方法newProxyInstance,需要传入三个参数
加载动态代理类的类加载器
ClassLoader loader:
代理类实现的接口, 可以传入多个接口
Class<?>[] interfaces:
指定代理类的调用处理程序, 即调用接口中的方法时, 会找到该代理工厂 h, 执行invoke() 方法
InvocationHandler h:
实例化代理对象时,需要传入3个参数
把注意力放在Proxy.newProxyInstance() 这个方法上, 这是整个JDK动态代理起飞的一个方法。
生成动态代理对象的关键
可来看看它里面到底干了些什么, 把重要的代码提取出来,一些对分析无用的代码就省略掉了。
Proxy.newProxyInstance源码分析
看到第6行获取了一个动态代理对象,那么是如何生成的呢?接着往下看,
获取了一个动态代理对象
发现里面用到一个缓存proxyClassCache, 从结构来看类似于是一个map结构,
根据类加载器loader和真实对象实现的接口interfaces查找是否有对应的Class对象, 我们接着往下看get() 方法.
get() 方法
在get方法中, 如果没有从缓存中获取到Class对象, 则需要利用subKeyFactory去实例化一个动态代理对象,
而在Proxy类中包含一个ProxyClassFactory内部类, 由它来创建一个动态代理类, 所以我们接着去看ProxyClassFactory中的apply方法。
ProxyClassFactory内部类
它是生成字节码文件的方法, 它返回了一个字节数组,
字节码文件本质上就是一个字节数组, 所以proxyCLassFile数组就是一个字节码文件
ProxyGenerator.generateProxyClass
生成字节码文件的Class对象
它是一个native本地方法, 调用操作系统底层的方法创建类对象
defineClass0()
apply方法中,注意有两个非常重要的方法
而proxyName是代理对象的名字, 可以看到它利用了proxyClassNamePrefix+计数器 拼接成一个新的名字。
停留在代理对象变量上, 你会发现变量名是$Proxy 0
所以在DEBUG时, 停留在代理对象变量上, 你会发现变量名是$Proxy 0
到了这里,源码分析完了,是不是感觉被掏空了?哈哈哈哈,其实我当时也有这种感觉,不过现在你也感觉到, JDK的动态代理其实并不是特别复杂吧
Proxy.newProxyInstance
如何通过代理工厂动态生成代理对象 ?Proxy类
JDK动态代理的使用方法/涉及到两个核心类/接口
1、定义一个委托类和公共接口。
调用处理器类,即实现InvocationHandler 接口
Preprocess
Postprocess
这个类的目的:指定运行时,将生成的代理类需要完成的具体任务,包括
即代理类调用任何方法,都会经过这个调用处理器类
2、自己定义一个类(InvocationHandler接口)
需要为他指定(1)委托对象(2)实现的一系列接口(3)调用处理器类的实例。
因此可以看出一个代理对象对应一个委托对象,对应一个调用处理器实例。
3、生成代理对象(当然也会生成代理类)Proxy类
Java 实现JDK动态代理的大致步骤
目标对象强制需要实现一个接口, 否则无法使用JDK动态代理
JDK动态代理的特点
以小红代理卖香水的故事为例, 香奈儿香水提供商依旧是真实对象, 实现了Sell Perfume接口, 这里不再重新写了
重点是小红代理,这里的代理对象不再是小红一个人,而是一个代理工厂,里而会有许多的代理对象
图解动态代理
小明来到代理工厂,需要购买一款法国在售的香奈儿香水,
那么工厂就会找一个可以实际的代理对象(动态实例化)分配给小明,例如小红或者小花,让该代理对象完成小明的需求。
该代理工厂含有无穷尽的代理对象可以分配,且每个对象可以代理的事情可以根据程序的变化而动态变化,无需修改代理工厂。
如果有一天小明需要招待一个可以代购红酒的代理对象,该代理工厂依旧可以满足他的需求,无论日后需要什么代理,都可以满足
回到例子
动态代理的UML类图结构
可以看到和静态代理区别不大,唯一的变动是代理对象,我做了标注:由代理工厂生产。
代理对象是在程序运行过程中, 由代理工厂动态生成, 代理对象本身不存在Java源文件。
执行结果和静态代理的结果相同,但二者的思想是不一样的,一个是静态,一个是动态。
在客户端请求代理时,就需要用到上面这个方法。
相比静态代理的前置增强和后置增强,少了小红二字,
实际上代理工厂分配的代理对象是随机的,不会针对某一个具体的代理对象,所以每次生成的代理对象都不一样,
也就不确定是不是小红了,但是能够唯一确定的是,这个代理对象能和小红一样帮小明买到香水!
这个代理对象能和小红一样帮小明买到香水
按照之前的故事线发展,小红去代理红酒,而小明又想买法国的名牌红酒,所以去找代理工厂,让它再分配一个人帮小明买红酒,代理工厂说:“当然没问题!我们是专业的!等着!”
红酒提供商类和售卖红酒接口。
实现两个类:红酒提供商类和售卖红酒接口。
小明在请求代理工厂时,就可以实例化一个可以售卖红酒的代理
然后我们的小明在请求代理工厂时,就可以实例化一个可以售卖红酒的代理了。
代理售卖红酒了,但是我们没有修改代理工厂
期待一下执行结果,你会很惊喜地发现,居然也能够代理售卖红酒了,但是我们没有修改代理工厂,
代码
创建新的红酒提供商Sell Wine Factory和售卖红酒接口Sell Wine
在客户端实例化一个代理对象,然后向该代理对象购买红酒
回顾一下我们新增红酒代理功能时,需要2个步骤
面向扩展开放,面向修改关闭。
动态代理正是满足了这一重要原则,在面对功能需求扩展时,只需要关注扩展的部分,不需要修改系统中原有的代码,
开闭原则
如何体现出动态代理的优势呢?
jdk 动态代理
案例代码
调用具体方法的时候调用invokeHandler
java反射机制生成一个代理接口的匿名类
实现接口
JDK生成的代理类类型是Proxy(因为继承的是Proxy),CGLIB生成的代理类类型是Enhancer类型
JDK动态代理是面向接口的,CGLib动态代理是通过字节码底层继承代理类来实现(如果被代理类被final关键字所修饰,那么会失败)
Proxy.newProxyInstance()
Proxy利用InvocationHandler,动态创建一个符合某一接口的实例,生成目标类的代理对象。
Proxy
InvocationHandler是一个接口,
通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编制在一起。
InvocationHandler
主要涉及到java.lang.reflect包中的两个类,主要通过这两个类和方法实现
调用被代理类方法时默认调用此方法
创建代理类proxy实现Invocation接口,重写invoke()方法
将被代理类作为构造函数的参数传入代理类proxy
$Proxy0 extends Proxy implements Person
类型为$Proxy0
生成的代理类
代理对象会实现用户提供的这组接口,因此可以将这个代理对象强制类型转化为这组接口中的任意一个
通过反射生成对象
总结: 代理类调用自己方法时,通过自身持有的中介类对象来调用中介类对象的invoke方法,从而达到代理执行被代理对象的方法。
实现过程
JDK Proxy(JDK提供的动态代理、JDK动态接口代理)
CGLib动态代理
CGLib全称为Code Generation Library
Code Generation Library
CGLIB(Code Generation Library )
Cglib
CGLIB(Code generation Library) 不是JDK自带的动态代理
是一个强大的高性能,高质量的代码生成类库
一个强大的高性能的代码生成包
它需要导入第三方依赖
它可以在运行期扩展Java 类与实现Java接口。
一个基于 ASM 的字节码生成库
一个第三方代码生成类库,
运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。
一个代码生成的类库,可以在运行时动态的生成某个类的子类
CGLIB 是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB 做动态代理的。
CGLIB是什么?
CGLib在创建对象的时候所花费的时间却比JDK动态代理多
Spring在运行期间通过CGlib继承要被动态代理的类,重写父类的方法,实现AOP面向切面编程。
实现原理类似于 jdk 动态代理,只是他在运行期间生成的代理对象是针对目标类扩展的子类
生成对象类型为Enhancer
修改字节码生成子类去处理
asm字节码编辑技术动态创建类 基于classLoad装载
可以在运行期扩展Java类与实现Java接口
CGLib封装了asm,可以再运行期动态生成新的class。
singleton的代理对象或者具有实例池的代理,因为无需频繁的创建代理对象,所以比较适合采用CGLib动态代理,反之,则适合用JDK动态代理
Cglib特性
如果目标类没有实现接口,那么Spring AOP 会选择使用CGLIB 来动态代理目标类。
例如Spring AOP 和dynaop
它广泛的被许多AOP 的框架使用,提供方法的interception(拦截)
使用场景
它是一个字节码生成类库, 能够在运行时,动态生成代理类对 Java类 和 Java接口 扩展
Cglib 包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类
不鼓励直接使用ASM,因为它需要你对JVM 内部结构都很熟悉,JVM内部结构包括class 文件的格式和指令集
CGLIB的底层实现
JDK Proxy和CGLIB的对比
二者都是用到了两个核心的类,它们也有不同
CGLIB采用动态创建被代理类的子类实现方法拦截, 子类内部重写被拦截的方法, 所以CGLIB不能代理被final关键字修饰的类和方法
为Java接口做代理
为普通的Java类做代理
如果需要代理的类没有实现接口, 可以选择Cglib作为实现动态代理的工具
如果想代理没有实现接口的类,就可以使用CGLIB实现。
CGLIB可以代理没有实现接口的Java类
CGLIB可以代理大部分类
使用cglib 代理的对象则无需实现接口,达到代理类无侵入。
CGLIB能够
CGLIB为Java的代理做了很好的扩展
仅能够代理实现了接口
只能为实现了接口的Java类,做代理
JDK 的动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口。
使用动态代理的对象,必须实现一个或多个接口
JDK Proxy
最明显的不同
JDK创建代理有一个限制,就是只能为接口创建代理实例,
而对于没有通过接口定义业务方法的类,则可以通过CGLib创建动态代理。
和JDK动态代理相比较
1.6和1.7时,CGLib更快
1.8时,jdk更快
CGLib所创建的动态代理对象在实际运行时,性能要比JDK动态代理高
CGLIB与JDK Proxy的区别
实现一个代理工厂的根接口
代理工厂需要实现Method Interceptor接口, 并重写方法, 内部关联真实对象, 控制第三者对真实对象的访问;
MethodInterceptor接口
代理工厂内部暴露getInstance(Object real Object) 方法, 用于从代理工厂中获取一个代理对象实例。
创建动态代理对象的类
Enhancer类用于从代理工厂中实例化一个代理对象, 给调用者提供代理服务,
Enhancer类
CGLIB的使用方法/CGLIB代理中有两个核心的类
被代理对象
Object o
被拦截的方法
Method method
被拦截方法的所有入参值
Object[] objects
方法代理, 用于调用原始的方法
invoke
invokeSuper
对于method Proxy参数调用的方法, 在其内部有两种选择
MethodProxy methodProxy
intercept方法涉及到4个参数
故事的结构图
CGLIB的POM依赖
带有-node p的依赖内部已经包括了ASM字节码框架的相关代码, 无需额外依赖ASM
另外一个CGLIB包, 二者的区别是
(1)导入依赖
定义代理工厂SellProxyFactory
在getInstance方法中, 利用Enhancer类实例化代理对象(可以看作是小红) 返回给调用者小明,即可完成代理操作。
(2)定义代理工厂SellProxyFactory
如果小明需要小红代理购买红酒,该如何做呢?
以小明找代理工厂买法国香水这个故事背景为例子
关注点依旧放在可扩展性和可维护性上, Cglib依旧符合开闭原则
cglib 动态代理
CGLIB(Code generation Library ,Cglib 动态代理)
Spring提供了两种方式来生成代理对象:
在目标类实现接口时,使用 JDK 动态代理创建代理对象
当目标类没有实现接口时,使用 CGLib 实现动态代理的功能
如果要被代理的对象是个实现类,那么Spring会使用JDK动态代理来完成操作(Spirng默认采用JDK动态代理实现机制)
如果要被代理的对象不是实现类,那么Spring会强制使用CGLib来实现动态代理。
CGLib 与 JDK 动态代理之间可以相互补充
具体使用哪种方式生成由AopProxyFactory根据AdvisedSupport对象的配置来决定
默认的策略是如果目标类是接口,则使用JDK动态代理技术,否则使用Cglib来生成代理。
AOP两种代理方式(常见的动态代理实现,实现动态代理有两种方式)
基于源码级别的API
开源的生成 Java 字节码的类库
比基于字节码的ASM简单
dubbo源码和mybatis源码的时候发现代理用的是javassist
Javassist (Java编程助手)使操作Java字节码变得简单。
它是一个用于编辑Java字节码的类库;
跟其他类似的字节码编辑器不同的是,它使Java程序能够在运行时定义一个新类,并在JVM加载类文件时修改它。
Javassist允许您检查、编辑和创建Java二进制类。
Javassist并不是唯一处理字节码的库,但它有一个特别功能,使其成为一个重要的开始来尝试字节码工作:
你可以使用Javassist改变一个Java类的字节码,而不需要学习任何关于字节码或Java虚拟机(JVM)的体系结构。
Javassist可以是一个很好的工具用于向类中添加新方法,以及在调用方和被调用方两边插入before/after/around通知。
面向切面编程:
Javassist另一个应用就是运行时反射;
Javassist允许Java程序使用一个元对象,该元对象控制基级别对象上的方法调用。
不需要专门的编译器或虚拟机。
反射:
要使用此级别的API,您需要详细了解Java字节码和类文件格式,而此级别的API允许您对类文件进行任何类型的修改。
Javassist还提供了用于直接编辑类文件的低级API。
Javassist是什么
如果用户使用源代码级API,他们可以不需要了解Java字节码的规范的前提下编辑类文件。
整个API仅使用Java语言的词汇表设计。甚至你可以以源文本的形式插入字节码中;Javassist动态编译它
源级别
字节码级API允许用户作为编辑器直接编辑类文件
字节码级别
Javassist提供了两种级别的API
基于javassist开发,不需要了解字节码的一些知识,
Javassist源代码级API比ASM中实际的字节码操作更容易使用
Javassist在复杂的字节码级操作上提供了更高级别的抽象层。
Javassist源代码级API只需要很少的字节码知识,甚至不需要任何实际字节码知识,因此实现起来更容易、更快。
Javassist使用Java源代码的简化版本,然后将其编译成字节码。这使得Javassist非常容易使用,但是它也将字节码的使用限制在Javassist源代码的限制之内。
封装性好、易用
其封装的一些工具类可以简单实现一些高级功能。比如HotSwaper。
功能强大
Javassist特点、优点
javassist提供者动态代理接口最慢,比JDK自带的还慢
Javassist使用反射机制,这使得它比运行时使用Classworking技术的ASM慢。
Javassist缺点
ASM是一个通用的Java字节码操作和分析框架。
它可以直接以二进制形式修改现有类或动态生成类。
提供了通用的转换和分析算法,允许轻松地组装定制的复杂转换和代码分析工具。
ASM提供了与其他字节码框架类似的功能,但它关注的是使用的简单性和性能。
因为它的设计和实现尽可能的小和快,所以它非常适合在动态系统中使用。
ASM是一个Java类操作工具,用于动态生成和操作Java类,这是实现可适应系统的有用技术。
ASM基于一种新的方法,与现有的相似工具相比,后者包括使用“访问者”设计模式,而不显式地用对象表示被访问的树。
对于大多数实际需求,这种新方法比现有的工具提供了更好的性能。
ASM是什么?
ASM比Javassist快得多,并且提供了更好的性能。
性能更快
灵活性高
ASM特点、优点
ASM在创建class字节码的过程中,操纵的级别是底层JVM的汇编指令级别
这要求ASM使用者要对class组织结构和JVM汇编指令有一定的了解。
ClassWriter接口
门槛更高
ASM的缺点
更简单的方法来动态操作或创建Java类,应该使用Javassist API 。
更加注重性能地方,应该使用ASM库。
两者的使用场景
Javassist 与ASM对比
实现原理、实现模式、代理模式
Spring AOP是什么?
Spring AOP原理
CGLIB
JDK
Spring中两种动态代理
作用?
AOP如何实现日志管理?
SpringAOP常见问题
AOP面向切面
事务就是一组由于逻辑上紧密关联而合并成一个整体(工作单元)的多个数据库操作,这些操作要么都执行,要么都不执行
如果需要某一组操作具有原子性,就用注解的方式开启事务,按照给定的事务规则来执行提交或者回滚操作
如果在dao层,回滚的时候只能回滚到当前方法,但一般我们的service层的方法都是由很多dao层的方法组成的
如果在dao层,commit的次数会过多
事务管理一般在Service层
事务(TRANSACTION)是作为单个逻辑工作单元执行的一系列操作,这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行 。
事务是一个不可分割的工作逻辑单元
什么是事务
概要
“原子”的本意是“不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。
事务的原子性要求事务中的所有操作要么都执行,要么都不执行
事务是一个完整的操作。事务的各步操作是不可分的(原子的);要么都执行,要么都不执行。
原子性(Atomicity)
“一致”指的是数据的一致,具体是指:所有数据都处于满足业务规则的一致性状态。
一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。
如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。
当事务完成时,数据必须处于一致状态。
一致性(Consistency)
在应用程序实际运行过程中,事务往往是并发执行的,所以很有可能有许多事务同时处理相同的数据,
因此每个事务都应该与其他事务隔离开来,防止数据损坏。
隔离性原则要求多个事务在并发执行过程中不会互相干扰。
对数据进行修改的所有并发事务是彼此隔离的,这表明事务必须是独立的,它不应以任何方式依赖于或影响其他事务。
隔离性(Isolation)
持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。
通常情况下,事务对数据的修改应该被写入到持久化存储器中。
事务完成后,它对数据库的修改被永久保持,事务日志能够保持事务的永久性。
持久性(Durability)
事务的特性(ACID)
ACID 属性
事务必须具备的四个属性
Conn.setAutoCommite(false); // 设置手动控制事务
粒度较细,比较灵活,但开发起来比较繁琐: 每次都要开启、提交、回滚
用户通过代码的形式手动控制事务
\t ①获取数据库连接Connection对象
②取消事务的自动提交
③执行操作
④正常完成操作时手动提交事务
⑤执行失败时回滚事务
⑥关闭相关资源
使用步骤
编程式事务管理需要将事务管理代码嵌入到业务方法中来控制事务 的提交和回滚。
在使用编程的方式管理事务时,必须在每个事务操作中包含额外的事务 管理代码。
相对于核心业务而言,事务管理的代码显然属于非核心业务,如果多个模块都使用同样模式的代码进行事务管理,显然会造成较大程度的代码冗余
优缺点
编程式事务管理
Spring提供对事务的控制管理
大多数情况下声明式事务比编程式事务管理更好:它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。
Spring在不同的事务管理API之上定义了一个抽象层,通过配置的方式使其生效,从而让应用程序开发人员不必了解事务管理API的底层实现细节,就可以使用Spring的事务管理机制。
声明式事务管理
事务的分类
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。
事务传播行为用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的时候事务如何传播。
例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
默认值. 默认方法
使用调用者的事务(全程一个事务)
如果当前方法已经有事务了,加入当前方法事务
Propagation.REQUIRED
如果当前方法有事务了,当前方法事务会挂起,
再为加入的方法开启一个新的事务,直到新的事务执行完、当前方法的事务才开始
Propagation.REQUIRES_NEW
其余五种方式(拓展学习),其它的行为作为了解
@Transactional注解的propagation属性中定义
事务传播行为
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。
一个事务与其他事务隔离的程度称为隔离级别。
SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
最低的隔离级别,
允许读取尚未提交的数据变更,
可能会导致脏读、幻读或不可重复读
没视图概念 都是返回最新的
TransactionDefinition.ISOLATION_READ_UNCOMMITTED:
读未提交:READ UNCOMMITTED
oracle默认的隔离级别
允许读取并发事务已经提交的数据,
可以阻止脏读,但是幻读或不可重复读仍有可能发生
不同的read view
TransactionDefinition.ISOLATION_READ_COMMITTED:
读已提交:READ COMMITTED
Mysql默认的隔离级别
对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,
可以阻止脏读和不可重复读,但幻读仍有可能发生。
用一个read view
TransactionDefinition.ISOLATION_REPEATABLE_READ:
可重复读:REPEATABLE READ
最高的隔离级别,完全服从ACID的隔离级别。
通常情况下也不会用到该级别。
所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,
也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。
TransactionDefinition.ISOLATION_SERIALIZABLE:
串行化/序列化:SERIALIZABLE
5.5之前回滚段删了文件也不会变小
没更早的read view删除
回滚日志
使用后端数据库默认的隔离级别,
Mysql 默认采用的 REPEATABLE_READ隔离级别
Oracle 默认采用的 READ_COMMITTED隔离级别.
使用后端数据库默认的隔离级别:TransactionDefinition.ISOLATION_DEFAULT:
@Transactional注解的isolation属性中定义
事务隔离级别
指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。
事务超时属性
对事物资源是否执行只读操作
事务只读属性
定义了哪些异常会导致事务回滚而哪些不会。
默认情况下,事务只有遇到运行期异常时才会回滚,而在遇到检查型异常时不会回滚,也可以用用户自己定义
回滚规则
事务属性
采用不同的连接器
共享链接
用AOP 新建立了一个 链接
ThreadLocal 当前事务
前提是 关闭AutoCommit
事务实现原理
Spring从不同的事务管理API中抽象出了一整套事务管理机制,让事务管理代码从特定的事务技术中独立出来。开发人员通过配置的方式进行事务管理,而不必了解其底层是如何实现的。
Spring的核心事务管理抽象是PlatformTransactionManager。它为事务管理封装了一组独立于技术的方法。无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。
事务管理器可以以普通的bean的形式声明在Spring IOC容器中
1) DataSourceTransactionManager:在应用程序中只需要处理一个数据源,而且通过JDBC存取。
2) JtaTransactionManager:在JavaEE应用服务器上用JTA(Java Transaction API)进行事务管理,基本不用
3) HibernateTransactionManager:用Hibernate框架存取数据库,不涉及
实现
事务管理器
Spring从不同的事务管理API中抽象出了一整套事务管理机制,让事务管理代码从特定的事务技术中独立出来。
开发人员通过配置的方式进行事务管理,而不必了解其底层是如何实现的。
Spring的核心事务管理抽象是PlatformTransactionManager。
它为事务管理封装了一组独立于技术的方法。
无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。
在应用程序中只需要处理一个数据源,而且通过JDBC存取。
1) DataSourceTransactionManager
在JavaEE应用服务器上用JTA(Java Transaction API)进行事务管理,基本不用
2) JtaTransactionManager
用Hibernate框架存取数据库,不涉及
3) HibernateTransactionManager
(平台)事务管理器
Spring并不直接管理事务,而是提供了多种事务管理器,通过PlatformTransactionManager这个接口,
Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。
PlatformTransactionManager
事务定义属性(事务隔离级别、传播行为、超时、只读、回滚规则)
TransactionDefinition
事务运行状态
TransactionStatus
Spring事务管理接口
SpringBoot中是如何使用事务的?
Spring如何实现事务管理?
事务的类型有那几种?
事务的隔离级别?
Spring事务常见问题
声明式事务
Spring 第三方结合
Spring 第三方框架集成
如何自定义注解
你写过自定义注解吗?
注解的生命周期?
bean注入与装配的的方式有很多种,可以通过xml,get set方式,构造函数或者注解等。
简单易用的方式就是使用Spring的注解了,Spring提供了大量的注解方式。
常用注解
把一个类作为一个IoC 容器,它的某个方法头上如果注册了@Bean,就会作为这个Spring 容器中的Bean。
@Configuration
作用域
用于指定scope 作用域的(用在类上)。
@Scope
用于指定初始化方法(用在方法上)。
初始化注解。
@PostConstruct
用于指定销毁方法(用在方法上)
摧毁注解默认单例启动就加载。
@PreDestory
表示延迟初始化。
@Lazy(true)
定义Bean 初始化及销毁时的顺序。
@DependsOn
自动装配时当出现多个Bean 候选者时,
被注解为@Primary 的Bean将作为首选者,否则将抛出异常。
@Primary
默认按类型装配,
如果想使用按名称装配,可以结合@Qualifier注解一起使用。
如下: @Autowired@Qualifier(\"personDaoBean\") 存在多个实例配合使用。
@Autowired
默认按名称装配,
当找不到与名称匹配的bean 才会按类型装配。
泛指组件,当组件不好归类的时候,可以使用这个注解进行标注。
@Component
用于标注业务层组件。
@Service
用于标注控制层组件
@Controller
用于标注数据访问组件,即DAO 组件。
@Repository
Spring 常用注解
指的是组件
@Controller,@Repository 和@Service 注解都被@Component 修饰
用于代码中区分表现层, 持久层和业务层的组件
泛指组件,代码中,当组件不好归类的时候,可以使用@Component注解进行标注。
@Component
Spring 中的这几个注解有什么区别
当前版本只有区分的作用,未来版本可能会添加更丰富的功能。
Spring与Spring常用的注解
通俗的讲:API 和SPI 都是相对的概念,差别只在语义上
Application Programming Interface
直接被应用开发人员使用
大多数情况下,都是实现方来制定接口并完成对接口的不同实现,调用方仅仅依赖却无权选择不同实现。
API
Service Provider Interface。
被框架扩展人员使用
如果是调用方来制定接口,实现方来针对接口来实现不同的实现。调用方来选择自己需要的实现方。
SPI
API 和SPI 的关系和区别
定义一组接口,并写出接口的一个或多个实现
(假设是org.foo.demo.IShout)
定义一组接口
org.foo.demo.animal.Dog
org.foo.demo.animal.Cat
并写出接口的一个或多个实现,假设是
步骤1:定义一组接口,并写出接口的一个或多个实现
新建文件,并写入对应的内容
在src/main/resources/ 下建立/META-INF/services 目录
新增一个以接口命名的文件(org.foo.demo.IShout 文件)
org.foo.demo.animal.Dog
内容是要应用的实现类,每行一个类,这里是
步骤2、新建目录和以接口命名的文件,并写入对应的内容
使用ServiceLoader 来加载配置文件中指定的实现。
步骤3、使用ServiceLoader 来加载配置文件中指定的实现
代码输出:
如何定义SPI
看ServiceLoader 类的签名类的成员变量:
ServiceLoader.load 方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量,
loader(ClassLoader 类型,类加载器)
acc(AccessControlContext 类型,访问控制器)
providers(LinkedHashMap 类型,用于缓存加载成功的类)
lookupIterator(实现迭代器功能)
包括:
应用程序调用ServiceLoader.load 方法
ServiceLoader 先判断成员变量providers 对象中(LinkedHashMap 类型)是否有缓存实例对象,
如果有缓存,直接返回。
读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称;
通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化;
把实例化后的类缓存到providers 对象中(LinkedHashMap 类型);
然后返回实例对象。
如果没有缓存,执行类的装载:
应用程序通过迭代器接口获取对象实例
参考具体源码,梳理了一下,实现的流程如下:
SPI 的实现原理
API&SPI
动态代理是什么?有哪些应用?
怎么实现动态代理?
Spring框架
0 条评论
回复 删除
下一页