第三章Spring第一讲-讲解Spring的基础和高级应用-学习笔记
- 前言
- 第三章Spring第一讲-讲解Spring的基础和高级应用
- 主题
- 学习目标
- Spring简介
- Spring IoC/DI 的三种应用方式-以及基础概念
- 基于XML方式的IoC和DI的应用
- 基于注解和XML方式混用的IoC和DI的应用
- 基于纯注解方式的IoC和DI的应用 (与SpringBoot类似)
- Spring基于AspectJ的AOP使用方式
- Spring整合JUnit
- 事务支持
- 结语
前言
- 2020.12.28 是正式开始学本课的日子,这一课讲解spring入门和一些高级的应用。本课为录播课因此学习起来会相对简单,我将跟着课程内容做笔记,并且尽量自己做出演示来验证知识点。
- 本文和本文内一切内容,均来自我对于开课吧Java企业级分布式架构师010期课程的学习笔记,并在我自身的理解上整理而成.
- 学习方式依旧是3+模型,开始学习。
- 本次学习课件的pdf文档加了水印,不方便复制里面的插图,因此需要配图的部分我会自己绘制,同时也加深理解。
本次学习的部分示例代码来自于课件资料,如有测试需求我会试图举一反三。
第三章Spring第一讲-讲解Spring的基础和高级应用
主题
Spring IoC和AOP的应用、事务支持
学习目标
- 明确IoC、DI、AOP、Spring容器各自的概念(Spring简介)
- 掌握Spring IoC/DI 的XML、注解和XML混用、纯注解应用方式(核心基础知识)
- 掌握Spring基于AspectJ的AOP使用方式(核心高级知识)
- 掌握Spring整合Junit和声明式管理事务(组件知识)
Spring简介
Spring 是一个分层的JavaSE/EE 一站式 轻量级开源框架。使用这个框架可以轻松的完成一个网站相关功能的搭建。
官网:https://spring.io/
(通常说的Spring框架指Spring Framework)
Spring的7大特点
方便解耦,简化开发
通过IoC容器,我们可以将对象之间的依赖关系由Spring进行控制,避免硬编码造成的过渡程序耦合。(即完成清晰的架构拆解,使得各个模组实现高内聚,低耦合。)
AOP编程的支持
通过Spring提供的AOP功能,方便进行面向切面的编程。使得许多采用OOP方式不容易实现的功能可以被轻易实现。(什么是AOP什么是OOP后面会讲,AOP功能简单表述就是在不修改目标类的前题下,去增强目标类的功能,怎么做后面会说;OOP是面向对象编程)
声明式事务的支持
Spring采用声明式方式进行管理事务,简化了事务管理代码,使得开发变的轻松。(编程式或者声明式管理事务,声明式是改良版本)
方便程序的测试
可以用非容器依赖的编程方式进行几乎所有的测试工作,在Spring里,测试动作变的轻松。(整合了JUnit)
方便集成各种优秀的框架
Spring不排斥优秀的各种开源框架,相反还提供了部分框架的直接支持。(记着spring对框架的兼容性高就好,具体要兼容时还是要试一试)
降低Java EE API 的使用难度
Spring对于很多难用的Java EE API (如JDBC,JavaMail,远程调用等)提供了简易封装。(很普通的优化,但是对于开发者来说很友好,记一个JDBC足够)
源码的设计理念很先进
Spring的源码设计精妙、充分展示了Java设计模式在架构设计中的思维构设。对于Spring源码的学习有助于提高自身的java相关水平。(真的么,如果是,第二期学习计划里我就专心学习Spring的几个重点知识)
总结
Spring具有如下6个优点:
- 低侵入式设计,代码污染低。(体现在什么地方?)
- 独立于各种应用服务器。(一站式)
- DI机制降低了业务对象替换的复杂性,提高了组件之间的解耦。(低耦合)
- AOP支持允许将一些通用任务(安全、事物、日志等)进行集中式管理,提供了更好的复用。(AOP的应用还要广泛,这只是一种方式)
- Spring中的ORM和DAO提供了与第三方持久层框架的良好整合,并简化了底层的数据库访问。(JDBC的优化很实用)
- Spring并不强制应用依赖于Spring,开发者可自行选用所需部分。(过于高级,跳过理解)
Spring版本介绍(略)
根据自身需求选择版本,5.开头的版本差别不大。(不值得记忆,略过)
Spring体系结构
终于给我找到一个图:
图解
横向分层,纵向表明所属(从上至下是从细节到具体整体的描述)
- Test:用于进行单元测试使用的模块,可以执行上方所有功能
- Core Container:就是IoC,接下来要讲的重点内容。这里包含了4大模块,这4大模块是spring架构体系(包括springboot等)的核心容器,是通用的。
- Data Access/Integration:企业级数据或设备的解决方案,包括持久层、消息服务、事物等
- Web:企业级web的解决方案,这部分包含了springMVC的设计
Spring核心概念介绍
- IoC:Inverse of Control,控制反转。对象的创建权力由程序反转给框架。(这是最重要的核心)
- DI:Dependency Injection,依赖注入。在Spring框架负责创建Bean对象时,动态的将依赖对象注入到Bean组件中。(和IoC相互依赖,在创建对象后进行赋值)
- AOP:Aspect Oriented Programming,面向切面编程。在不修改目标对象源码的情况下,增强IoC容器中的Bean的功能(关键在于不需要修改源码)
- Spring容器:指的就是IoC容器,底层就是一个BeanFactory。(管理Bean)
Spring IoC/DI 的三种应用方式-以及基础概念
三种方式中选择自身喜欢或者项目适宜的方式。
基于XML方式的IoC和DI的应用
IoC配置
在Spring的xml文件中通过一个bean标签,完成IoC的配置
举例:
<bean id="student" class="com.kkb.spring.ioc.xml.po.Student" init-method="initMethod" destroy-method="destroyMethod">
<property name="name" value="zhangsan"></property>
<property name="course" ref="course"></property>
</bean>
如上,为class属性所标注的类Student创建唯一标识student。
在类初始化时调用方法initMethod,在类销毁时调用destroyMethod。这种方式是以默认方式进行bean标签的实例化,也是最常用的方式。
同时,上述代码的中的子标签property 则是依赖注入中的常用方法(set方法注入),分别注入了简单类型value和引用类型ref。以这种方式进行依赖注入需要class选择的目标类中包含setter方法。
(这个举例中没有指定对象的作用范围,在这种情况属于默认,而默认值为singleton 表示单例)
bean标签介绍
bean标签作用:
用于配置被spring容器管理的bean的信息。
默认情况下调用目标类的无参构造函数。如果缺少这个函数则会创建失败(一般情况下,实体类有自身的默认无参构造,但是如果复写了构造函数就应补上一个)
bean标签属性:
- id:为类所连接的对象自定义唯一标识。用于获取对象
- class:指定类的全限定名。用于反射创建对象。默认情况下调用无参构造函数
- init-method:预热,指定类中的初始化方法名称
- destroy-method:销毁,指定类中销毁方法名称。常用于释放内存。
- scope:指定对象的作用范围
- ssingleton:默认值,常用,单例(容器内只有一个对象),生命周期如下:
对象出生:当应用加载,创建容器时,对象就被创建了。
对象存活:与容器共存
对象死亡:当应用卸载,销毁容器时,对象也被销毁。 - prototype:多例,不常用,每次访问对象时,都会创建(当多次访问要求每次的参数不一致时,常用多例,因为单例本质上近似是全局变量,对于单个通路不应受其他通路的影响),生命周期如下:
对象出生:当使用对象时,创建新的对象实例。
对象活着:只要对象在使用中,就一直活着
对象死亡:当对象长时间不用时,被java的垃圾回收器回收了(注意,会被回收)
- ssingleton:默认值,常用,单例(容器内只有一个对象),生命周期如下:
- request:(非重点,略)将Spring创建的Bean对象存入request域中。
- session:(非重点,略)将Spring创建的Bean对象存入session域中。
- global session:(非重点,略)WEB项目中,应用在Portlet环境,如果没有Portlet环境那么这个参数等效于session。
bean实例化的三种方式
第一种:(重点)使用默认无参构造
bean中没有无参构造函数,将会创建失败。(之前我遇到过类似的错误,因为我覆写了构造函数,后来解决方案是补上一个无参构造函数)
举例:
<bean id="student" class="com.kkb.spring.ioc.xml.po.Student" />
第二种:(略)静态工厂
使用StaticFactory类中的静态方法createUserService创建对象,并存入Spring容器。
这和第三种都是采用工厂来创建对象,不常用,看到了要能明白是什么然后找资料学习,现在略
举例:(略)
判别方式:在xml文件的bean标签中只会有属性:factory-method。
第三种:(略)实例工厂
先把工厂的创建给spring来管理,然后用工厂的bean来调用。(因为没有静态,所以先new一个实例)。
判别方式:在xml文件的bean标签中会有属性:factory-method和factory-bean。
DI配置
概述
什么是依赖(是属性)
依赖指的就是Bean实例中的属性。
依赖(属性)分为:简单类型,POJO类型,集合数组类型的属性
什么是依赖注入(指赋值操作)
依赖注入:Dependency Injection。它是spring框架核心IoC的具体实现(指一种方法)
为什么要进行依赖注入(为了给目标属性赋值)
在编写程序时,通过控制翻转(IoC),把对象的创建交给了Spring,但是代码中不能出现没有依赖的情况(创建对象时需要赋值的情况)。
那么如果一个bean中包含了一些属性,那么Spring帮我们实例化了Bean对象后,也需要将对应的属性信息进行赋值操作,这种操作就是依赖注入。
依赖注入的方式
set方法注入(重点)
手动装配方式(XML方式)。
需要配置bean标签的子标签property;需要配置bean中指定的setter方法
类端:
package com.kkb.spring.ioc.xml.po;
public class Course {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
xml端
<bean id="course" class="com.kkb.spring.ioc.xml.po.Course" >
<property name="name" value="english"></property>
</bean>
自动装配方式(略)
(注解方式,后面讲解)。
构造函数注入
用类中构造函数给成员赋值。(这种情况可能跟其他地方需要无参构造的请求冲突,因为这种方法需要类提供构造有参构造函数)。
这种赋值动作是通过配置的方式,让Spring框架为我们注入参数。举例:
类端:
package com.kkb.spring.ioc.xml.po;
public class Course_2 {
private String name;
public Course_2(String name) {
this.setName(name);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
xml端:
<bean id="course2" class="com.kkb.spring.ioc.xml.po.Course_2" >
<constructor-arg name="name" value="english" ></constructor-arg>
</bean>
使用p名称空间注入数据
本质上还是调用set方法。(详略)
第一步:在schema的名称空间加入:
xmlns:p = “http://www.springframework.org/schema/p”
第二步:使用p名称空间的语法
简单类型为p:属性名 = “”
引用类型为p:属性名-ref= “”
举例对比这俩种相同功能的写法:
set:
<bean id="student" class="com.kkb.spring.ioc.xml.po.Student" >
<property name="name" value="zhangsan"></property>
<property name="course" ref="course"></property>
</bean>
p:
<bean id="student" class="com.kkb.spring.ioc.xml.po.Student" p:name = "zhangsan" p:course-ref = "course">
</bean>
依赖注入不同的属性
以set方式进行各类的讲解。
简单类型(value)
<property name="name" value="zhangsan"></property>
引用类型(ref)
ref是reference的缩写
<property name="course" ref="course"></property>
集合类型(数组)
- 如果是数组或者list集合,注入配置文件的方式相同。如果是简单类型则用value标签,如果是pojo类则用bean标签(bean标签用法类似嵌套关系)
<bean id="students" class="com.kkb.spring.ioc.xml.po.Students" >
<property name="names">
<list>
<value>张三</value>
<value>李四</value>
</list>
</property>
</bean>
- 如果是Set集合,注入的配置文件方式如下:
可以看出用法和list类似,同理,value对应简单类,其余的用bean
<bean id="students2" class="com.kkb.spring.ioc.xml.po.Students" >
<property name="names">
<set>
<value>张三</value>
<value>李四</value>
</set>
</property>
</bean>
- 如果是Map集合,注入配置的方式如下
map方式是以键值对的模式存储的
<bean id="students3" class="com.kkb.spring.ioc.xml.po.Students2" >
<property name="nameMaps">
<map>
<entry key = "张三" value = "1"/>
<entry key = "李四" value = "21"/>
</map>
</property>
</bean>
- 如果是Properties集合的方式,注入的配置如下
<property name="pro">
<props>
<prop key = "uname" >root</prop>
<prop key = "pass" >6543</prop>
</props>
</property>
基于注解和XML方式混用的IoC和DI的应用
因为有时公司项目中新老项目的交互,迁移都有可能会导致需要共存注解方式和xml方式,所以在xml的基础上进行知识拓展,研究在以下的主要情况内,注解模式将如何取代xml模式。(工作中也通常是以混用的方式进行开发。)
IoC注解使用方法
第一步:在Spring配置文件中,配置context:component-scan标签
注意图上划分的部分,beans部分配置的内容是为了使用这个标签,如果没配置,则不可使用。
第二步:类前注释@Component或者其衍生注解@Controller、@Service、@Repository
关于这4个注解,在这种情况下,实现的功能是相同的,可以任意替换。下面会简单介绍这几个的区别。
代码举例:
package com.kkb.spring.ioc.annotation.po;
import org.springframework.stereotype.Component;
@Component("course")
public class Course {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
常用注解举例
IoC注解(创建对象)
对于xml中的<bean id =”” class= “”/>
有4种注解方式(本质上是同一种)
注意:如果注解中有且只有一个属性要赋值时,且属性名为value时,value在赋值时可以不写。
@Component注解
- 作用:把资源交给Spring来管理,相当于在xml中配置一个bean。
- 属性:value(括号内的内容),相当于bean标签里的id属性值,当没有给出value值时,默认生成首字母小写的字符串作为id属性(唯一标识)
@Controller&@Service&@Repository注解
都是针对@Component的衍生注解,作用和属性相同,仅用于编程人员进行语义区分(写错不影响功能,但是影响人对代码的理解)。
应用场景为:
- @Controller:一般用于表现层的注解
- @Service:一般用于业务层的注解
- @Repository:一般用于持久层的注解
DI注解(依赖注入)
对于xml中的
<property name="" value=" "></property>
<property name="" ref=""></property>
有5种注解应对不同情况:
@Autowired-默认按类型适配
- @Autowired默认按类型适配(byTpye)
- @Autowired是由AutowiredAnnotationBeanPostProcessor类实现(这个类直接翻译为:自动连线注释Bean后处理器)
- @Autowired是Spring自带的注解
- @Autowired默认情况下要求依赖对象必须存在,如果需要允许null值,可以设置它的required属性为false,如:@Autowired(required = false)
如果想要按名称适配则适宜采用@Qualifier
@Qualifier-按名称适配
- @Qualifier在自动按类型注入的基础上,再按照Bean的id注入
- @Qualifier在给字段注入时不能独立使用,必须和 @Autowired(注意不是@Autowire,课件里写错了)一起使用
- @Qualifier在给方法参数注入时,可以独立使用。
@Resource-根据情况选择按名称适配或者按类型适配
- @Resource默认按名称适配(byName),可以通过name属性指定名称,如果没有指定name属性,当注解写在字段上时,默认取字段名进行按照名称查找,当找不到与名称匹配的bean时才按照类型进行装配,但是如果指定了name属性就一定进行按照名称进行装配。
- @Resource属于J2EE JSR250规范的实现
推进使用@Resource注解,因为它是属于J2EE的,减少了于Spring的耦合。这样代码看起来就比较美观。
@Inject-根据类型进行自动装配的,根据情况也可以按名称装配
- @Inject是根据类型进行自动装配的如果需要按名称进行装配,则需要配合注解:@Named;
- @Inject 属于JSR330中的规范,使用需要导入javax.inject.Inject,实现注入。
- @Inject可以作用在变量、setter方法、构造函数上。
@Value-给基本类型和Sring类型注入值
- @Value可以使用占位符获取属性文件中的值
举例:
@Value(“${name}”)//name是properties文件中的key
private String name;
@Autowired、@Resource、@Inject区别
- @Autowired是Spring自带的,@Inject是JSR330规范实现的,@Resource是JSR250规范实现的,后两者需要导入不同的包。
- @Autowired、@Inject用法基本一样,但是@Autowired多了一个request属性
- @Autowired、@Inject默认按照类型适配,@Resource是按照名称匹配的
- 如果需要按照名称匹配,需要@Autowired和@Qualifier一起使用,@Inject和@Name一起使用
改变Bean作用范围的注解:@Scope
对于xml中的里的scope属性,有着特定注解来实现功能@Scope
使用方法举例:
@Scope(singleton)
生命周期相关注解(初始化和销毁)
相对于bean标签里的初始化init-method和销毁destroy-method分别有着@PostConstruct和@PreDestroy来实现。
用法为各自注解的value值进行赋值,值为期望方法。
关于注解和XML的选择问题
注解的优势
配置简单,维护方便(代码量少,适用于设计末端,设计完毕后基本不进行变化)
XML的优势
修改时,不用修改源码。(实现了模块的拆分,减少了变数,适用于可能会需要修改的部分)
Spring管理Bean方式的比较
比较对象 | 基于XML配置 | 基于注解配置 |
---|---|---|
Bean定义 | <bean id = “…” class = “…”/> | @Component和衍生类@Repository、@Service、@Controller |
Bean名称 | 通过id属性或者name属性指定 | @Component和衍生类的属性,例如@Component(“person”) |
Bean注入 | <property> 标签,或者p命名空间 | 5种方式,建议使用@Resource |
生命过程、Bean作用范围 | 初始化Init-method;销毁destroy-method;范围scope | 初始化@PostConstruct;销毁@PreDestroy;范围@Scope |
适合场景 | 接入第三方设计,或者代码变动较大 | 完全由用户设计,末端开发 |
基于纯注解方式的IoC和DI的应用 (与SpringBoot类似)
Spring架构既能支持xml方式,又能支持注解方式,并且可以共存。那么思考一个问题:**能不能不断减少XML部分最终只有注解部分呢。**这种设计思想最后生成了SpringBoot架构,而其原生的Spring其他已经能够满足纯注解的需求。
从混合开发转向纯注解需要解决的问题
在绝大部分场景注解和XML的相互替换已经很普遍,但是想要完全替换成注解模式,整体上需要解决以下3个问题:
问题1:能否去掉扫描注解配置
在共存模式下,开启注解功能需要XML里配置相关信息:
<!-- 开启注解扫描 -->
<context:component-scan base-package="com.kkb.spring.ioc.annotation.po"/>
问题2:如何导入外源性Bean配置(导入第三方)
在之前的XML和注解方式比较表格中,XML的一大优势就是方便导入第三方资源,现在这部分动作需要用注解来实现应该怎么做
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
问题3:如何以注解方式创建ApplicationContext
目前创建ApplicationContext需要配置的spring-ioc.xml文件
ApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-ioc.xml");
@Configuration(相当于配置bean标签的xml文件)
介绍
相当于spring的XML配置文件
从Spring3.0(目前常用5.0)开始可以使用@Configuration定义配置类,可替换XML配置文件
配置类内部包含一个或多个被@Bean(相当于bean标签,后面会讲)注解的方法,这些方法将会被AnnotationConfigApplicationContext或者AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义对象,初始化Spring容器
属性
- value:用于指定配置类的字节码
示例
@Configuration
public class SpringConfiguration {
//初始化化时调用此无参构造函数
public void SpringConfiguration() {
//TODO
}
}
@Bean(注册bean对象,主要用来配置非自定义的Bean)
介绍
- 相当于标签
- 作用为:注册bean对象,主要用来配置非自定义的Bean(比如SQLSessionFactory)
- @Bean标注在方法上(标注在能返回实例的方法上)
- @Bean注解默认作用域为单例作用域,如果需要配置为多例则可以搭配@Scope(“prototype”)使用
属性
- name:等效于bean标签的id,如果不做说明则默认为标注的方法名
示例:
@Bean(name = "sqlFactoryuserService")
@Scope("prototype")
public SqlSessionFactory userService() {
SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory();
sqlSessionFactory.setxxx();
return sqlSessionFactory;
}
@ComponentScan(是组件扫描器,扫描@Component和其衍生注解所注解的类)
介绍
- 对应xml文件相当于context:component-scan标签。
- 功能是组件扫描器,扫描@Component和其衍生注解所注解的类。
- 使用时该类是编写在类上面的,一般配合@Configuration注解使用
属性
- basepackages或者value:用于指定要扫描的包
示例:
@Configuration
@ComponentScan("com.kkb.spring.mvc.controller")
public class SpringConfiguration {
//初始化化时调用此无参构造函数
public void SpringConfiguration() {
//TODO
}
}
另一处:
package com.kkb.spring.mvc.controller;
@Controller
public class DemoController {
@PropertySource(用于加载properties配置文件)
介绍
- 对应XML文件相当于context:property-placeholder标签
- 使用时编写在类上面,作用是加载properties配置文件
- 获取参数时可以使用注解@Value。
属性
- value:用于指定properties文件路径,如果是类路径下,需要写上classpath
示例:
@Configuration
@PropertySource("classpath:sql.properties")
public class MySqlConfig{
@Value("${sql.driver}")
private String driver;
@Value("${sql.url}")
private String url;
@Value("{sql.username}")
private String uname;
@Value("{sql.password}")
private String passwd;
@Bean(name = "dateSource")
public DataSource createDataSource() {
try {
ComboPooledDataSource ds = new ComboPooledDataSource();
ds.setDriverClass(driver);
ds.setJdbcUrl(url);
ds.setUser(uname);
ds.setPassword(passwd);
return ds;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@import(用于组合多个配置类)
介绍
- 相当于spring配置文件中的标签
- 用于组合多个配置类,在引入其他配置时,可以不用再写@Configuration注解,写上也没问题。
属性
- value:用来指定其他配置类的字节码文件
示例:
@Configuration
@ComponentScan("com.kkb.spring.mvc.controller")
@Import({MySqlConfig.class})
public class SpringConfiguration {
//初始化化时调用此无参构造函数
public void SpringConfiguration() {
//TODO
}
}
创建纯注解方式上下文容器
java应用(AnnotationConfigApplicationContext)
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class);
UserService service = context.getBean(UserService.class);
service.saveUser();
这部分代码和上文的5个注解如果能够理解,就差不多初步掌握了纯注解方式和了解了一些SpringBoot的运行模式。
对于这段的的解析为,将原本从XML里加载内容改为从类中加载内容。结合之前XML的方式进行对照理解
Web应用(AnnotationConfigWebApplicationContext)
(这一块课上没讲,但是讲义里有,所以也做个笔记)
<web-app>
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.
support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.kkb.spring.test.SpringConfiguration</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>
Spring基于AspectJ的AOP使用方式
AOP介绍
概念
- 在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程。
- 作用:在不修改目标类代码的前题下,可以通过AOP技术去增强目标类的功能。通过【预编译方式】和【运行期动态代理】实现程序功能的统一维护的一种技术。
- AOP是一种编程范式,属于软工范畴,指导开发者如何组织程序结构。
- AOP最早由AOP联盟的组织提出的,制定了一套规范。Spring将AOP思想引入框架中,需要遵守AOP联盟制定的规范。
- AOP是OOP的延续,补充了OOP对于重复事物管理的不足,成互补关系,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。
-利用AOP可以对业务代码中【业务逻辑】和【系统逻辑】进行隔离,从而使得【业务逻辑】和系统逻辑之间的耦合度降低,提高程序的可重用性,提高开发效率。
AOP的原理及优势
原理简述
AOP采取横向抽取机制,补充了传统纵向继承体系(OOP)无法解决的重复性代码优化(性能监视、事务管理、安全检查、缓存),将业务逻辑和系统处理的代码(关闭连接、事务管理、操作日志记录)解耦。
纵向继承体系
横向抽取机制:
优势
重复性代码被抽取出来后,维护更加方便(相比较纵向继承,虽然也可以将重复性代码取出,但是需要修改类使得进行调用或者继承)
相关术语
Joinpoint(连接点)
所谓连接点是指那些被拦截到的点。在Spring中,这些点指的是方法,因为Spring只支持方法类型的连接点.
Pointcut(切入点)
所谓切入点是指对哪些Joinpoint进行拦截的定义。
Advice(通知/增强)
所谓通知是指拦截到Joinpoint后需要做的事情。通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能)
Introduction(引介)
所谓引介是指一种特殊的通知,在不修改类代码前提下Introduction可以在运行期为类动态地添加一些方法或Field(属性,成员变量的属性)
Target(目标对象)
代理的目标对象
Weaving(织入)
所谓织入是指把增强(Advice)应用到目标对象来创建新的代理对象的过程。
Proxy(代理)
一个类被AOP织入增强后,就产生一个结果代理类
Aspect(切面)
是切入点和通知的结合,后续用于编写和配置相关信息。
Advisor(通知器、顾问)
近似Aspect
图示
(手动绘制)
AOP实现原理(仅了解)
两种方式AspectJ(静态)、Spring AOP(动态)
在真正使用过程中,也都不是采用这两种代理方式,而是更胜一筹的整合AspectJ的Spring AOP,因此以下仅做了解,后续高级课程里讲到设计模式时会讲。
AspectJ
- AOP实现之AspectJ是一个Java实现的框架(和Spring无关是一个独立的实现方案),它能够对java代码进行AOP编译(一般在编译期进行),让Java代码具有AspectJ的AOP功能(需要特殊的编译器)
- AspectJ是目前实现AOP框架中,最成熟,功能最丰富的语言,并且和Java完全兼容。因此对于Java工程师而言,上手会很容易。
- AspectJ应用到Java代码的过程被称为织入,对于这个概念可以简单理解为:Aspect(切面)应用到目标函数(类)的过程。
- 对于织入这个过程,一般分为动态织入和静态织入,动态织入的方式是在运行时动态将要增强的代码织入到目标类中,这样往往是通过动态代理技术完成的,入Java JDK的动态代理(Proxy,底层通过反射实现)或者是CGLIB的动态代理(底层通过继承实现),Spring AOP采用的就是基于运行时增强的代理技术。
- AspectJ采用的就是静态织入的方式。主要采用编译期织入,在这个期间使用AspectJ的acj编译器(类似javac)把aspect类编译成class字节码后,在java目标类编译时织入,即先编译aspect类再编译目标类。
Spring AOP
原理
- Spring AOP是用动态代理技术实现的
- 动态代理基于反射实现(反射机制是指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制)。
- 对于动态代理有两种方法:基于接口的JDK动态代理(应用场景为针对有接口类的情况)和基于继承的CGLib动态代理(不论是否有接口都支持)
基于接口的JDK动态代理方案
目标对象必须实现接口
/**
* 使用JDK的方式生成代理对象
*/
public class MyProxyUtils{
public static UserService getProxy(final UserService service){
//使用Proxy类生成代理对象
UserService proxy = (UserService)Proxy.newProxyInstance(
service.getClass.getClassLoader(), //参数1,类加载器,作用为加载目标类
service.getClass.getInterfaces(), //参数2,实现接口,为目标类生成源代码
new InvocationHandler(){ //参数3,具体功能实现的接口
//代理对象方法一执行,invoke方法就会执行一次
public Object invoke(Object proxy, Method method, object[] args)
throws Throwable{
if("save".equals(method.getName()){
//开启事物
}
//提交事务
//让service类的save或者update方法正常的执行下去
return method.invoke(service,args);
}
}
);
return proxy;
}
}
基于继承的CGLib动态代理方案
- 目标对象不需要实现接口
- 底层是通过继承目标对象产生代理子对象(代理子对象中继承了目标对象的方法,并可以)
- 需要注意这种方法返回的类型是MethodProxy.invokeSuper(obj,args);
public static UserService getProxy(){
//创建CGLIB核心的类
Enhancer enhancer = new Enhancer();
//设置父类
enhancer.setSupuerClass(UserServiceImpl.class);
//设置回调函数
enhancer.setCallback(new MethodInterceptor)(){
@Override
public Object intercept(Object object,Method method ,Object [] args,
MethodProxy methodProxy) throws Throwable{
if("save".equals(method.getName())){
//记录日志
//。。。
}
return methodProxy.invokeSuper(object,args);
}
});
//生成代理对象
UserService proxy = (UserService) enhancer.create();
return proxy;
}
使用
- 使用ProxyFactoryBean创建:
使用<aop:advisor>
定义通知器的方式实现AOP则需要通知类实现Advice接口 - 增强(通知)的类型有:
- 前置通知:org.spirngframework.aop.MethodBeforeAdvice
在目标方法执行后实施增强 - 后置通知:org.springframework.aop.AfterReturningAdvice
在目标方法执行前实施增强 - 环绕通知:org.aopalliance.intercept.MethodInterceptor
在目标方法执行前后实施增强 - 异常抛出通知:org.springframework.aop.ThrowsAdvice
在方法抛出异常后实施增强 - 引介通知:org.springframework.aop.IntroductionInterceptor
在目标类中添加一些新的方法和属性。
- 前置通知:org.spirngframework.aop.MethodBeforeAdvice
Spring基于AspectJ的AOP使用
Spring已经将AspectJ收录到自身的框架中,并且底层采用的是动态织入方式
添加依赖
<!--基于AspectJ的AOP依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
编写目标类和目标方法
- 编写接口和实现类
UserService 接口
UserServiceImpl 实现类 - 配置目标类,将目标交给spring IoC容器管理
<context:component-scan base-package = "sourcecode.ioc" />
使用XML实现
实现步骤
编写通知(增强类,一个普通的类)
public class MyAdvice{
public void log(){
//记录日志
}
}
配置通知将通知交给spring IoC容器管理
<bean name = "myAdvice" class = "cn.spring.advice.MyAdvice"/>
配置AOP切面
<aop:config>
<aop:aspect ref = "myAdvice">
<!-- method : 指定要增强的方法,也就是指定通知类中的增强功能方法 -->
<!-- pointcut : 指定切入点,需要通过表达式来指定 -->
<aop:before method = "log"
pointcut = "execution(void cn.spring.dao.UserDaoImpl.insert())" />
</aop:aspect>
</aop:config>
切入点表达式
切入点表达式的格式
execution([ 修饰符] 返回值类型 包名.类名.方法名(参数))
eg:"execution(void cn.spring.dao.UserDaoImpl.insert())"
表达式格式说明
- execution:必要
- 修饰符:可省略
- 返回值类型:必要,但是可以使用
*
通配符 - 包名:{
- 多级包之间用.分割
- 包名可以用
*
代替,多级包名可以用多个*
代替 - 如果想省略中间包名可以使用…
}
- 类名:{
- 可以使用
*
代替 - 比如可以写成
*DaoImpl
(匹配路下所有以DaoImpl结尾的类,这种替代方式方法名也适用)
}
- 可以使用
- 方法名:{
- 可以用
*
代替 - 也可以写成
add*
(近似类名,匹配以add开头的所有方法,这种替代方式类名也适用)
}
- 可以用
- 参数:{
- 参数可以使用
*
代替 - 如果有多个参数可以使用
..
代替(这种方式是以两个点,注意是两个点)
}
- 参数可以使用
通知类型
有五种通知类型:(前置通知、后置通知、最终通知、环绕通知、异常抛出通知) 。
前置通知:
- 执行时机:目标对象方法之前执行通知
- 配置文件:
<aop:before method = "before" pointcut-ref ="myPoincut"/>
- 应用场景:方法开始时可以进行校验
后置通知:
- 执行时机:目标对象方法之后执行通知,有异常时无法执行完毕因此不执行
- 配置文件:
<aop:after-returning method = "afterReturning" pointcut-ref ="myPoincut"/>
- 应用场景:可以修改方法的返回值
最终通知:
- 执行时机:目标对象方法之后执行通知,相比起后置通知必然执行,不受异常影响
- 配置文件:
<aop:after method = "after" pointcut-ref ="myPoincut"/>
- 应用场景:释放内存资源
环绕通知:
- 执行时机:目标对象方法之前、之后都会执行(如果发生异常方法后会执行吗,存疑)
- 配置文件:
<aop:around method = "around" pointcut-ref ="myPoincut"/>
- 应用场景:事务、统计代码执行时机
异常抛出通知:
- 执行时机:在抛出异常后通知
- 配置文件:
<aop:after-throwing method = "afterThrowing" pointcut-ref ="myPoincut"/>
- 应用场景:包装异常
使用注解实现
实现步骤
编写切面类(不是通知类)
是切面类而不是通知类,因为切面类可以指定切入点
下方的表达式表示匹配所有路径下所有后缀DaoImpl的类里的所有方法(规则看前文的XML里对表达式的讲解)
/**
* 切面类(通知+切入点)
*/
@Component("myAspect")
@Aspect
public class MyAspect {
//@Before : 标记该方法是一个前置通知
//Value:切入点表达式
@Before(Value = “execution(* *..*.*DaoImpl.*(..))”)
public void log{
//记录日志
}
}
配置切面类
<context:component-scan base-package = "com.kkb.spring" />
开启AOP自动代理
<aop:aspect-autoproxy/>
环绕通知注解配置
@Around
- 作用:把当前方法看成是环绕通知。
- 属性:value:用于指定切入点表达式,还可以指定切入点表达式的引用。
@Around(value = "execution(* *.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint){
// 定义返回值
Object rtValue = null;
try {
// 获取方法执行所需的参数
Objec[] args = joinPoint.getArgs();
//前置通知:开启事物beginTransaction();
//执行方法
rtValue = joinPoint.proceed(args);
//后置通知:提交事务commit();
} catch(Throwable e){
//异常通知:回滚事务rollback();e.printStackTrace();
} finally{
//最终通知:释放资源release();
}
return rtValue;
}
定义通用切入点(@Pointcut)
使用@Pointcut注解在切面类中定义一个通用的切入点,其他通知可以引用该切入点
/**
* 切面类(通知+切入点)
*/
@Component("myAspect")
// @Aspect:标记该类是一个切面类
@Aspect
public class MyAspect {
//@Before : 标记该方法是一个前置通知
//Value:切入点表达式
//@Before(Value = “execution(* *..*.*DaoImpl.*(..))”)
@Before(value = "MyAspect.fn()")
public void log{
//记录日志
}
@Before(value = "MyAspect.fn()")
public void validate{
//后台校验
}
//通过@Pointcut定义一个通用的切入点
//(Value = “execution(* *..*.*DaoImpl.*(..))”)
@Pointcut(Value = “execution(* *..*.*DaoImpl.*(..))”)
public void fn () {
}
}
纯注解方式(略)
(此处课上没详细讲,因此略过)
Spring整合JUnit
在单元测试的情况下,不需要运行整个Spring容器。
单元测试问题
在测试类中,每个测试方法都有以下两行代码:
ApplicationContext ctx = new ClassPathXmlApplicationContext("Spring.xml");
UserService service = context.getBean(UserService.class);
这两行代码是用于获取容器,以构建运行环境,但是测试的目的是测试业务问题,因此希望不用手动添加这类代码。
解决思路分析
针对这类问题,期望程序能自动创建spring容器,从而省略这部分代码。
从这个需求出发,发现junit本身不识别spring,从而也无法创建spring容器了。
但是深入探索junit后发现其拥有一个注解 (@RunWith),可以根据这个注解替换其运行器。
根据这个思路如果用这个注解使用spring框架的运行器,再根据spring框架读取配置文件的方式,去创建容器。
实现方案
第一步:添加依赖
添加spring-test包即可。
第二步:通过@RunWith注解指定运行器
Spring的运行器是SpringJUnit4ClassRunner
第三步:通过@ContextConfiguration注解,指定配置文件
指定Spring运行器需要的配置文件
第四步:通过@Autowired注解给测试类中的变量注入数据
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class TestSpringJunit{
@Autowired
private UserService service;
@Test
public void saveUser() {
service.saveUser();
}
}
事务支持
事务回顾
事务介绍
事务的概念
事务:指逻辑上的一组操作,指一整个完整的操作,任何一个环节的错误导致通路的故障都算事务的失败,整体完整执行完毕才算成功。
事务的特性(ACID):
原子性(Atomicity)
指操作要么全面成功,要么失败全部回滚
一致性(Consistency)
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说事务在执行前后都保持一致性。
隔离性(Isolation)
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事物,不被其他事务干扰,多个并发事务之间要隔离。
持久性(Durability)
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即使是在数据库系统中遇到故障的情况下也不会丢失提交事务的操作。
事务并发问题(隔离性导致)
在事务的并发操作中可能会出现一些问题:
- 脏读:一个事务读取到另一个事务未提交的数据。
- 不可重复读:一个事务因读取到另一个事务已经提交的数据。导致对同一条记录读取两次结果不一致。(update操作)
- 幻读:一个事务因读取到另一个事务已经提交的数据。导致对同一张表读取两次结果不一致。(insert操作、delete操作)
事务隔离级别
为了避免上述几种情况,在标准SQL规范中,定义了4个事务隔离级别,不同的隔离级别对事务的处理不同。
四种隔离级别
MySQL提供了4种隔离级别(以下由低到高排序):
- Read uncommitted(读未提交):最低级别,任何情况都无法保证。
- Read committed (读已提交):可避免脏读的发生。
- Repeatable read (可重复读):可避免脏读、不可重复读的发生。
- Serializable (串行化):可避免脏读、不可重复读、幻读的发生。
默认隔离级别
大多数数据库的默认隔离界别是Read committed (RC),比如Oracle、DB2等。
MySQL数据库的默认隔离级别是Repeatable Read(RR)。
注意事项(级别和性能负相关):
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。
参考大多数情况,可以考虑优先设置隔离级别为RC,能避免脏读,并且具有较好的并发性能。尽管对于剩下的不可避免,但是对于幻读和不可重复读可以用应用程序使用悲观锁或乐观锁来控制(简单描述就是总是假设最坏的情况或者是总是假设最好的情况,进行对应情况的处理)。
Spring框架事务管理相关接口
Spring并不直接管理事务,而是提供了事务管理接口是PlatformTransactionManager,通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。
1.TransactionDefinition接口
事务定义信息。(事务的隔离级别,传播行为,超时,只读)
2.PlatformTransactionManager接口
平台事务管理器。(真正管理事务的类)。该接口有具体的实现类,根据不同的持久层框架,需要选择不同的实现类!
3.TransactionStatus接口
事务的状态(是否新事务、是否已提交、是否有保存点,是否回滚)。
4.上述3点的总结
上述对象之间的关系:
平台事务管理器PlatformTransactionManager真正管理事务对象,根据事务定义的信息TransactionDefinition进行事务管理,在管理事务中产生的一些状态会记录到TransactionStatus。
5. PlatformTransactionManager接口中实现类和常用的方法
接口实现类:
- 如果是Spring的JDBC模板或者MyBatis框架,需要选择DataSourceTransactionManager实现类。
- 如果是Hibernate的框架,需要选择HibernateTransactionManager实现类
接口常用方法:
- void commit(TransactionStatus status)
- TransactionStatus getTransaction(TransactionDefinition definition)
- void rollback(TransactionStatus status)
6.TransactionDefinition接口里的信息
事务隔离级别的常量
- static int ISOLATION_DEFAULT --采用数据库的默认隔离级别
- static int ISOLATION_READ_UNCOMMITTED --读未提交(基本没有隔离)
- static int ISOLATION_READ_COMMITTED --读已提交
- static int ISOLATION_REPEATABLE_READ --可重复读
- static int ISOLATION_SERIALIZABLE --串行化
事务的传播行为常量(不设置时,使用默认值)
事务的传播行为是指解决业务层之间的方法调用
- PROPAGATION_REQUIRED(默认值) --A中有事务,使用A的事务,如果没有,B就会开启一个事务,将A包含进来。从而保证A、B处理同一个事务。
- PROPAGATION_SUPPORTS --A中有事务,使用A的事务,如果没有,B也不会使用事务。
- PROPAGATION_MANDATORY --A中有事务,使用A的事务,如果没有,抛出异常 。
- PROPAGATION_REQUIRES_NEW --A中有事务,将A的事务挂起,B创建一个新的事务,使得A、B不处理同一个事务。
- PROPAGATION_NOT_SUPPORTED --A中有事务,将A的事务挂起。
- PROPAGATION_NEVER --A中有事务,抛出异常。
- PROPAGATION_NESTED –嵌套事务。当A执行之后就会在这个位置设置一个保存点,如果B没有问题就执行通过。如果B出现异常,运行客户根据需求回滚(回滚回到保存点或者是初始状态)。
Spring框架事务管理的分类
-
Spring的编程式事务管理(不推荐使用)
通过手动编写代码的方式完成事务的管理。 -
Spring的声明式事务管理(底层采用AOP的技术)
通过一段配置方式完成事务的管理
编程式事务管理(简要了解)
说明:Spring为了简化事务管理的代码:提供了模板类TransactionTemplate,因此手动编程来管理事务只需要使用该模板类
步骤如下:
步骤一:配置一个事务管理器,Spring使用PlatformTransactionManager接口来管理事务
因此使用其实现类
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
步骤二:配置事务管理的模板
<!-- 配置事务管理的模板 -->
<bean id="transactionTemplate"
class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager">
</property>
</bean>
步骤三:在需要进行事务管理的类中,注入事务管理的模板
<!-- 为需要管理的类,注入事务管理的模板 -->
<bean id="accountService"
class="com.bao.spring_test.service.accountServiceImpl">
<property name="accountDAO" ref="accountDAO" />
<property name="transactionTemplate" ref="transactionTemplate" />
</bean>
步骤四:在业务层使用模板管理事务
//注入事务模板对象
private TransactionTemplate transactionTemplate;
public void setTransactionTemplate( TransactionTemplate transactionTemplate) {
this.transactionTemplate =transactionTemplate;
}
//用模板管理事务
public void pay(final String out,final String in,final double money) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus arg0) {
// TODO 自动生成的方法存根
//扣钱
accountDao.outMoney(out,money);
//加钱
accountDao.outMoney(in,money);
}
});
}
声明式事务管理(重点)
相比起编程式,声明式方案则近乎可以一劳永逸的管理事务,一劳则是是进行相关信息的配置。
声明式管理也有3种方案,XML、XML和注解混用、纯注解
(下方根据课件来看有些笼统,如果我根据视频有所收获我会自行补充一些信息)
以转账这个业务模型举例:
XML方案
业务层
两个类
- AccountService
- AccountServiceImpl
//业务函数
public void transfer(String in,String out ,double money) {
dao.outMoney(out,money);
//假定会出bug的代码
System.out.println(1/0);
dao.inMoney(in,money);
}
持久层
- AccountDao
- AccountDaoImpl
(普通的CRUD类,略)
spring配置
<bean id = "dataSource"
class = "com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method = "close">
<!--配置连接数据库的4个基本信息-->
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<bean class = "cn.spring.dao.AccountDaoImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>
<context:component-scan base-package="cn.spring.service"></context:component-scan>
单元测试代码
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext-tx.xml"})
public class TransactionTest {
@Autowired
private AccountService service;
@Test
public void test01() {
service.transfer("李聪", "陈阳", 100);
}
}
配置事务管理的AOP
平台事务管理器:DataSourceTransactionManager
这部分和程序配置方案一样,在配置完数据源后需要配置一个平台事务管理器,这平台事务管理器是为了管理事务通知功能:
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
事务通知
使用标签:<tx:advice id = "" transaction-manager = "" />
代码举例:
<tx:advice id = "txAdvice" transaction-manager = "transactionManager" >
<!-- 配置事务相关属性 -->
<tx:attributes>
<!-- 对方法级别设置 事务的隔离级别、事务的传播行为 -->
<!-- 设置了默认的隔离级别和传播行为 -->
<tx:method name = "transfer*" />
<!-- 添加 -->
<tx:method name = "insert*" />
<!-- 删除 -->
<tx:method name = "delete*" />
<!-- 修改 -->
<tx:method name = "update*" />
<!-- 查询 -->
<tx:method name = "select*" />
</tx:attributes>
</tx:advice>
- 这些被
tx:method
定义的方法是那些切入点里需要被添加事务管理属性的方法。 - 采用这种方式管理事务也要求了编写代码时起名规范。
AOP配置
使用配置模型,这种配置模型适用与Spring和AOP和AspectJ整合的方式:
<aop:config>
<aop:advisor advice-ref="" pointcut=""/>
</aop:config>
代码举例:
<!-- 配置AOP -->
<aop:config>
<!-- 切入点:所有业务层实现类中的方法(通过*匹配) -->
<aop:advisor advice-ref="txAdvice" pointcut="execution(* *..*.*Service.*.*(..))"/>
</aop:config>
XML和注解混合使用
这种方式写起来更为简单,但是代码量会变大。每一个管理的类都要去去写@Transaction
service类上或者方法上加上注解@Transaction:
- 在类前加上注解@Transaction:表示该类中所有的方法都被事务管理
- 方法上注解@Transaction:表示只有该方法被事务管理
开启事务注解:
这部分和程序配置方案一样:
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
这部分是关键,注意是"transactionManager"
<!-- 配置事务注解驱动 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
基于AspectJ的纯注解方式
课上的意思是这个方法不好用,略过不讲,最方便的方法是XML
相比起混用方式仅仅需要用注解:
@EnableTransactionManagement
去替换
<!-- 配置事务注解驱动 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
结语
- 2021.1.4 学习结束,我的学习速度有些过于缓慢了,为了稍微增加进度,后半段许多的代码原文我都摘抄自课件的学习资料。
- 整体学习完毕后,我发现我真的是缺少相关技术知识,但是经过学习,我相信我能够在面对本篇中所描述的所有问题有解决方案。
- 关于那些摘抄自课件的实例代码,后续当我自行搭建架构的时候我会适当的来替换一部分从而加深自身的理解。
- 在学习的后半段,我的计划改为了先抄写课件,再看视频印证理解,添加笔记,而之前的方案是只看视频,做对应的笔记,但是很多知识点要反复回放不太方便。对此我认为我的学习策略进步了。
- 这种学习方式一个知识点至少学5遍,分别是,看视频,看讲义,看讲义做笔记,看视频补充笔记,发布前最后整理
- 直播课同样有大纲的课件,方案同理。