一、Spring简介
Spring是一个轻量级Java开发框架,由Rod Johnson创建,目的是为了解决企业级应用开发的业务逻辑层和其他各层的耦合问题。它是一个JavaSE/JavaEE分层的full-stack(一站式)轻量级开源框架,为开发Java应用程序提供全面的基础架构支持。
Spring负责基础架构,因此Java开发者可以专注于应用程序的开发。
Spring的优点:
- 方便解耦,简化开发:Spring就是一个大工厂,可以将所有对象创建和依赖的关系维护,交给Spring管理。
- AOP编程的支持:Spring提供面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等功能。
- 声明式事务的支持:只需要通过配置就可以完成对事务的管理,而无需手动编程。
- 方便程序的测试:Spring对Junit4支持,可以通过注解方便的测试Spring程序。
- 方便集成各种优秀框架:Spring不排斥各种优秀的开源框架,其内部提供了对各种优秀框架的直接支持(如:Struts、Hibernate、MyBatis等)。
- 降低JavaEE API的使用难度:Spring对JavaEE开发中非常难用的一些API(JDBC、JavaMail、远程调用等),都提供了封装,使这些API应用难度大大降低。
Spring框架可以说是当前Java世界中最为成功的框架,在企业实际应用中,大部分的企业架构都基于Spring框架。Spring的成功来自于理念,而不是技术,最核心的理念是控制反转(IOC/DI)和面向切面编程(AOP),以及声明式事务。其中IOC是spring的基础,AOP则是其重要的功能,最为典型的当属数据库事务的使用。
spring框架已经融入了J2EE开发的各个领域,不论是数据访问层,还是控制层,又或是表现层,全都可以看到spring的身影。
Spring体系结构:
Spring框架至今已集成了20多个模块,这些模块分布在以下模块中:
- 核心容器(Core Container)
- 数据访问/集成(Data Access/Integration)层
- Web层
- AOP(Aspect Oriented Programming)模块
- 植入(Instrumentation)模块
- 消息传输(Messaging)
- 测试(Test)模块
核心容器:
Spring的核心容器是其他模块建立的基础,有Spring-core、Spring-beans、Spring-context、Spring-context-support和Spring-expression(Spring表达式语言)等模块组成。
- Spring-core模块:提供了框架的基本组成部分,包括控制反转(Inversion of Control,IOC)和依赖注入(Dependency Injection,DI)功能。
- Spring-beans模块:提供了BeanFactory,是工厂模式的一个经典实现,Spring将管理对象称为Bean。
- Spring-context模块:建立在Core和Beans模块的基础之上,提供一个框架式的对象访问方式,是访问定义和配置的任何对象的媒介。ApplicationContext接口是Context模块的焦点。
- Spring-context-support模块:支持整合第三方库到Spring应用程序上下文,特别是用于高速缓存(EhCache、JCache)和任务调度(CommonJ、Quartz)的支持。
- Spring-expression模块:提供了强大的表达式语言去支持运行时查询和操作对象图。这是对JSP2.1规范中规定的统一表达式语言(Unified EL)的扩展。该语言支持设置和获取属性值、属性分配、方法调用、访问数组、集合和索引器的内容、逻辑和算术运算、变量命名以及从Spring的IOC容器中以名称检索对象。它还支持列表投影、选择以及常用的列表聚合。
AOP和Instrumentation
- Spring-aop模块:提供了一个符合AOP要求的面向切面的编程实现,允许定义方法拦截器和切入点,将代码按照功能进行分离,以便干净地解耦。
- Spring-aspects模块:提供了与AspectJ的集成功能,AspectJ是一个功能强大且成熟的AOP框架。
- Spring-instrument模块:提供了类植入(Instrumentation)支持和类加载器的实现,可以在特定的应用服务器中使用。
消息:
Spring4.0以后新增了消息(Spring-messaging)模块,该模块提供了对消息传递体系结构和协议的支持。
数据访问/集成
数据访问/集成层由JDBC、ORM、OXM、JMS和事务模块组成。
- Spring-jdbc模块:提供了一个JDBC的抽象层,消除了烦琐的JDBC编码和数据库厂商特有的错误代码解析。
- Spring-orm模块:为流行的对象关系映射(Object-Relational Mapping)API提供集成层,包括JPA和Hibernate。使用Spring-orm模块可以将这些O/R映射框架与Spring提供的所有其他功能结合使用,例如声明式事务管理功能。
- Spring-oxm模块:提供了一个支持对象/XML映射的抽象层实现,例如JAXB、Castor、JiBX和XStream。
- Spring-jms模块(Java Messaging Service):指Java消息传递服务,包含用于生产和使用消息的功能。自Spring4.1以后,提供了与Spring-messaging模块的集成。
- Spring-tx模块(事务模块):支持用于实现特殊接口和所有POJO(普通Java对象)类的编程和声明式事务管理。
Web:
Web层由Spring-web、Spring-webmvc、Spring-websocket和Portlet模块组成。
- Spring-web模块:提供了基本的Web开发集成功能,例如多文件上传功能、使用Servlet监听器初始化一个IOC容器以及Web应用上下文。
- Spring-webmvc模块:也称为Web-Servlet模块,包含用于web应用程序的Spring MVC和REST Web Services实现。Spring MVC框架提供了领域模型代码和Web表单之间的清晰分离,并与Spring Framework的所有其他功能集成。
- Spring-websocket模块:Spring4.0以后新增的模块,它提供了WebSocket和SocketJS的实现。
- Portlet模块:类似于Servlet模块的功能,提供了Portlet环境下的MVC实现。
测试:
Spring-test模块支持使用JUnit或TestNG对Spring组件进行单元测试和集成测试。
spring环境搭建
使用Maven搭建spring环境,在pom.xml中进行以下依赖即可:
IoC/DI:控制反转/依赖注入
控制反转是软件设计大师 Martin Fowler在 2004 年发表的《Inversion of Control Containers and the Dependency Injection pattern》提出的。这篇文章系统阐述了控制反转的思想,提出了控制反转有依赖查找和依赖注入实现方式。
控制反转是一种通过描述(XML或注解)并通过第三方去产生或获取特定对象的方式。使用控制反转带来的最大好处就是降低对象之间的耦合。
程序中对象的产生是基于IoC容器,而不是开发者主动的行为。开发者主动创建的模式,责任归于开发者,在使用IoC容器被动创建的模式下,责任归于IoC容器。基于这样的被动形式,我们就说对象被控制反转了。
spring支持XML和注解两种方式实现IoC。
自定义IoC框架理解IoC/DI
需求:模拟通过配置文件实现IoC/DI、通过注解实现IoC/DI。
1.通过xml配置文件实现自定义IoC框架
//模拟三层架构
//dao
public class UserDao {
public void dao(){
System.out.println("dao层方法执行!!!");
}
}
//service
public class UserService {
private UserDao userDao=new UserDao();
public void service(){
System.out.println("service层方法执行!!!");
userDao.dao();
}
}
//servlet
public class UserServlet {
public static void main(String[] args) {
UserService service = new UserService();
userService.service();
}
}
2.理解IoC框架要实现的功能
3.创建对应的类
//PropertyDifinition
public class PropertyDifinition {
private String name;
private String ref;
//setter/getter......
}
//BeanDifinition
public class BeanDifinition {
private String id;
private String className;
private List propertyDifinitions;
//setter/getter......
}
//ApplicationContext:容器类型有多种,通过读取数据方式不同而不同,此处需要使用两种:xml或注解
public interface ApplicationContext {
void addBean(String id,Object object);//将实例存入容器
Object getBean(String id);//根据实例唯一标识从容器中取出实例
}
//ClasspathXmlApplicatonContext:先实现读取xml文件完成IoC
public class ClasspathXmlApplicationContext implements ApplicationContext{
private Map<String,Object> beans=new HashMap<>();//容器
public ClasspathXmlApplicationContext() {
BeanFactory beanFactory = new BeanFactory(this);//将容器传入BeanFactory,方便调用
beanFactory.initBean();//初始化bean--控制反转
beanFactory.dependencyInjection();//依赖注入
public void addBean(String id,Object object){
beans.put(id,object);//向容器中存放实例
}
public Object getBean(String id){
return beans.get(id);//从容器中取出实例
}
}
//BeanFactory
public class BeanFactory {
private List beanDifinitions=new ArrayList<>();//存放所有的标签信息
private ApplicationContext ApplicationContext;//
/**
* 在构造方法中读取配置文件,将配置文件内容存入beanDifinitions
* IoC容器分为两种类型:一种通过读取配置文件构建,一种通过读取类的注解构建
* 使用接口类型实现多态
* 需要注意的是:此处只为简单实现以理解IoC原理,没有去考虑xml与注解混用的情况
*/
public BeanFactory(ApplicationContext ApplicationContext) {
this.applicationContext=applicationContext;
String path = this.getClass().getResource("/").getPath();//获取项目根目录
//项目根目录以“/”开头时,可使用subString()用于去除,否则不必执行截取操作,但replace()操作必须有
path=path.substring(1).replace("/",File.separator);
//如果传入的容器是读取xml配置时,读取配置文件信息
if (applicationContext instanceof ClasspathXmlApplicationContext) {
try {
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(path+"applicationContext.xml");
Element root = document.getRootElement();
List<Element> beanElements = root.elements("bean");
//遍历所有bean标签,将bean标签信息存入BeanDifinition
beanElements.forEach(element -> {
BeanDifinition beanDifinition = new BeanDifinition();
String id = element.attributeValue("id");
String className = element.attributeValue("class");
beanDifinition.setId(id);
beanDifinition.setClassName(className);
//获取bean标签中所有property标签
List<Element> propertyElements = element.elements("property");
ArrayList<PropertyDifinition> propertyDifinitions = new ArrayList<>();
//遍历所有property标签,将property标签信息存入PropertyDifinition
propertyElements.forEach(element1 -> {
PropertyDifinition propertyDifinition = new PropertyDifinition();
String name = element1.attributeValue("name");
String ref = element1.attributeValue("ref");
propertyDifinition.setName(name);
propertyDifinition.setRef(ref);
propertyDifinitions.add(propertyDifinition);
});
beanDifinition.setPropertyDifinitions(propertyDifinitions);
beanDifinitions.add(beanDifinition);
});
} catch (DocumentException e) {
e.printStackTrace();
}
}
}
/**
* 根据beanDifinitions内容反射构建类实例
*/
public void initBean(){
//遍历所有BeanDifinition,生成对应的Bean实例,存入IoC容器
beanDifinitions.forEach(beanDifinition -> {
try {
String id = beanDifinition.getId();
String className = beanDifinition.getClassName();
//反射:根据类的全限定名字符串创建类的实例
Class<?> c = Class.forName(className);
Object instance = c.newInstance();
//存入容器
applicationContext.addBean(id,instance);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
});
}
/**
* 实现依赖注入
*/
public void dependencyInjection(){
beanDifinitions.forEach(beanDifinition -> {
String id = beanDifinition.getId();
String className = beanDifinition.getClassName();
List<PropertyDifinition> propertyDifinitions = beanDifinition.getPropertyDifinitions();
propertyDifinitions.forEach(propertyDifinition -> {
String name = propertyDifinition.getName();
String ref = propertyDifinition.getRef();
//需要注入的实例
Object originBean = applicationContext.getBean(id);
//用于注入的实例
Object refBean = applicationContext.getBean(ref);
try {
//反射获取需要注入属性
Class<?> c = Class.forName(className);
Field field = c.getDeclaredField(name);
//强制放开私有属性赋值操作
field.setAccessible(true);
//给私有属性赋值
field.set(originBean,refBean);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
});
});
}
}
4.在Servlet类中测试
//测试之前应将UserService类中创建UserDao实例的代码去掉
public class UserService {
private UserDao userDao;
public void service(){
System.out.println("service层方法执行!!!");
userDao.dao();
}
}
//使用自定义IoC框架中的容器获取UserService实例
public class UserServlet {
public static void main(String[] args) {
// UserService service = new UserService();
ApplicationContext applicationContext = new ClasspathXmlApplicationContext();
UserService userService = (UserService) applicationContext.getBean("userService");
userService.service();
}
}
5.增加注解配置
修改xml配置文件,在配置文件中指定框架递归扫描哪些包下的注解
6.修改BeanFactory,增加扫描注解的代码
//创建注解
@Target(ElementType.TYPE)//作用于类上,为创建实例做准备
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
String value() default "";
}
@Target(ElementType.FIELD)//作用于属性上,为依赖注入做准备
@Retention(RetentionPolicy.RUNTIME)
public @interface Resource {
}
//在UserDao、UserService类上加@Component注解,在UserService类中的userDao属性上增加@Resource注解
//增加容器类,用于注解配置
public class AnnotationConfigurationApplicationContext implements ApplicationContext{
private Map<String,Object> beans=new HashMap<>();
public AnnotationConfigurationApplicationContext() {
BeanFactory beanFactory = new BeanFactory(this);
beanFactory.initBean();
beanFactory.dependencyInjection();
}
public void addBean(String id,Object object){
beans.put(id,object);
}
public Object getBean(String id){
return beans.get(id);
}
}
//修改BeanFactory
public class BeanFactory {
private List beanDifinitions=new ArrayList<>();
private ApplicationContext applicationContext;
/**
* 在构造方法中读取配置文件,将配置文件内容存入beanDifinitions
*/
public BeanFactory(ApplicationContext applicationContext) {
this.applicationContext=applicationContext;
String path = this.getClass().getResource("/").getPath();//获取项目根目录
path=path.substring(1).replace("/",File.separator);
//如果是纯XML配置
if (applicationContext instanceof ClasspathXmlApplicationContext) {
......
}else if(applicationContext instanceof AnnotationConfigurationApplicationContext){//如果是注解配置
try {
//解析配置文件,获取扫描范围
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(path+"applicationContext.xml");
Element root = document.getRootElement();
Element element = root.element("component-scan");
String packageName = element.attributeValue("package");
packageName=packageName.replace(".",File.separator);
//递归扫描path所在路径下的所有文件
File directory = new File(path+packageName);
loopDirectory(directory,path);
} catch (DocumentException e) {
e.printStackTrace();
}
}
}
/**
* 递归遍历包下所有类,将被注解的类生成实例
*/
public void loopDirectory(File file,String path){
if (file.isDirectory()) {//如果是目录,递归遍历
File[] files = file.listFiles();
if (files != null && files.length > 0) {
for (File child : files) {
loopDirectory(child,path);
}
}
}else{//如果是文件,获取文件的绝对路径,从中筛选出所有以.class结尾的文件
String absolutePath = file.getAbsolutePath();
if (absolutePath.endsWith(".class")) {
//从绝对路径中获取类的全限定名
//示例:绝对路径为E:\a\b\c\com\woniu\dao\UserDao.class,path为E:\a\b\c\
//链式操作,replace(path,"")====》 com\woniu\dao\UserDao.class
//replace(".class","")====》 com\woniu\dao\UserDao
//replace(File.separator,".")====> com.woniu.dao.UserDao
String className=absolutePath.replace(path,"").replace(".class","").replace(File.separator,".");
try {
Class<?> c = Class.forName(className);
//查看类上是否有@Component注解
if (c.isAnnotationPresent(Component.class)) {
BeanDifinition beanDifinition = new BeanDifinition();
beanDifinition.setClassName(className);
String id=null;
//如果@Component注解没有指定value,使用的默认值(即类名首字母小写)
if (!"".equals(c.getDeclaredAnnotation(Component.class))) { id=c.getSimpleName().substring(0,1).toLowerCase()+c.getSimpleName().substring(1);
}else{//如果指定了value,以指定值为准
id=c.getDeclaredAnnotation(Component.class).value();
}
beanDifinition.setId(id);
//获取类中所有属性
Field[] fields = c.getDeclaredFields();
ArrayList<PropertyDifinition> propertyDifinitions = new ArrayList<>();
//如果属性上有@Resource注解,则进行依赖注入
if (fields != null && fields.length > 0) {
for (Field field : fields) {
if (field.isAnnotationPresent(Resource.class)) {
PropertyDifinition propertyDifinition = new PropertyDifinition();
propertyDifinition.setName(field.getName());
propertyDifinition.setRef(field.getName());
propertyDifinitions.add(propertyDifinition);
}
}
}
beanDifinition.setPropertyDifinitions(propertyDifinitions);
beanDifinitions.add(beanDifinition);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
}
7.测试
public class UserServlet {
public static void main(String[] args) {
// UserService service = new UserService();
AnnotationConfigurationApplicationContext applicationContext = new AnnotationConfigurationApplicationContext();
UserService userService = (UserService) applicationContext.getBean("userService");
userService.service();
}
}
spring:注解实现
在实际应用开发过程中,更多的会考虑使用注解而不是XML来装配bean。因为使用注解的方式可以大大减少XML配置,且功能更为强大。注解不但实现了XML的功能,还提供了自动装配功能,采用了自动装配后,开发人员需要做的决断就变少了,从而更有利于程序的开发,这也体现了“约定优于配置”的开发原则。<br /> 在spring中,提供了两种方式来让spring容器发现bean:
- 组件扫描:通过定义资源的方式,让spring容器扫描对应的包,从而把bean装配进来;
自动装配:通过注解定义,使得一些依赖关系可以通过注解完成;
通过扫描和自动装配,绝大部分的项目都可以用java配置完成,而不是XML,目前注解已经成为spring开发的主流。
spring IoC容器使用的注解有四类:创建对象,其作用相当在配置一个
标签; - 注入数据,其作用相当于在
标签中配置子标签 ; - 控制作用域,其作用相当于为
标签配置scope属性; 生命周期相关,其作用相当于为
标签配置init-method和destroy-method属性; spring p命名空间和c命名空间
在通过构造方法或set方法给bean注入关联项时通常是通过constructor-arg元素和property元素来定义的。在有了p命名空间和c命名空间时我们可以简单的把它们当做bean的一个属性来进行定义。
p命名空间
使用p命名空间时需要先声明使用对应的命名空间,即在beans元素上加入xmlns:p=”http://www.springframework.org/schema/p"。
c命名空间
c命名空间的用法和p命名空间类似,其对应于constructor-arg,即可以将constructor-arg元素替换为bean的一个以c命名空间前缀开始的属性。使用c命名空间之前也需要通过xmlns:c=”http://www.springframework.org/schema/c”进行声明。
<bean id="student" class="domain.Student" p:sid="1" p:sname="张三" p:ssex="1" p:sage="35" p:saddress="北京海淀" p:sbirthday-ref="sqlDate" p:cid="3"/> <bean id="sqlDate" class="java.sql.Date" c:_0="95" c:_1="5" c:_2="25"/>
在 Spring IoC 容器中具有以下几种作用域:
singleton:单例模式,默认,在整个Spring IoC容器中,使用singleton定义的Bean将只有一个实例,适用于无状态bean;
- prototype:原型模式(多例),每次通过容器的getBean方法获取prototype定义的Bean时,都将产生一个新的Bean实例,适用于有状态的Bean;
- request:对于每次HTTP请求,使用request定义的Bean都将产生一个新实例,即每次HTTP请求将会产生不同的Bean实例。在Web应用中使用Spring时,该作用域才有效;
- session:对于每次HTTP Session,使用session定义的Bean都将产生一个新实例。在Web应用中使用Spring时,该作用域才有效;
- globalsession:每个全局的HTTP Session,使用session定义的Bean都将产生一个新实例。典型情况下,仅在使用portlet context的时候有效。在集群环境下使用spring时,该作用域生效,如不是集群环境,该作用域等同于session。
面试相关: 面试时有些人会被问到Spring中Bean的生命周期,其实也就是考察一下对Spring是否熟悉,一般在工作中很少用到其中的内容。 1、实例化一个Bean(也就是我们常说的new); 2、按照Spring上下文对实例化的Bean进行配置(也就是IOC注入); 3、如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String)方法,此处传递的就是Spring配置文件中Bean的id值; 4、如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory(setBeanFactory(BeanFactory)传递的是Spring工厂自身(可以用这个方式来获取其它Bean,只需在Spring配置文件中配置一个普通的Bean就可以); 5、如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文(同样这个方式也可以实现步骤4的内容,但比4更好,因为ApplicationContext是BeanFactory的子接口,有更多的实现方法); 6、如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessBeforeInitialization(Object obj, String s)方法,BeanPostProcessor经常被用作是Bean内容的更改,并且由于这个是在Bean初始化结束时调用那个的方法,也可以被应用于内存或缓存技术; 7、如果Bean在Spring配置文件中配置了init-method属性会自动调用其配置的初始化方法; 8、如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj, String s)方法; 以上工作完成以后就可以应用这个Bean了,那这个Bean是一个Singleton的,所以一般情况下我们调用同一个id的Bean会是在内容地址相同的实例,当然在Spring配置文件中也可以配置非Singleton,这里我们不做赘述。 9、当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用那个其实现的destroy()方法; 10、如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。 以上10步骤可以作为面试或者笔试的模板,另外我们这里描述的是应用Spring上下文(applicationContext)Bean的生命周期,如果应用Spring的工厂(BeanFactory)的话,去掉第5步即可。
Spring Bean 的生命周期在整个 Spring 中占有很重要的位置,根据bean的作用域不同,其生命周期也是不相同的。<br /> **单例对象**:
- 出生:容器创建时,由于单例对象会立即加载,因此单例也就随着容器的创建就被创建了;
- 存活:当容器一直存在时,单例对象也一起存在;
死亡:当容器销毁时,单例对象随着容器的销毁一起销毁。
也就是说,单例对象的生命周期与spring容器保持一致。
多例对象:出生:容器创建时,多例对象延迟加载,直到使用该对象时spring容器执行创建操作;
- 存活:只要对象被使用,对象就一直存在;
死亡:多例对象不会随着容器销毁而销毁,它的销毁由java垃圾回收机制决定;
自动装配
spring框架为了提高开发效率,提供自动装配功能,可用于简化配置。自动装配功能默认不开启,要想使用自动装配,需要修改配置文件中标签的autowire属性。
自动装配属性有5个值可选,分别代表不同的含义。1.byName
从Spring环境中获取目标对象时,目标对象中的属性会根据名称在整个Spring环境中查找标签的id属性值。如果有相同的,那么获取这个对象,实现关联。
整个Spring环境:表示所有的spring配置文件中查找,那么id不能有重复的。2.byType
从Spring环境中获取目标对象时,目标对象中的属性会根据类型在整个spring环境中查找标签的class属性值。如果有相同的,那么获取这个对象,实现关联。
缺点:如果存在多个相同类型的bean对象,会出错。如果属性为单一类型的数据,那么查找到多个关联对象会发生错误。
如果属性为数组或集合(泛型)类型,那么查找到多个关联对象不会发生异常。
3.constructor
使用构造方法完成对象注入,其实也是根据构造方法的参数类型进行对象查找,相当于采用byType的方式。
如果spring容器中出现多个类型和构造函数中的类型相匹配的bean,那么bean的名称和要依赖的名称相同的将会注入进去,会自动将同类不同名的bean过滤掉;
- 如果只有一个bean,类型相同但是名称不同,也会将这个注入到该类中;
- 如果有两个以上bean,类名相同,但是名称不同,这个时候spring容器不知道选择哪一个bean,需要使用autowire-candidate=”false”进行过滤;
4.no
不支持自动装配功能。5.default
表示默认采用上一级标签的自动装配的取值。如果存在多个配置文件的话,那么每一个配置文件的自动装配方式都是独立的。public class Test { public static void main(String[] args) { //1、通过反射生成spring核心容器 ApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class); //2、根据bean标签id获取spring核心容器管理的bean实例 Subject sub = (Subject) ac.getBean("subject"); System.out.println(sub); } }
AOP
AOP:Aspect Oriented Programming的缩写,意为:面向切面编程,可以通过预编译方式和运行期动态代理,实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。线程同步。
AOP术语
通知(advice):代理对象的方法。分为前置通知,返回通知,异常通知,最终通知,环绕通知五类。以反射执行目标对象方法为基准:
- 前置通知:在目标对象方法执行之前执行;
- 最终通知:在目标对象方法执行完成后,无论在执行过程中是否出现异常,都会执行;
- 异常通知:目标对象方法执行过程中产生异常后执行;
- 返回通知:目标对象方法执行过程中不产生异常时执行;
- 环绕通知:特殊的通知,不以目标对象方法为基准,可以同时实现前置和返回通知。环绕通知保留了调度目标对象原有方法的功能,也就是说,使用环绕通知甚至可以不必要调用目标对象方法,可以取代目标对象的方法。因此环绕通知非常强大,而且灵活,但可控性差,一般不需要大量改变业务逻辑的情况下,不会使用环绕通知。
织入(weaving):将通知组合到目标的过程。
连接点(join point):能被代理对象增强的方法。
切入点(pointcut):真正被增强的方法。
注意:只有被切入点表达式匹配的连接点,才能成为切入点。切入点一定是连接点,而连接点不一定是切入点,因为连接点有可能不需要进行增强。
切面(aspect):通知与切入点的结合。
引入(Introduction):为代理对象增加方法。
环绕通知
环绕通知是spring中最为强大和灵活的通知,使用它可以得到目标方法的完全控制权:控制目标方法是否执行、控制目标方法参数、控制目标方法返回值。 spring框架为我们提供了一个接口:proceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。 环绕通知的使用: 修改事务管理类:TransactionManager.java
@Component
@Aspect
public class TransactionManager {
@Autowired
private ConnectionUtils connectionUtils;
@Pointcut("execution(* com.woniuxy.service.impl.*.*(..))")
private void pointCut(){}
public void beginTransaction(){
try {
connectionUtils.getThredLocalConnection().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}
public void commit(){
try {
connectionUtils.getThredLocalConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
public void rollback(){
try {
connectionUtils.getThredLocalConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
public void close(){
try {
connectionUtils.getThredLocalConnection().close();
connectionUtils.unbindThreadLocalConnection();
} catch (SQLException e) {
e.printStackTrace();
}
}
//配置环绕通知,手动控制通知执行顺序
@Around("pointCut()")
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
Object retValue=null;
try {
//获取目标方法的所有参数
Object[] args=proceedingJoinPoint.getArgs();
this.beginTransaction();
//执行目标方法
retValue = proceedingJoinPoint.proceed(args);
this.commit();
return retValue;
} catch (Throwable e) {
this.rollback();
throw new RuntimeException(e);
} finally {
this.close();
}
}
}
运行测试类,测试异常和正常执行,执行结果正常。
声明式事务
在spring中,数据库事务可以使用声明式事务,也可以使用编程式事务。但编程式事务会造成大量代码冗余,代码可读性差,因此目前基本上都使用声明式事务。
spring的数据库事务是通过PlatformTransactionManager进行管理的,该接口下有两个常用的实现类:DataSourceTransactionManager和HibernateTransactionManager(专供Hibernate框架使用)。
对于spring来说,事务控制中的前置通知(开启事务)和后置通知(释放资源)属于公共部分,每个事务都需要进行对应操作,因此spring已经对其进行了封装,在PlatformTransactionManager接口中只提供了commit()和rollback(),用于对返回通知和异常通知进行处理。
spring声明式事务(XML)
需求:使用spring声明式事务进行事务控制(XML配置实现),修改之前的XML配置实现spring AOP转账案例即可。
使用XML实现spring声明式事务的步骤:
- 配置事务管理器;
- 配置事务通知:为业务层方法配置事务,并设置事务属性;
- 配置切面:配置切入点表达式,并让切入点表达式与事务通知产生关联;
步骤1:在XML实现spring AOP项目的的基础上导入spring-tx,该jar包是spring事务控制的依赖,同时本案例使用JdbcTemplate实现,commons-dbutils不再需要。
步骤2:删除ConnectionUtils.java和TransactionManager.java。
步骤3:修改dao层实现类AccountDaoImpl.java
public class AccountDaoImpl implements AccountDao {
private JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public Account findById(int aid) {
try {
List<Account> accounts = jdbcTemplate.query("select * from t_account where aid=?",
new BeanPropertyRowMapper<Account>(Account.class), aid);
return accounts.isEmpty()?null:accounts.get(0);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public Account findByName(String userName) {
try {
List<Account> accounts = jdbcTemplate.query("select * from t_account where userName=?",
new BeanPropertyRowMapper<Account>(Account.class), userName);
if (accounts.isEmpty()) {
return null;
}
if (accounts.size() > 1) {
throw new RuntimeException("结果集不唯一,数据有问题");
}
return accounts.get(0);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void updateAccount(Account account) {
try {
jdbcTemplate.update("update t_account set balance=? where userName=?",
account.getBalance(), account.getUserName());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
步骤4:修改核心配置文件applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- 要使用声明式事务,需要引入tx和aop约束,在data access中查找xmlns:tx即可 -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 配置业务层 -->
<bean id="accountService" class="com.woniuxy.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"/>
</bean>
<!-- 配置数据访问层 -->
<bean id="accountDao" class="com.woniuxy.dao.impl.AccountDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<!-- 配置jdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/taotao"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置事务通知:
id:事务通知的唯一标识
transaction-manager:引入事务管理器处理事务通知
-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 配置事务通知的属性
isolation:设定事务隔离级别,用于保证数据的完整性。
DEFAULT,默认使用数据库的事务隔离级别
READ_UNCOMMITTED,可读取未提交数据,有可能出现脏读、不可重复读、幻读
READ_COMMITTED,可读取已提交数据,有可能出现不可重复读、幻读
REPEATABLE_READ,读取的数据表被加行锁,有可能出现幻读
SERIALIZABLE,读取的数据表加表锁,安全程度最高
propagation:当被事务控制的业务方法进行相互调用时,设定事务的传播行为。
REQUIRED,默认,之前的操作中有事务时加入该事务,没有事务时创建一个事务(增删改);
SUPPORTS,之前的操作中有事务时加入该事务,没有事务时不使用事务(查询)
MANDATORY,必须在事务内部执行,没有事务就报异常
REQUIRES_NEW,将原有事务挂起,新建一个事务执行自己的操作,两个事务之间没有关联
NOT_SUPPORTED,必须在非事务内部执行,如果有事务存在,将事务挂起,执行自己的操作
NEVER,不能在事务内部执行,有事务就报异常
NESTED,之前的操作有事务时,创建一个嵌套事务,两个事务之间会产生关联
read-only:设定事务是否只读。默认false,读写(增删改),true,只读(查询)
timeout:设定事务的超时时间,默认-1,永不超时,设定数值时,以秒为单位计算
no-rollback-for:
设定一个异常,事务执行过程中出现该异常时不回滚,其它异常会回滚,不设默认全回滚
rollback-for:
设定一个异常,事务执行过程中出现该异常时回滚,其它异常不会回滚,不设默认全回滚
建议手动抛异常时设定该属性
-->
<tx:attributes>
<!-- 配置需要事务控制的方法
name属性指定业务层方法名,可使用*进行通配
一般会使用*通配所有业务层方法,然后使用某些特定规则将查询方法单独标记
比如下例中的*通配所有业务层方法,find*匹配以find开头的所有业务层方法,
find*影响范围比*小,优先级比*高,不会造成执行异常。
建议查询方法会使用固定方式进行命名。
-->
<tx:method name="*" propagation="REQUIRED" read-only="false"/>
<tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice>
<!-- 配置AOP -->
<aop:config>
<!-- 配置切入点表达式 -->
<aop:pointcut id="pointCut" expression="execution(* com.woniuxy.service.impl.*.*(..))"/>
<!-- 建立切入点表达式与事务通知的关联 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointCut"/>
</aop:config>
</beans>
运行测试类,通过对transfer()方法中“int i=1/0;”的注释与放开,运行结果正常,事务控制成功实现。
spring声明式事务(注解)
spring声明式事务使用注解实现时,需要使用@Transactional。该注解代替了XML配置中的事务通知标签\的功能,通过该注解的对应属性配置,一样可对事务的隔离级别、传播行为等属性进行设置。
需求:使用spring声明式事务进行事务控制(XML+注解实现)。
使用注解实现spring声明式事务的步骤:
- 配置事务管理器
- 开启spring对注解事务的支持
- 在需要事务控制的地方使用@Transactional注解实现事务配置
步骤1:修改核心配置文件applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置spring容器启动时扫描的包 -->
<context:component-scan base-package="com.woniuxy"/>
<!-- 配置jdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/taotao"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 开启spring对注解事务的支持 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
步骤2:修改dao层和service层接口实现类
AccountDaoImpl.java
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {
@Autowired
private JdbcTemplate jdbcTemplate;
......
}
AccountServiceImpl.java
@Service("accountService")
//使用@Transactional将当前类中所有方法添加事务管理,如不设置@Transactional的属性,全部使用默认值。
@Transactional
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
//对查询方法单独配置传播行为和是否只读
@Transactional(propagation = Propagation.REQUIRED,readOnly = true)
@Override
public Account findById(int aid) {
Account account = accountDao.findById(aid);
return account;
}
@Transactional(propagation = Propagation.REQUIRED,readOnly = true)
@Override
public Account findByName(String userName) {
Account account = accountDao.findByName(userName);
return account;
}
//类上已做通用配置,如果此处希望使用通用配置,可不使用@Transactional注解。
@Override
public void transfer(String sourceUserName, String targetUserName, Double transferAmount) {
Account source = accountDao.findByName(sourceUserName);
Account target = accountDao.findByName(targetUserName);
source.setBalance(source.getBalance()-transferAmount);
target.setBalance(target.getBalance()+transferAmount);
accountDao.updateAccount(source);
int i=1/0;
accountDao.updateAccount(target);
}
}
运行测试类,结果运行正常,事务控制成功实现。
建议不要使用注解实现事务控制,因为当业务层实现类中方法太多,而且查询和增删改方法分布较为平均,此时需要在多个方法上使用@Transactional进行单独配置,造成代码冗余,而使用XML配置的方式可以做到一次配置,多次重用。
spring整合mybatis
目前绝大部分Java互联网项目,都是使用spring MVC+spring+mybatis搭建平台的。使用spring IoC可以有效管理各类java资源,达到即插即拔功能;通过AOP框架,数据库事务可以委托给spring处理,消除很大一部分事务代码,配置mybatis的高灵活、可配置、可优化SQL等特性,完全可以构建高性能的大型网站。
mybatis并没有被整合到spring中,不能直接使用,但mybatis社区提供了Mybatis-Spring项目,以供spring使用,因此,如果要使用spring整合mybatis,需要导入mybatis-spring-..*.jar包(本课程使用1.3.2)。
需求:使用spring整合mybatis,将dao层代码交给mybatis实现。
配置mybatis-spring整合项目的步骤:
配置数据源;
配置SqlSessionFactory;
配置Mapper;
配置事务管理;
步骤1:在项目中导入mybatis-spring、mybatis核心包。
步骤2:创建映射器
映射器接口AccountMapper.java
public interface AccountMapper {
Account findByName(String userName);
void updateAccount(Account account);
}
映射器XML文件AccountMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.woniuxy.mapper.AccountMapper">
<!-- 根据名称查询账户 -->
<select id="findByName" parameterType="string" resultType="account">
select * from t_account where userName=#{userName}
</select>
<!-- 修改账户信息 -->
<update id="updateAccount" parameterType="account">
update t_account set balance=#{balance} where userName=#{userName}
</update>
</mapper>
步骤3:修改spring核心配置文件applicationContext.xml
<!-- 配置sqlSessionFactory,用于生产SqlSession,
只要sqlSessionFactory被spring容器管理,IoC容器中的其他bean就可以获取sqlSession实例
-->
<!-- mybatis -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 向mybatis注入运行环境 -->
<property name="dataSource" ref="dataSource"/>
<!-- 引用mybatis核心配置文件 -->
<property name="configLocation" value="classpath:sqlMapConfig.xml"/>
</bean>
<bean id="factory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 向mybatis注入数据源 -->
<property name="dataSource" ref="dataSource"/>
<!-- 包扫描创建实体类别名 -->
<property name="typeAliasesPackage" value="com.woniuxy.domain"/>
</bean>
<!-- 配置扫描器
MapperScannerConfigurer会自动扫描对应路径下的Mapper接口,并生成代理注入到Spring容器
扫描器如不被引用,id属性可省略不写。
如果使用了多个数据源,需要使用sqlSessionFactoryBeanName或sqlSessionTemplateBeanName指定正确的bean名称来使用。只有一个数据源时,该属性不是必须指定的,可以省略。
-->
<bean id="mapper" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--<property name="sqlSessionFactoryBeanName" value="factory"/>-->
<!-- 包扫描创建映射器注入spring容器 -->
<property name="basePackage" value="com.woniuxy.mapper"/>
</bean>
<!-- 声明式事务 -->
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 开启spring对事务注解的支持 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
sqlMapConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 复杂的mybatis配置 -->
......
</configuration>