Spring 概述
我们学习 Spring 框架的最终目的是用它整合 Struts2、Hibernate 框架(SSH)。
Spring 框架的作用
Spring 框架主要负责技术整合,该框架提供 IOC 和 AOP 机制,可以降低系统组件之间的耦合度,便于系统组件的维护、扩展和替换。
Spring 框架的优点
其实与 Spring 框架的作用相同:
在 SSH 中,主要是利用 Spring 容器管理我们程序中的 Action、DAO 等 Bean 组件,通过容器的 IOC 机制,可以降低 Action、DAO 之间调用的耦合度(关联度),利用 AOP可以降低共通组件和一批目标组件的耦合度,进行事务管理等共通部分的处理。
在 SSH 中,Struts2 主要是利用它的控制器,而不是标签、表达式;Hibernate 主要利用 它的数据库访问;Spring 主要是利用它的整合。
Spring 框架的容器
Spring 框架的核心是提供了一个容器,该容器类 型是ApplicationContext(它是 BeanFactory 的子类,建议用这个类型因为功能更多)。
该容器具有以下功能:
1)容器可以创建和销毁 Bean 组件对象,等价于原来“工厂”类的作用。
2)容器可以采用不同的模式创建对象,如单例模式创建对象。
3)容器具有 IOC 机制实现。
4)容器具有AOP 机制实现。
Spring 容器的基本应用
如何将一个 Bean 组件交给 Spring 容器
1)Bean 组件其实就是个普通的Java 类!
2)方法:在applicationContext.xml 中添加以下组件定义,见 2.6 案例中 step4。
如何获取 Spring 容器对象和 Bean 对象
1)实例化容器:
ApplicationContext ac=new ClassPathXmlApplicationContext("/applicationContext.xml");
//FileSystemXmlApplicationContext("");//去指定的磁盘目录找,上面的为去 Class 路径找
2)利用 getBean("标识符")方法获取容器中的 Bean 对象。见 2.6 案例中 step5。
如何控制对象创建的模式
Spring 支持 singleton(单例)和 prototype(原型,非单例)两种模式。
默认是 singleton 模式,可以通过
以后在 Web程序中,通过扩展可以使用 request、session 等值。见 2.6 案例中 step4、step7。
例如:
注意事项:对于 NetCTOSS 项目,一个请求创建一个 Action,所以用 Spring 时必须 指明 prototype,否则默认使用 singleton 会出问题。而 DAO 则可用 singleton 模式。
Bean 对象创建的时机
1)singleton 模式的 Bean 组件是在容器实例化时创建。
2) 模式是在调用 getBean()方法时创建。
3)singleton 模式可以使用
Spring 框架 IoC 特性
IoC 概念
1)Inverse of Controller 被称为控制反转,其实真正体现的是“控制转移”。
2)所谓的控制指的是负责对象关系的指定、对象创建、初始化和销毁等逻辑。
3)IoC 指的是将控制逻辑交给第三方框架或容器负责(即把 Action 中的控制逻辑提出来, 交给第三方责),当两个组件关系发生改变时,只需要修改框架或容器的配置即可。
4)IoC 主要解决的是两个组件对象调用问题,可以以低耦合方式建立使用关系。
DI 概念
1)Dependency Injection 依赖注入:在容器实例化的时候依赖对象将依赖关系主动注入到对象中.
2)Spring 框架采用 DI 技术实现了 IoC 控制思想。
3)Spring 提供了两种形式的注入方法:
- setter 方式注入(常用):依靠 set 方法,将组件对象传入(可注入多个对象)。 A.首先添加属性变量和 set 方法。
B.在该组件的定义中采用下面的描述方式:
注意事项:例如 CostAction 中有 costDAO 属性,而它的标准 set 方法名为 setCostDAO,那么配置文件中的 name 就应该写 costDAO(去掉 set,首字 母小写)。如果 set 方法名为 setCost,那么 name 就应该写 cost(去掉 set, 首字母小写)!确切的说,name 不是看定义的属性名,而是 set 方法名。 - 构造方式注入(用的少):依靠构造方法,将组件对象传入。
A.在需要注入的组件中,添加带参数的构造方法。 B.在该组件的定义中,使用下面格式描述:
AOP 概念
什么是 AOP
Aspect Oriented Programming,被称为面向切面编程。对单个对象(一对一)的解耦用 IOC, 而当有个共通组件,它对应多个其他组件(一对多),则解耦用 AOP。AOP是对OOP的一种补充,而AOP是面向横向的编程。
AOP,可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。
如,拦截器。这也是为何在程序中大量的用 IoC,而AOP却用的很少,因为程序中不可能有很多的共通部分。
AOP 和 OOP 的区别
OOP 是面向对象编程,AOP 是以 OOP 为基础的。
OOP 主要关注的是对象,如何抽象和封装对象。
AOP 主要关注的是方面,方面组件可以以低耦合的方式切入到(作用到)其他某一批目 标对象方法中(类似于 Struts2 中的拦截器)。
AOP 主要解决共通处理和目标组件之间解耦。
AOP 相关术语
1)方面(Aspect):指的是封装了共通处理的功能组件。该组件可以作用到某一批目标 组件的方法上。
2)连接点(JoinPoint):指的是方面组件和具体的哪一个目标组件的方法有关系。
3)切入点(Pointcut):用于指定目标组件的表达式。指的是方面组件和哪一批目标组件 方法有关系。多个连接点组成的集合就是切入点。如:a、b 为切入点,1、2 为连接点。
a方面组件
b
1HibernateCostDAO
2
1JdbcCostDAO
2save() delete()
save()
delete()
4)通知(Advice):用于指定方面组件和目标组件方法之间的作用时机。例如:先执行 方面组件再执行目标方法;或先执行目标方法再执行方面组件。
5)目标(Target):利用切入点指定的组件和方法。
6)动态代理(AutoProxy):Spring 同样采用了动态代理技术实现了 AOP 机制。当使用 AOP 之后,从容器 getBean()获取的目标组件,返回的是一个动态生成的代理类。然后通过代 理类执行业务方法,代理类负责调用方面组件功能和原目标组件功能。
Spring 提供了下面两种动态代理技术实现:
1)采用 CGLIB 技术实现(目标组件没有接口采用此方法)
例如:public class 代理类 extends 原目标类型 { }
CostAction action=new 代理类();//代理类中有原来类的方法
2)采用 JDK Proxy API 实现(目标组件有接口采用此方法,即实现了某个接口)
例如:Public class 代理类 implements 原目标接口 { }
CostDAO costDAO=new 代理类();//代理类去实现了原目标接口,所以没
有原来类的方法
案例:AOP 的使用,模拟某些组件需要记录日志的功能
接 3.3、3.4 案例,想让所有的操作进行日志记录,那么按以前的方式就需要给所有 Action 或 DAO 中添加记录日志的代码,如果 Action 或 DAO 很多,那么不便于维护。而使用 AOP 机制,则可以很方便的实现上述功能:
step1:导入 AOP 需要的包:aopalliance.jar、aspectjrt.jar、aspectjweaver.jar、cglib-nodep-2.1_3.jar
step2:在 org.tarena.aop 包下新建 LoggerBean 类,并添加 logger 方法用于模拟记录日志功能
public void logger(){
System.out.println("记录了用户的操作日志"); }
step3:在 applicationContext.xml 配置文件中,添加 AOP 机制
step4:执行 3.3 案例 step3,则发现添加操作已有了记录日志功能
创建 CostDAO 对象 初始化 CostDAO 对象 记录了用户的操作日志 处理资费添加操作 利用 JDBC 技术实现保存资费记录
step5:执行 3.4 案例 step3,则发现删除操作已有了记录日志功能,记得加无参构造方法!
记录了用户的操作日志 处理资费删除操作 利用 Hibernate 技术实现删除资费记录
注意事项:DeleteAction 用的是构造注入,所以此处要把无参构造器再加上!因为 AOP底层调用了 DeleteAction的无参构造方法。不加则报错:Superclass has no null constructors but no arguments were given
通知类型 通知决定方面组件和目标组件作用的关系。
主要有以下几种类型通知:
1)前置通知:方面组件在目标方法之前执行。
2)后置通知:方面组件在目标方法之后执行,目标方法没有抛出异常才执行方面组件。
3)最终通知:方面组件在目标方法之后执行,目标方法有没有异常都会执行方面组件。
4)异常通知:方面组件在目标方法抛出异常后才执行。
5)环绕通知:方面组件在目标方法之前和之后执行。
try{ //前置通知执行时机
//执行目标方法
//后置通知执行时机
}catch(Exception e){//异常通知执行时机
}finally{ //最终通知执行时机
}//环绕通知等价于前置+后置
切入点
切入点用于指定目标组件和方法,Spring 提供了多种表达式写法:
1)方法限定表达式:指定哪些方法启用方面组件。
①形式:execution(修饰符? 返回类型 方法名(参数列表) throws 异常?)
②示例:
execution(public * * (..)),匹配容器中,所有修饰符是 public(不写则是无要求 的),返回类型、方法名都没要求,参数列表也不要求的方法。
execution(* set(..)),匹配容器中,方法以 set 开头的所有方法。 execution( org.tarena.CostDAO.(..)),匹配 CostDAO 类中的所有方法。 execution( org.tarena.dao..(..)),匹配 dao 包下所有类所有方法。
execution(* org.tarena.dao...(..)),匹配 dao 包及其子包中所有类所有方法。
2)类型限定表达式:指定哪些类型的组件的所有方法启用方面组件(默认就是所有方法 都启用,且知道类型,不到方法)。
①形式:within(类型) ②示例:
within(com.xyz.service.),匹配 service 包下的所有类所有方法 within(com.xyz.service..),匹配 service 包及其子包中的所有类所有方法 within(org.tarena.dao.CostDAO),匹配 CostDAO 所有方法
注意事项:within(com.xyz.service...),为错误的,就到方法名!
3)Bean 名称限定:按
①形式:Bean(id 值) ②示例:
bean(costDAO),匹配 id=costDAO 的 bean 对象。
bean(*DAO),匹配所有 id 值以 DAO 结尾的 bean 对象。
4)args 参数限定表达式:按方法参数类型限定匹配。
①形式:args(类型) ②示例:
args(java.io.Serializable),匹配方法只有一个参数,并且类型符合 Serializable 的 方法,public void f1(String s)、public void f2(int i)都能匹配。
注意事项:上述表达式可以使用&&、| | 运算符连接使用。
案例:环绕通知,修改 5.4 案例使之动态显示所执行的操作
step1:新建 opt.properties 文件,自定义格式:包名.类名.方法名=操作名。在高版本 MyEclipse 中,切换到 Properties 界面,点击 Add 直接输入键和值,则中文会自动转为 ASCII 码。低版 本的则需要使用 JDK 自带的转换工具:native2ascii.exe
第一个为资费添加,第二个为资费删除 org.tarena.action.CostAction.execute=\u8D44\u8D39\u6DFB\u52A0
org.tarena.action.DeleteAction=\u8D44\u8D39\u5220\u9664 step2:新建 PropertiesUtil 工具类,用于解析.properties 文件
private static Properties props = new Properties();
static{ try{ props.load(PropertiesUtil.class.getClassLoader()
.getResourceAsStream("opt.properties"));
}catch(Exception ex){ ex.printStackTrace(); } } public static String getValue(String key){
String value = props.getProperty(key);
if(value == null){ return ""; }else{ return value; } } step3:使用环绕通知,将 5.4 案例 step3 中的
public Object logger(ProceedingJoinPoint pjp) throws Throwable{//采用环绕通知,加参数
//前置逻辑
String className=pjp.getTarget().getClass().getName();//获取要执行的目标组件类名 String methodName=pjp.getSignature().getName();//获取要执行的方法名
//根据类名和方法名,给用户提示具体操作信息
String key=className+"."+methodName; System.out.println(key);
//解析 opt.properties,根据 key 获取 value String value=PropertiesUtil.getValue(key);
//XXX 用户名可以通过 ActionContext.getSession 获取
System.out.println("XXX 执行了"+value+"操作!操作时间:"+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(
new Date(System.currentTimeMillis()))); Object obj=pjp.proceed();//执行目标方法
//后置逻辑
return obj; }
step5:分别执行 3.3 案例 step3 和 3.4 案例 step3,执行结果动态显示所执行的操作及时间 XXX 执行了资费添加操作!操作时间:2013-08-19 20:14:47
XXX 执行了资费删除操作!操作时间:2013-08-19 20:15:45
案例:利用 AOP 实现异常处理,将异常信息写入文件
1)分析:方面:将异常写入文件。切入点:作用到所有 Action 业务方法上
within(org.tarena.action..)。通知:异常通知
public class ExceptionBean {//模拟,将异常信息写入文件
public void exec(Exception ex){//ex 代表目标方法抛出的异常
System.out.println("将异常记录文件"+ex); //记录异常信息 } } step2:在 applicationContext.xml 配置文件中进行配置
step3:在 DeleteAction 的 execute 方法中添加异常
String str=null; str.length();
step4:执行 3.3 案例 step3 则添加操作执行正常;执行 3.4 案例 step3 则删除操作报 空指针异常!显示结果:将异常记录文件 java.lang.NullPointerException