Spring框架
第三方编写的一套能够实现Ioc(DI)和Aop功能的框架
Spring被称之为java的"救世主"
Spring的出现改变了java既有的编程方式
Spring的主要功能
Spring主要功能有两个
分别是
1.Ioc(DI) ---- 控制反转(依赖注入)
控制反转
控制反转(Ioc:Inversion of Control)
要想了解控制反转,就要知道我们之前编写的程序都是"主动控制"
主动控制:是由程序员来主动管理和控制程序中需要使用的对象
控制反转:是将控制和管理程序中所使用到对象的权利交给外部容器
什么是DI
DI中文翻译就是依赖注入(dependency injection)
是Spring框架中实现Ioc的一种操作
什么是依赖
所谓依赖,就是一个对象在运行某个业务或方法时需要的原材料
我们称这个对象依赖这个原材料来实现这个业务
人依赖笔完成写字方法
关羽依赖青龙偃月刀完成战斗方法
什么是注入
将一个外部的对象,保存到指定对象的内部
比如将User对象注入到Spring容器中
通过上面对依赖和注入的分别解释,
依赖注入可以总结为:
使用注入的方式,将一个对象依赖的原材料直接保存到这个对象的属性中
编写低耦合的依赖注入
什么是高耦合(紧耦合)
所谓的高耦合,就是依赖对象和被依赖的对象的关系是紧密的,不可替换的
关羽类中声明依赖青龙偃月刀类型,就是依赖对象和被依赖对象关系是紧密的
因为关羽除了青龙偃月刀不能使用其他任何兵器完成战斗业务
我们编写程序,高耦合被认为是不好的,因为它不能应对程序的变化,不方便扩展和维护
我们要对现有的代码进行"解耦"
所谓解耦就是将现在高耦合的依赖状态变化为低耦合(松耦合)的
做法非常简单,就是提取当前被依赖类的抽象类或者接口
让依赖本类的类中声明为这个抽象类或接口即可
2.AOP ---- 面向切面编程
AOP:面向切面的程序设计
Aspect-oriented programming
也译作:剖面导向程序设计
是一种程序设计思想,
旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。通过在现有代码基础上增加额外的通知(Advice)机制,能够对被声明为“切点(Pointcut)”的代码块进行统一管理与扩展。
AOP术语
切点(point cut):方法与方法之间的调用会产生切点,我们通过使用AOP拦截方法的调用并对调用进行更改或扩展
通知(Advice):设置具体的针对切点方法的运行时机
前置通知(before advice):被调用方法运行之前要运行
后置通知(after advice):被调用方法运行之后要运行
环绕通知(around advice):被调用方法运行之前和之后都运行
事后返回通知(afterReturning advice):被调用方法返回值之后运行
事后返回通知比较少见
异常通知(afterThrowing advice):被调用方法发生异常时运行
目标对象(target):调用方法的对象
织入(weaving):将编写的扩展方法镶嵌到指定的切点的操作
切面(aspect):是一个可以定义切点,编写各类通知和织入操作的内容
通知的分类
实际上AOP机制提供了如下的通知方式
@Before 前置
@After 后置
@AfterReturning 返回后
@Around 环绕
@AfterThrowing 异常时
在进行通知增强方法运行时
我们可以在运行的方法的参数中添加JoinPoint类型的参数
一旦添加这个类型的参数
可以通过这个参数获得切入点的目标方法一些信息
例如:目标对象,目标方法参数返回值等
下面我们演示一下各种通知的使用和JoinPoint的使用
@Aspect//表示这个类是一个设置切面的类
@Component
@Slf4j
public class DemoAspect {
//如果想对某些方法(或某个方法)设置通知,扩展功能的话
//需要先声明这个方法的切入点
//使用@Pointcut声明切入点
//这个切入点指定的是TestController的test方法
@Pointcut("execution(public * cn.tedu.straw.gateway." +
"controller.TestController.test(..))")
//这个方法不需要任何代码
//这个方法的意义仅在于方法名会成为上面声明切入点的id
public void pointCut() {
}
//声明一个前置通知
//@Before("pointCut()")含义是下面的方法会在
// pointCut()指定的切入点的方法运行之前运行
@Before("pointCut()")
public void before(JoinPoint joinPoint){
//获得目标方法的信息
Signature method= joinPoint.getSignature();
log.info("在{}方法之前运行",method);
}
@After("pointCut()")
public void after(JoinPoint joinPoint){
Signature method= joinPoint.getSignature();
log.info("在{}方法之后运行",method);
}
//环绕通知的JoinPoint类型比较特殊,它是ProceedingJoinPoint
//是JoinPoint的子接口,相较于JoinPoint有更多的方法用于支持环绕通知
@Around("pointCut()")
public void around(ProceedingJoinPoint joinPoint)
throws Throwable {
log.info("环绕通知的前置运行...");
//ProceedingJoinPoint接口的特有方法,放行运行目标方法
joinPoint.proceed();//运行目标方法
log.info("环绕通知的后置运行...");
}
//异常通知
@AfterThrowing("pointCut()")
public void afterThrowing(JoinPoint joinPoint){
Signature method= joinPoint.getSignature();
log.info("在{}方法发生异常时运行",method);
}
//返回后通知
@AfterReturning("pointCut()")
public void afterReturning(JoinPoint joinPoint){
Signature method= joinPoint.getSignature();
log.info("在{}方法返回值之后运行",method);
}
}
切入点声明的格式
上面章节中我们使用@PointCut声明切入点
@Pointcut("execution(public * cn.tedu.straw.gateway." +
"controller.TestController.test(..))")
上面的代码指定了切入点的方法
但实际上我们还可是使用更多变化来表示更丰富的含义
声明切入点的语法格式如下
execution(
modifier-pattern?
ret-type-pattern
declaring-type-pattern?
name-pattern(param-pattern)
throws-pattern?)
modifier-pattern:匹配修饰符(可省略)
ret-type-pattern:匹配返回值(必须写)
declaring-type-pattern:类路径\全类名(可省略)
name-pattern:匹配方法名(必须写)
param-pattern:匹配参数列表(必须写)
throws-pattern:匹配声明的异常(可省略)
下面根据几个例子来理解
execution(* *(..)) 表示匹配所有方法
execution(public * com.test.TestController.*(..))
TestController类中的public修饰的所有方法
execution(* com.test..*.*(..))
com.test包下的所有类中的所有方法
AOP实际应用
我们现在要利用aop的环绕通知增强来检测faq模块中业务逻辑层所有方法每个方法的耗时
在faq模块
faq中创建一个aspect包,包中新建一个专门的类来编写检测性能的程序
代码如下
@Aspect
@Component
@Slf4j
public class PerformanceAspect {
//声明切入点
// 当前项目中业务逻辑层中所有公有方法的运行时间
@Pointcut("execution(public * cn.tedu.straw.faq." +
"service.*Service.*(..))")
public void serviceCut(){}
//测试性能的环绕增强
@Around("serviceCut()")
public Object perform(ProceedingJoinPoint joinPoint)
throws Throwable {
//记录纳秒时间
long t1=System.nanoTime();
Object obj=joinPoint.proceed();
long t2=System.nanoTime();
Signature method=joinPoint.getSignature();
log.info("方法用时:{}:{}",method,t2-t1);
return obj;
}
}
利用JUnit进行测试
什么是JUnit
JUnit就是java单元测试,能够针对java程序中的某段代码或指定的方法进行测试
为什么需要JUnit
如果没有JUnit,我们使用项目中创建一个类,这个类中编写main方法的方式来进行测试,这样的方式一个类只能有一个main方法
测试不方便,而且测试用的main方法的类和其它类都写在同一个区域中,造成类的数量比较多,不好管理
Junit可以在我们编写正常java类的区域之外,再创建专门的测试区域
而且一个类可以测试多个方法,更专业更简单
Spring组件扫描
我们现在学习了怎么在Config类中使用@Bean来注入对象到Spring容器
但是Spring提供了很多中注入方法方便我们在不同情况下使用注入功能
什么时候使用组件扫描
当一个类不需要进行任何属性或状态的设置,单纯的直接实例化就注入到Spring容器时,就不需要使用@Bean的注入方式了,推荐使用组件扫描
使用组件扫描注入对象
在要注入的类名上(前一行)添加如下注解
@Component 这个单词是组件的意思
一般情况下,组件扫描方式注入的对象的id就是类名
但是对类名可能有细微的修改
当类名首字母是大写,第二个字母是小写时,id为首字母小写的样式
例:DragonBlade -> dragonBlade
当类名前两个字母连续大写时,id就是类名不变
例:AOPTest -> AOPTest
自定义组件id
如果我们不想使用类名做Spring容器中的id
我们也可以自定义组件的id
需要注意,实例开发中,很少有需要自定义id的需求和必要,一般情况下使用类名即可!!!
Spring容器中对象的管理
Spring容器中对象的作用域
Spring容器中对象可能会有不同的作用域
在不同的程序需求下,设置为不同的管理模式
根据程序中对象的出现形式,我们可以将程序中的对象分为两种
1.程序中只出现唯一一个的:单例(singleton)
2.程序中出现很多的:原型(prototype)
Spring容器中注入的对象,在程序中运行也要分出这两种对象
实际开发中,大部分的对象都是单例的,提高的内存的利用率,所以Spring在默认情况下,也将注入到Spring容器的对象设置为单例
Spring容器中对象的创建和销毁
如果想研究Spring容器对对象实例化和销毁的时机
将User类中添加如下代码
//在对象被创建时运行的代码
@PostConstruct
public void open(){
System.out.println("User对象实例化完毕");
}
//在对象销毁时运行的代码
@PreDestroy
public void close(){
System.out.println("User对象即将被销毁");
}
再次使用单例测试
我们会发现单例的作用域下
在实例化acac对象即Spring容器时,User对象就已经实例化并注入到Spring容器中了
无论在测试代码中获得多少次User对象,都不会再次实例化
并且在acac对象销毁前,User对象也会自动销毁!
如果上述代码在原型的作用域下
我们会发现原型的作用域下
User对象不会随acac即Spring容器的实例化而实例化
每当测试代码中获得User对象时,都会实例化一个新的User对象反复获得时就会反复实例化
acac对象销毁时,原型状态下的User对象没有销毁
我们今后使用的比较多的是单例的
单例模式还有一个小缺点,需要额外的弥补一下
懒惰初始化
通过上面章节的测试,我们发现单例模式的对象会在Spring容器实例化的同时也实例化,这样情况下,就会造成Spring容器中单例对象较多时,实例化时间较长
如果很多单例的对象在程序实际运行中根本就用不到,这样这些实例化操作和内存就白白浪费了
这种情况下,我们就可以使用懒惰初始化
一个对象的注入一旦被标记为懒惰初始化,那么在acac实例化时,就不会同时实例化它了,而会在获得这个类型对象时,再实例化
@Autowired注解实现自动装配的过程
首先在Spring容器中寻找匹配类型的对象
如果没有,则注入失败
如果上面的寻找成功,并且只有唯一一个匹配类型对象,则注入成功
如果上面的寻找成功,并且匹配两个或以上的对象,那么就需要使用id的匹配,如果id匹配有对应的对象,则注入成功
上面3中的情况下,如果没有任何id匹配,则注入失败