Spring 是分层的 Java SE/EE 应用 full-stack 轻量级开源框架,以 IoC(Inverse Of Control)控制反转 和 AOP(Aspect Oriented Programming)面向切面编程为内核,提供了展示层 Spring MVC 和持久层 Spring JDBC 以及业务层事务管理等技术,还能整合开源的第三方框架和类库,是使用最多的 Java EE 企业应用开源框架。java
咱们日常说的 Spring 指的是 Spring Framework,其为 Java 程序提供全面的基础架构支持,Spring 处理基础结构,使得咱们能够专一于业务自己。是非入侵式框架(导入项目不会破坏原有项目代码)mysql
Spring 之父:Rod Johnson程序员
Spring Framework 由组成大约 20 个模块的 feature 组成,这些模块分为:web
Core Container 核心容器正则表达式
Date Access/Integration 数据访问/整合spring
Websql
AOP (Aspect Oriented Programming) 面向切面编程数据库
Instrumentation 检测express
Messaging 消息apache
Test
在面向对象编程中,咱们常常处理的问题就是解耦,程序的耦合性越低,表名这个程序的可读性以及可维护性越高。IoC(Inversion of Control) 控制反转,就是经常使用的面向对象编程的设计原则,使用这个原则咱们能够下降耦合性。其中依赖注入是控制反转最多见的实现。
耦合
程序间的依赖关系
包括:类之间的依赖
方法之间的依赖
解耦
下降程序间的依赖关系
咱们在开发中,有些依赖关系是必须的,有些依赖关系能够经过优化代码来解除的。
因此实际开发中应作到:编译期不依赖,运行时才依赖
解耦思路:
范例:JDBC 链接数据库
public static void main(String[] args) throws Exception{ //1.注册驱动 /*使用new对象的方式注册驱动 DriverManager.registerDriver(new com.mysql.jdbc.Driver());*/ /*使用反射方式建立对象注册驱动,此时配置内容只做为一个字符串传递 Class.forName("com.mysql.jdbc.Driver");*/ //而经过读取配置文件的方式,解决上面将字符串在代码中写死的问题,便于修改配置 Properties properties = new Properties(); properties.load(new FileInputStream("src/main/resources/data.properties")); //略... //2.获取链接 Connection conn = DriverManager.getConnection(url,user,password); //3.获取操做数据库的预处理对象 PrepareStatement ps = conn.prepareStatement("select * from tb_students"); //4.执行SQL,获取结果集 result = ps.executeQuery(); //5.遍历结果集 while(result.next()){ int no = result.getInt("no"); String name = result.getString("name"); System.out.println(no + "," + name); //6.释放资源 result.close(); ps.close(); conn.close(); }
传统的 JDBC 获取链接方式也是为了解耦而使用读取配置文件的方式配置数据源。
耦合性(Coupling),也叫耦合度,是对模块间关联程度的度量。耦合的强弱取决于模块间接口的复杂性、调 用模块的方式以及经过界面传送数据的多少。模块间的耦合度是指模块之间的依赖关系,包括控制关系、调用关系、数据传递关系。模块间联系越多,其耦合性越强,同时代表其独立性越差( 下降耦合性,能够提升其独立 性)。耦合性存在于各个领域,而非软件设计中独有的,可是咱们只讨论软件工程中的耦合。
在软件工程中,耦合指的就是就是对象之间的依赖性。对象之间的耦合越高,维护成本越高。所以对象的设计应使类和构件之间的耦合最小。软件设计中一般用耦合度和内聚度做为衡量模块独立程度的标准。划分模块的一个 准则就是高内聚低耦合
它有以下分类:
(1) 内容耦合。当一个模块直接修改或操做另外一个模块的数据时,或一个模块不经过正常入口而转入另 一个模块时,这样的耦合被称为内容耦合。内容耦合是最高程度的耦合,应该避免使用之。 (2) 公共耦合。两个或两个以上的模块共同引用一个全局数据项,这种耦合被称为公共耦合。在具备大 量公共耦合的结构中,肯定到底是哪一个模块给全局变量赋了一个特定的值是十分困难的。 (3) 外部耦合 。一组模块都访问同一全局简单变量而不是同一全局数据结构,并且不是经过参数表传 递该全局变量的信息,则称之为外部耦合。
(4) 控制耦合 。一个模块经过接口向另外一个模块传递一个控制信号,接受信号的模块根据信号值而进 行适当的动做,这种耦合被称为控制耦合。
(5) 标记耦合 。若一个模块 A 经过接口向两个模块 B 和 C 传递一个公共参数,那么称模块 B 和 C 之间 存在一个标记耦合。
(6) 数据耦合。模块之间经过参数来传递数据,那么被称为数据耦合。数据耦合是最低的一种耦合形 式,系统中通常都存在这种类型的耦合,由于为了完成一些有意义的功能,每每须要将某些模块的输出数据做为另
一些模块的输入数据。 (7) 非直接耦合 。两个模块之间没有直接关系,它们之间的联系彻底是经过主模块的控制和调用来实 现的。
总结: 耦合是影响软件复杂程度和设计质量的一个重要因素,在设计上咱们应采用如下原则:若是模块间必须 存在耦合,就尽可能使用数据耦合,少用控制耦合,限制公共耦合的范围,尽可能避免使用内容耦合。
内聚与耦合
内聚标志一个模块内各个元素彼此结合的紧密程度,它是信息隐蔽和局部化概念的天然扩展。内聚是从 功能角度来度量模块内的联系,一个好的内聚模块应当刚好作一件事。它描述的是模块内的功能联系。耦合是软件结构中各模块之间相互链接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及经过接口的数据。 程序讲究的是低耦合,高内聚。就是同一个模块内的各个元素之间要高度紧密,可是各个模块之 间的相互依存度却要不那么紧密。
内聚和耦合是密切相关的,同其余模块存在高耦合的模块意味着低内聚,而高内聚的模块意味着该模块同其余模块之间是低耦合。在进行软件设计时,应力争作到高内聚,低耦合
具体到项目中,带来了哪些依赖问题呢:
先了解一下工厂模式解耦的思想,会给下面 Spring 控制反转使用带来启发。
在实际开发中咱们能够把三层的对象都使用配置文件配置起来,当启动服务器应用加载的时候,让一个类中的方法经过读取配置文件,把这些对象建立出来并存起来。在接下来的使用的时候,能够直接拿过来用。
那么,这个读取配置文件,建立和获取三层对象的类就是工厂(Factory)
范例:
项目结构:
对应代码:以表现层 - 业务层 - 持久层 - 工厂 顺序
表现层代码:
package com.yh.view; import com.yh.factory.BeanFactory; import com.yh.service.INameService; /** * 模拟一个表现层用于调用业务层 * @author YH * @create 2020-05-07 16:19 */ public class Cilent { public static void main(String[] args){ //想调用业务层方法依赖与其实现类对象 // INameService service = new NameServiceImpl(); INameService service = (INameService)BeanFactory.getBean("nameService"); System.out.println("表现层后台代码执行调用业务逻辑层:1"); service.method(); } }
业务层代码:
package com.yh.service; /** * 业务逻辑层接口 * @author YH * @create 2020-05-07 16:17 */ public interface INameService { void method(); }
package com.yh.service.impl; import com.yh.dao.INameDao; import com.yh.dao.impl.NameDaoImpl; import com.yh.factory.BeanFactory; import com.yh.service.INameService; /** * 模拟业务逻辑层调用持久层 * @author YH * @create 2020-05-07 16:18 */ public class NameServiceImpl implements INameService { @Override public void method() { //想调用持久层方法依赖与其实现类对象 // INameDao nameDao = new NameDaoImpl(); INameDao nameDao = (INameDao) BeanFactory.getBean("nameDao"); System.out.println("业务逻辑层实现类执行调用持久层:2"); nameDao.method(); } }
持久层代码:
package com.yh.dao; /** * 持久层接口 * @author YH * @create 2020-05-07 16:14 */ public interface INameDao { void method(); }
package com.yh.dao.impl; import com.yh.dao.INameDao; /** * 模拟持久层 * @author YH * @create 2020-05-07 16:15 */ public class NameDaoImpl implements INameDao { @Override public void method() { System.out.println("持久层dao执行 3"); } }
工厂:
package com.yh.factory; import java.io.InputStream; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.Properties; /** * 一个建立Bean对象的工厂 * Bean:在计算机英语中,有可重用组件的含义 * JavaBean:用java语言编写的可重用组件 * 注意:JavaBean不等于实体类,且包含实体类,即 JavaBean > 实体类 * * 建立service和dao对象 * * 1.须要经过配置文件读取配置,可用两种方式: xml 或 properties * 配置的内容:惟一标识=全限定类名(key-value) * 2.再经过读取配置文件中配置的内容,反射建立对象 * @author YH * @create 2020-05-07 17:14 */ public class BeanFactory { private static Properties props; /** * 定义一个Map,做为存储对象的容器,存放咱们要建立的对象 */ private static Map<String,Object> beans = null; /** * 静态代码块只执行一次,保证了从始至终只生成配置中对应的惟一一个实例 */ static { try { props = new Properties(); InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("factory.properties"); props.load(in); //实例化Map容器 beans = new HashMap<>(); //取出配置文件中全部的key Enumeration<Object> keys = props.keys(); //遍历枚举 while(keys.hasMoreElements()){ //取出每一个key String key = keys.nextElement().toString(); //根据key从配置中读取value String beanPath = props.getProperty(key); //反射建立实例对象 Object value = BeanFactory.class.forName(beanPath).newInstance(); //把key和value存入容器中 beans.put(key,value); } } catch (Exception e) { //读取配置文件出现异常那么后面的操做都无心义,因此直接声明一个错误终止程序 throw new ExceptionInInitializerError("初始化properties时发生错误!"); } } /** * 根据bean的名称获取bean对象 * @param beanName * @return */ public static Object getBean(String beanName){ return beans.get(beanName); } /** * 传入key的名称寻找对应的value全类名 并建立对象返回 * @param beanName * @return *//* public static Object getBean(String beanName){ Object bean = null; try { String beanPath = props.getProperty(beanName); //每次都会调用默认构造函数建立对象 bean = (Object) Class.forName(beanPath).newInstance(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } return bean; }*/ }
小结:经过工厂类初始化加载就将配置文件中所表明的类建立并存储到 Map 中,须要使用时调用工厂方法便可,避免了 new,即避免了反复建立对象,也下降了程序的耦合度
控制反转(Inversion Of Control)把建立对象的权利交给框架,是框架的重要特征,并不是面向对象编程的专用术语。它包括依赖注入(DI)和依赖查找(DL)
做用:消减计算机程序的耦合(解除咱们代码中的依赖关系)
以上面小节为例:
咱们经过工厂建立对象,将对象存储在容器中,提供获取对象的方法。在这个过程当中:
获取对象的方式发生了改变:
之前:获取对象,采用 new 的方式,是主动的
如今:经过工厂获取对象,工厂为咱们查找或者建立对象,是被动的
准备 spring 的开发包
以上一节工厂解耦改成使用 spring
第一步:向项目的 pro.xml 文件中加入配置,将 spring 的 jar 包导入工程:
<!--设置打包方式--> <packaging>jar</packaging> <dependencies> <!-- 导入spring--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.2.RELEASE</version> </dependency> </dependencies>
第二步:在资源目录下建立一个 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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- bean标签:用于配置让spring建立对象,而且存入IOC容器之中 id属性:对象的惟一标识 class属性:指定要建立对象的全限定类名 --> <bean id="dao" class="yh.dao.impl.NameDaoImpl"></bean> <bean id="service" class="yh.service.impl.NameServiceImpl"></bean> </beans>
第三步:让 spring 管理资源,在配置文件中配置 service 和 dao
public class Client { /** * 获取spring的核心容器 并根据id获取对象 * @param args */ public static void main(String[] args){ //1.获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml"); //2.根据id获取bean对象 INameDao dao = (INameDao)ac.getBean("dao"); INameService service = ac.getBean("service",INameService.class); System.out.println(dao); System.out.println(service); } }
测试配置是否成功:
Spring 中工厂的类结构:
能够看出 BeanFactory 是 Spring 容器中的顶层接口,ApplicationContext 是其子接口,它们建立对象的时间点的区别:
ApplicationContext:只要一读取配置文件,默认状况下就会建立对象(即时建立),能够推断:即时建立对象适合使用在单例模式的场景,对象只建立一次
BeanFactory:何时使用对象了,才会建立对象(延迟建立),同理:延迟建立对象适合于多例模式的场景,节省性能开销
ApplicationContext 的三个经常使用实现类:
控制反转 IoC,是一种设计思想,DI(依赖注入)是实现 IoC 的一种方法。没有 IoC 的程序中,使用面向对象编程,对象的建立与对象的依赖关系彻底硬编码在程序中,对象的建立由程序本身控制,控制反转后将对象的建立转移给第三方,即得到依赖对象的方式反转了。
IoC 是 Spring 框架的核心内容,使用多种方式完美实现了 IoC,可使用 XML 配置,也可使用注解,新版本的 Spring 也能够零配置实现 IoC。Spring 容器在初始化时先读取配置文件,根据配置文件或元数据建立与组织对象存入容器中,程序使用时再从 IoC 容器中取出须要的对象。
控制反转是一种经过描述(XML 或注解)并经过第三方去生产或获取特定对象的方式,在 Spring 中实现控制反转的是 IoC 容器,其实现方法是依赖注入(Dependency Injection,DI)。
所谓控制反转,就是应用自己不负责依赖对象的建立及维护,依赖对象的建立及维护是由外部容器负责的。其中依赖注入是控制反转最多见的实现。
那咱们来先搞清这个依赖对象是什么,下面是传统三层架构的代码示例:
持久层:
//持久层接口 public interface IUserDao { void daoMethod(); }
//持久层接口实现1 public class UserDaoImpl implements IUserDao { public void daoMethod() { System.out.println("数据库链接1"); } }
//持久层接口实现2 public class UserDaoImpl2 implements IUserDao { public void daoMethod() { System.out.println("数据库链接2"); } }
持久层即数据访问层(DAL 层),其功能主要是负责数据库的访问,实现对数据表的 CEUD 等操做。
可能会有变动接口实现的需求(如 MySQL 换为 Oracle)
业务逻辑层:
//业务逻辑层接口 public interface IUserService { void serviceMethod(); }
//业务逻辑层接口实现 public class UserServiceImpl implements IUserService { //业务层须要或许持久层对象,调用其方法 IUserDao dao = new UserDaoImpl(); public void serviceMethod() { dao.daoMethod(); } }
三层架构的核心,其关注点是业务规则的制定、业务流程的实现等与业务需求有关的系统设计。
视图层(表示层):
@Test public void test1(){ //程序入口要获取业务层对象来调用功能 IUserService service = new UserServiceImpl(); service.serviceMethod(); }
表示层主要做用是与用户进行交互,显示数据(如打印到控制台的信息)和接收传输用户的数据,提供用户操做界面等。
运行结果:
这就是传统三层架构的一个调用流程,能够看出做为三层核心的业务层起的一个承上启下的做用。表示层与用户交互,要执行功能那么就须要先货到控制层的对象,调用相关功能。即没有业务层对象就无法实现操做,则表示层依赖于业务逻辑层,没它不行;一样的,业务逻辑层做为一个指挥全局的头,须要指挥小弟来办事,因此他先得有个小弟,那么就获取一个持久层对象了,一样是没有这个小弟无法办事,并且加入要办另一件事须要另外一个小弟,那业务层大哥也要作相应的调整(改代码)。此时业务逻辑层依赖于持久层。
真是世间美好与你环环相扣,变强了,头也就秃了(手动**)
针对变动持久层实现须要修改业务层代码的问题作一个优化,使用 set 方法注入方式获取对象,以下:
业务层实现类:
public class UserServiceImpl implements IUserService { /** * 对象注入 */ private IUserDao dao; public void set(IUserDao dao){ this.dao = dao; } public void serviceMethod() { dao.daoMethod(); } }
利用多态的特性可接收任何其实现对象,外部根据不一样的需求传递不一样的实现对象参数,从而避免了二次修改业务层代码。
测试代码:
@Test public void test1(){ //程序入口要获取业务层对象来调用功能 IUserService service = new UserServiceImpl(); // service.setDaoImpl(new UserDaoImpl()); service.setDaoImpl(new UserDaoImpl2()); service.serviceMethod(); }
传入不一样的实现参数,获取不一样的链接:
对比:
以前,程序主动建立对象,由程序员决定使用的功能(更改代码)
使用 set 注入后,程序变成被动接受对象,由使用者决定使用的功能(传递对应的参数)
这种让程序员再也不管理对象建立的思想,使得程序系统的耦合性大大下降,让程序员能够更加专一于业务的实现上,这就是 IoC 的原型。
对,是原型,起关键做用的就是 set 方法,它是得以注入的关键,下面就使用 Spring IoC 来创建第一个程序:
JavaBean:
public class Hello { private String name; //注意此set方法 public void setName(String name){ this.name = name; } public void run(){ System.out.println("Hello!" + name); } }
使用 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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!--配置元数据 bean的配置: 使用Spring建立对象,对象都用Bean表示 对比原有的new建立对象方式: Heelo hello = new Hello() 即 类型 变量名 = new 类型() - id 指定对象变量名 -> 变量名 - class 指定要建立的对象的类 - property 指定对象的属性 name 指定属性名 value 指定属性值 --> <bean id="hello" class="yh.pojo.Hello"> <property name="name" value="熊大"/> </bean> </beans>
测试代码:
@Test public void test1(){ //获取Spring的上下文对象 ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); //咱们的对象如今都在spring中管理了,咱们要使用,直接去里面取出来便可(取Bean) Hello hello = context.getBean("hello", Hello.class); //Hello hello = (Hello)context.getBean("hello"); hello.run(); }
结果:
整个过程当中:
hello 对象有 Spring 建立
hello 对象的属性也由 Spring 容器设置
这就是控制反转:
控制:传统程序的对象是由程序自己控制建立的,使用 Spring 后,对象是由 Spring 建立的。
反转:程序自己不建立对象,而变成被动地接收对象。
依赖注入:就是利用 set 方法进行注入。
IOC 就是一种编程思想,由主动的编程编编程被动的接收。
至此,咱们完全不用去程序中改动了,要实现不一样的操做,只须要在 xml 配置文件中进行修改,对象由 Spring 来建立、管理、装配。
如今咱们来修改最开始的那个传统实例,看看用 IoC 如何实现它:
配置文件:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="daoImpl" class="yh.dao.impl.UserDaoImpl"/> <bean id="daoImpl2" class="yh.dao.impl.UserDaoImpl2"/> <bean id="service" class="yh.service.impl.UserServiceImpl"> <!-- ref:引用spring容器中建立好的对象 value:具体的值类型数据 --> <property name="dao" ref="daoImpl"/> </bean> </beans>
因为业务层实现中本来就设置了 set 方法,因此能够直接配置注入属性的信息
注意:set 方法命名必定要按照规范,不然没法识别注入
其余地方都不用修改,直接进行测试:
@Test public void test1(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); IUserService service = context.getBean("service", IUserService.class); service.serviceMethod(); }
结果:
如需更改配置,直接修改配置文件中的 dao
属性值(配置文件不属于程序代码),以下:
<bean id="service" class="yh.service.impl.UserServiceImpl"> <!--更改引用值--> <property name="dao" ref="daoImpl2"/> </bean>
增长实现,或换实现原均可以经过元数据完成了。
做用:用于配置对象让 spring 来建立
默认状况下他调用的是类中的无参构造器,若是没有无参构造器则不能建立成功
属性:
id:给对象在容器中提供一个惟一标识,用于获取对象
class:指定类的全限定类名,用于反射建立对象。默认状况下调用无参构造器
scope:指定对象的做用范围
init-method:指定类中的初始化方法名称
destroy-method:指定类中销毁方法名称
第一种方式:使用构造器
<!--在默认状况下: 他会根据默认无参构造函数来建立类对象,若是bean中没有默认无参构造函数,将会建立失败--> <bean id="service" class="yh.service.impl.NameServiceImpl"></bean>
<bean id="hello" class="yh.pojo.Hello"> <constructor-arg name="name" value="Spring"/> </bean>
第二种方式:spring管理实例工厂,使用实例工厂的方法建立对象
<!--先把工厂的建立交给spring来管理,而后使用工厂bean来调用里面的方法(先建立工厂对象,再用其获取service对象) factory-bean 属性:用于指定实例工厂bean的id factory-method 属性:用于指定实例工厂中建立对象的方法 --> <bean id="instanceFactory" class="yh.factory.InstanceFactory"></bean> <bean id="nameService" factory-bean="instanceFactory" factory-method="createNameService"></bean>
第三种方式:spring管理静态工厂,使用静态工厂的方法建立对象
<!--使用StaticFactory类中的静态方法建立对象,并存入spring容器 id:指定bean的id,用于从容器中获取 class:指定静态工厂的全限定类名 factory-method 属性:指定生成对象的工厂静态方法 --> <bean id="nameService" class="yh.factory.StaticFactory" factory-method="createNameService"></bean>
调用类:
package yh.view; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import yh.service.INameService; /** * 模拟一个表现层用于调用业务层 * @author YH * @create 2020-05-07 16:19 */ public class Client { /** * 获取spring的核心容器 并根据id获取对象 * @param args */ public static void main(String[] args){ //1.获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml"); //2.根据id获取bean对象 INameService service = ac.getBean("nameService",INameService.class); System.out.println(service); } }
测试结果都能获取到对象:
在配置文件加载的时候,容器中管理的对象(Bean)就已经初始化了,须要哪一个对象经过 Spring 上下文对象直接获取便可(getBean()
)。
参考官方介绍:
范围 | 描述 |
---|---|
singleton | (默认)为每一个 Spring IoC 容器的单个 object 实例定义单个 bean 定义。 |
prototype | 为任意数量的 object 实例定义单个 bean 定义。 |
request | 将单个 bean 定义范围限定为单个 HTTP 请求的生命周期。也就是说,每一个 HTTP 请求都有本身的 bean 实例,该实例是在单个 bean 定义的后面建立的。仅在 web-aware Spring ApplicationContext 的 context 中有效。 |
session | 将单个 bean 定义范围限定为 HTTP Session 的生命周期。仅在 web-aware Spring ApplicationContext 的 context 中有效。 |
application | 将单个 bean 定义范围限定为ServletContext 的生命周期。仅在 web-aware Spring ApplicationContext 的 context 中有效。 |
websocket | 将单个 bean 定义范围限定为WebSocket 的生命周期。仅在 web-aware Spring ApplicationContext 的 context 中有效。 |
bean 对象的做用范围:
使用 scope 属性指定对象的做用范围,参数:
singleton:单例的(默认值)
prototype:多例的
request:WEB 项目中 Spring 建立一个 Bean 的对象,将对象存入到 request 域中
session:WEB 项目中 Spring 建立一个 Bean 的对象,将对象存入到 session 域中
global session:做用于集群环境的会话范围(全局会话范围),不是集群它就是 session
global session(全局变量)应用场景:
一个web工程可能有多个服务器分流,用户首次发送请求访问 web 时所链接的服务器和提交登陆所请求的服务器可能不一同一个服务器,可是验证码生成首先是从第一次访问时的服务器获取的,并保存在独有 session 中,提交登陆时确定须要比较验证码正确性,因为可能不在一个服务器没法验证,因此就须要 global session 这个全局变量,不管在哪一个服务器均可以验证
示意图:
生命周期:
单例对象:scope="singleton"
一个应用只有一个对象的实例,它的做用范围就是整个应用
对象出生:当应用加载,建立容器时,对象就被建立了
对象活着:只要容器在,对象一直活着
对象死亡:当应用卸载,容器销毁时,对象也被销毁
多例对象:scope="prototype"
每次访问时,都会从新建立对象实例
对象出生:当使用对象时,建立新的对象实例
对象活着:对象使用期间一直活着
对象死亡:当对象长时间不用,被java的垃圾回收机制回收了
依赖注入:Dependdency Injection。它是 spring 框架核心 IOC 的具体实现
咱们的程序在编写时,经过控制反转,把对象的建立交给了 spring,可是代码中不可能出现没有依赖的状况。ioc 解耦只是下降他们的依赖关系,但不会消除。例如:咱们的业务层仍会调用持久层的方法。
那这种业务层和持久的依赖关系,在使用 spring 以后,就让 spring 来维护了;
简单的说,就是坐等框架把持久层对象传入业务层,而不用咱们本身去获取
顾名思义,就是使用类中的构造函数,给成员变量赋值
要求:
类中须要提供一个对应的带参构造器
涉及的标签:
constructor-arg
属性:
index:指定要注入的数据给构造函数中指定索引位置的参数赋值,索引从0开始
type:指定要注入数据的数据类型,该类型也是某个或某些参数的类型
name:指定给构造器中指定名称的参数赋值
---------------以上三个属性用于指定要给哪一个参数赋值---------------
value:用于提供基本类型和String类型的数据
ref:用于指定其余的bean类型数据(即在spring的IOC核心容器中出现过的bean对象
- 优点:
在获取bean对象时,注入数据时必须的操做,不然对象没法建立成功
- 弊端:
改变了bean对象的实例化方式,调用有参构造器,使咱们在建立对象时,无论需不须要这些数据,也必须提供
xml 文件配置:
<!--使用构造函数的方式,给service中的属性传值--> <bean id="nameService" class="yh.service.impl.NameServiceImpl"> <constructor-arg name="name" value="云翯"></constructor-arg> <constructor-arg name="age" value="18"></constructor-arg> <constructor-arg name="birthday" ref="now"></constructor-arg> </bean> <!-- 配置一个日期对象--> <bean id="now" class="java.util.Date"></bean>
实现类提供有参构造器:
public class NameServiceImpl implements INameService { private String name; private Integer age; private Date birthday; public NameServiceImpl(String name, Integer age, Date birthday) { this.name = name; this.age = age; this.birthday = birthday; } @Override public void method() { System.out.println(name + "," + age + "," + birthday); } }
调用类:
public class Client { /** * 获取spring的核心容器 并根据id获取对象 * @param args */ public static void main(String[] args){ //1.获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml"); //2.根据id获取bean对象 INameService service = ac.getBean("nameService",INameService.class); service.method(); } }
测试结果:
涉及的标签:property
出现的位置:bean标签内部
属性:
name:指定所用的set方法名称
value:指定基本类型和String类型的数据
ref:指定其余bean类型数据(即spring的IOC核心容器中出现过的bean对象)
顾名思义,实现类中须要提供set方法。范例:
xml 配置文件:
<bean id="nameService1" class="yh.service.impl.NameServiceImpl1"> <property name="name" value="云翯1"></property> <property name="age" value="19"></property> <property name="birthday" ref="now"></property> </bean> <!-- 配置一个日期对象--> <bean id="now" class="java.util.Date"></bean>
带有 set() 方法的实现类:
public class NameServiceImpl1 implements INameService { private String name; private Integer age; private Date birthday; public NameServiceImpl1() {} public void setName(String name) { this.name = name; } public void setAge(Integer age) { this.age = age; } public void setBirthday(Date birthday) { this.birthday = birthday; } @Override public void method() { System.out.println(name + "," + age + "," + birthday); } }
调用类:
public class Client { /** * 获取spring的核心容器 并根据id获取对象 * @param args */ public static void main(String[] args){ //1.获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml"); //2.根据id获取bean对象 INameService service = ac.getBean("nameService1",INameService.class); service.method(); } }
测试结果:
注入集合数据(复杂类型注入)
顾名思义,就是给集合成员传值,他用的也是set方法注入的方式,只不过变量的数据类型都是集合。
用于给 List 结构集合注入的标签
list、array、set
用于给 Map 结构集合注入的标签
map、props
结构相同,标签能够互用
范例:
xml 配置文件:
<!-- 复杂类型的注入/集合类型的注入--> <bean id="nameService2" class="yh.service.impl.NameServiceImpl3"> <property name="myStrs"> <array> <value>AAA</value> <value>BBB</value> <value>CCC</value> </array> </property> <property name="myList"> <list> <value>AAA</value> <value>BBB</value> <value>CCC</value> </list> </property> <property name="mySet"> <set> <value>AAA</value> <value>BBB</value> <value>CCC</value> </set> </property> <property name="myMap"> <map> <entry key="testA" value="aaa"></entry> <entry key="testB" value="bbb"></entry> <entry key="testC" value="ccc"></entry> </map> </property> <property name="myProps"> <props> <prop key="testA">aaa</prop> <prop key="testB">bbb</prop> </props> </property> </bean>
集合等复杂类型的属性,一样使用set方法赋值:
public class NameServiceImpl3 implements INameService { private String[] myStrs; private List<String> myList; private Set<String> mySet; private Map<String,String> myMap; private Properties myProps; public void setMyStrs(String[] myStrs) { this.myStrs = myStrs; } public void setMyList(List<String> myList) { this.myList = myList; } public void setMySet(Set<String> mySet) { this.mySet = mySet; } public void setMyMap(Map<String,String> myMap) { this.myMap = myMap; } public void setMyProps(Properties myProps) { this.myProps = myProps; } @Override public void method() { System.out.println(myStrs); System.out.println(myList); System.out.println(mySet); System.out.println(myMap); System.out.println(myProps); } }
调用类:
public class Client { /** * 获取spring的核心容器 并根据id获取对象 * @param args */ public static void main(String[] args){ //1.获取核心容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml"); //2.根据id获取bean对象 INameService service = ac.getBean("nameService2",INameService.class); service.method(); } }
测试结果:
咱们可使用 p 命名空间和 c 命名空间,进行注入
<?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:p="http://www.springframework.org/schema/p" xmlns:c="http://www.springframework.org/schema/c" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!--p命名空间注入,能够直接注入属性的值--> <bean id="user" class="yh.pojo.User" p:name="无问西东" p:age="18"/> <!--c命名空间注入,能够直接注入构造器的值--> <bean id="user2" class="yh.pojo.User" c:name="无问西东" c:age="18"/> </beans>
p 和 c 命名空间容许 bean 元素经过属性(而不是嵌套的子元素)来描述注入的属性值。可是不能直接使用,须要导入 XML 约束:
xmlns:p="http://www.springframework.org/schema/p" xmlns:c="http://www.springframework.org/schema/c"
结构:
Account 类:
public class Account { private int id; private String name; private float money; //标准JavaBean,剩余代码略... }
dao 接口:
public interface IAccountDao { /** * 查询全部 * @return */ List<Account> findAccounts(); /** * 查询一个 * @param account * @return */ Account findAccountById(Integer account); /** * 保存 * @param account */ void saveAccount(Account account); /** * 更新 * @param account */ void updateAccount(Account account); /** * 删除 * @param id */ void deleteAccountById(Integer id); }
dao 接口实现:
public class AccountDaoImpl implements IAccountDao { private QueryRunner runner; public void setRunner(QueryRunner runner) { this.runner = runner; } public List<Account> findAccounts() { try { return runner.query("select * from account",new BeanListHandler<Account>(Account.class)); } catch (Exception e) { throw new RuntimeException(e); } } public Account findAccountById(Integer account) { try { return runner.query("select * from account where id=?",new BeanHandler<Account>(Account.class),account); } catch (Exception e) { throw new RuntimeException(e); } } public void saveAccount(Account account) { try { runner.update("insert into account(name,money) values(?,?)",account.getName(),account.getMoney()); } catch (Exception e) { throw new RuntimeException(e); } } public void updateAccount(Account account) { try { runner.update("update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId()); } catch (Exception e) { throw new RuntimeException(e); } } public void deleteAccountById(Integer id) { try { runner.update("delete from account where id=?",id); } catch (Exception e) { throw new RuntimeException(e); } } }
service 层:
public interface IAccountService { /** * 查询全部 * @return */ List<Account> findAccounts(); /** * 查询一个 * @param account * @return */ Account findAccountById(Integer account); /** * 保存 * @param account */ void saveAccount(Account account); /** * 更新 * @param account */ void updateAccount(Account account); /** * 删除 * @param id */ void deleteAccountById(Integer id); }
service 接口实现:
public class AccountServiceImpl implements IAccountService { private IAccountDao accountDao; /** * set注入 * @param accountDao */ public void setAccountDao(IAccountDao accountDao) { this.accountDao = accountDao; } /** * 获取全部帐户信息 * @return */ public List<Account> findAccounts() { return accountDao.findAccounts(); } public Account findAccountById(Integer account) { return accountDao.findAccountById(account); } public void saveAccount(Account account) { accountDao.saveAccount(account); } public void updateAccount(Account account) { accountDao.updateAccount(account); } public void deleteAccountById(Integer id) { accountDao.deleteAccountById(id); } }
Spring 上下文配置:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!--配置service--> <bean id="accountService" class="yh.service.impl.AccountServiceImpl"> <property name="accountDao" ref="accountDao"/> </bean> <!--配置dao--> <bean id="accountDao" class="yh.dao.impl.AccountDaoImpl"> <property name="runner" ref="dbutils"/> </bean> <!--配置dbutils 避免多线程干扰,设此bean设为多例--> <bean id="dbutils" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"> <constructor-arg name="ds" ref="dateScore"/> </bean> <!--配置数据源--> <bean id="dateScore" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <!--链接数据库的基本信息--> <property name="driverClass" value="com.mysql.jdbc.Driver"/> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?SSl=true&useUnicode=true&characterEncoding=utf8"/> <property name="user" value="root"/> <property name="password" value="root"/> </bean> </beans>
测试代码:
public class Mytest { @Test public void testFindAll(){ //获取容器 ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); //获取业务层对象 IAccountService service = context.getBean("accountService", IAccountService.class); //调用方法 List<Account> accounts = service.findAccounts(); for (Account account : accounts) { System.out.println(account.toString()); } } @Test public void testFindOne(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); IAccountService service = context.getBean("accountService", IAccountService.class); Account account = service.findAccountById(2); System.out.println(account.toString()); } @Test public void testSave(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); IAccountService service = context.getBean("accountService", IAccountService.class); service.saveAccount(new Account(5,"ddd",999)); } @Test public void testUpdate(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); IAccountService service = context.getBean("accountService", IAccountService.class); service.updateAccount(new Account(2,"bbb2",999)); } @Test public void testDelete(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); IAccountService service = context.getBean("accountService", IAccountService.class); service.deleteAccountById(2); } }
从 JDK 5.0 开始,java 增长了对元数据(MetaData)的支持,也就是 Annotation(注解)
注解是代码里的特殊标记,能够在编译、类加载、运行时被读取,并执行相应的处理,经过使用注解,咱们能够在不改变原有逻辑的状况下,在源文件中嵌入一些补充信息。代码分析工具、开发工具和部署工具能够经过这些补充信息进行验证或进行部署。
注解能够像修饰符同样被使用,用来修饰包、类、构造器、方法、成员变量、参数、局部变量的声明,这些信息被保存在 Annation 的 name=value 对中。
框架 = 注解 + 反射 + 设计模式
自定义注解
使用 @interface 关键字如:public @interface testAnnotation
,自定义注解自动继承了 java.lang.annotation.Annotation
接口;
注解的成员变量在定义时以无参方法的形式来声明(如:String[] value()
),其方法名和返回值定义了该成员变量的名字和类型,此为配置参数,类型只能是八种基本数据类型、String、Class、enum、Annotation 这几个类型的数组(有多个 value 值);
能够在定义注解的成员变量时使用 default 关键字,为其指定初始值。若是只有一个参数成员,建议设置参数名为 value
若是定义的注解有配置参数,那么使用时必须指定参数值,除非它有默认值。格式:参数名 = 参数值,若是只有一个参数成员,且名称为 value,能够省略 value = 参数值,直接写参数值便可;
没有成员定义的注解称为标记(如:@Override)包含成员变量的注解称为元数据注解
注意:自定义注解必须配上注解的信息处理流程(使用反射)才有意义。
JDK 中的元注解
元注解:对现有注解进行解释说明的注解。
jdk 提供的 4 中元注解:
@Retention:用于修饰一个 Annotation 定义,指定其生命周期,包含一个 RetentionPolicy 类型的成员变量,使用时需指定 value 的值:
RetentionPolicy.SOURCE:在源文件中有效(即源文件保留),编译器直接丢弃这种策略的注释;
RetentionPolicy.CLASS:在 class 文件中有效(即 class 保留),当运行 Java 程序时,JVM 不会保留注释。这是默认值;
RetentionPolicy.RUNTIME:在运行时有效(即运行时保留),当运行 Java 程序时,JVM 会保留注释。程序能够经过反射获取该注释;
扩展:元数据,是指对数据进行修饰的数据。如:在
String name = "YunHe";
中"YunHe"
为数据,而String name =
就为元数据
配置注解与配置 xml 文件要实现的功能是同样的,都是要下降程序间的耦合,只是配置的形式不同 。
与 xml 配置对应,可将注解简单分为:
用于建立对象的:
至关于:<bean id="" class=""> @Component: 做用:用于把当前类对象存入 spring 容器中 属性: value:用于指定 bean 的 id。默认值为当前类名首字母小写 @Controller:通常用在表现层 @Service:通常用在业务层 @Repository:通常用在持久层 以上三个注解做用和属性与 @Component 同样(父子关系),是 spring 框架提供明确的三层使用注解, 使咱们的三层对象更加清晰
用于注入数据的:
至关于:<property name="" ref=""> / <property name="" value=""> @Autowired 做用:自动按照类型注入(自动装配)。当使用注解注入属性时,set方法能够省略。自动将spring容器中 的 bean 注入到类型匹配的带有此注解的属性。当有多个类型匹配时,配合@Qualifier指定要注入的bean @Qualifier 做用:在自动按照类型注入的基础之上,再按照bean的id注入(解决自动注入存在多个同类型的 bean 所产生的歧义问题),它在给字段注入时不能独立使用,必须和 @Autowire 一块儿使用;可是给方法参数注 入时,能够独立使用(指定形参所要接收的bean的id名)。 属性: value:指定bean的id @Resource 做用:直接按照bean的id注入,它也只能注入其余bean类型 属性: name:指定bean的id @Value 做用:注入基本数据类型和 String 类型数据 属性: value:用于指定值
用于改变做用范围的:
至关于:<bean id="" class="" scope=""> @Scope 做用:指定 bean 的做用范围 属性: value:指定范围的值 取值:singleton/prototype/request/session/globalsession
声明周期相关:
至关于:<bean id="" class="" init-method="" destroy-method=""> @PostConstruct 做用:用于指定初始化方法 @PreDestroy 做用:用于指定销毁方法
自动按照类型注入示意图:
注意:spring 识别 bean 的范围时需经过 xml 配置设置 spring 建立容器时要扫描的包。
这里就贴上修改的部分代码(改动过小了)
dao 实现类:(两个注解)
@Repository("accountDao") public class AccountDaoImpl implements IAccountDao { @Autowired private QueryRunner runner; //... }
service 实现类:(两个注解)
@Service("accountService") public class AccountServiceImpl implements IAccountService { @Autowired private IAccountDao accountDao; //... }
使用注解的 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:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 告知 spring 建立容器时要扫描的包--> <context:component-scan base-package="yh"/> <bean id="dbutils" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"> <constructor-arg name="ds" ref="dateScore"/> </bean> <bean id="dateScore" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="com.mysql.jdbc.Driver"/> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?SSl=true&useUnicode=true&characterEncoding=utf8"/> <property name="user" value="root"/> <property name="password" value="root"/> </bean> </beans>
这样就完成配置了,可是 DBUtils 及 c3p0 的配置能不能也转换成注解形式呢?答案是固然能够,这就要新引入一个配置类的概念。
在项目中新建一个结构以下:
并新建配置类以下:
/** * 该类是一个配置类它的做用和bean.xml是同样的 * spring中的新注解: * * Configuration 注解 * 做用:指定当前类是一个配置类 * ComponentScan 注解 * 做用:指定Spring在建立容器时扫描配置的包 * 属性:value,指定要包 * 效果同xml配置中的<context:component-scan base-package=""/>同样 * Bean 注解 * 做用:用于把当前方法的返回值做为bean对象存入spring的IoC容器中 * 属性: * name:用于指定bean的id,当不写时,默认为方法名 * 细节: * 当咱们使用注解配置方法时,若是方法有参数,spring框架会去容器中查找有没有可用的bean对象 * 查找的方式和Autowired注解的做用同样 * Import 注解 * 做用:用于导入其余配置的类 * 属性: * value:用于指定其余配置类的字节码 * 当咱们使用Import的注解以后,使用Import注解的类就是父配置类,而导入的都是子配置类 * @author YH * @create 2020-05-22 21:36 */ @Configuration @ComponentScan("yh") @Import(JDBCConfig.class) public class SpringConfiguration { /** * 用于建立一个QueryRunner对象 * 细节:默认获取的是单例的,但runner对象咱们须要多例的,因此可加上scope * @param dataSource * @return */ @Bean(name="runner") @Scope("prototype") public QueryRunner createQueryRunner(DataSource dataSource){ return new QueryRunner(dataSource); } }
配置jdbc的配置类:
/** * 注解方式获取jdbc链接的配置类 * PropertySource 注解 * 做用:指定properties文件的位置 * 属性: * value 注解:指定文件的名称和路径(properties文件的key) * 关键字:classpath,即是类路径下 * @author YH * @create 2020-05-23 9:47 */ @PropertySource("classpath:data.properties") //引入外部的properties属性文件 public class JDBCConfig { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; /** * 建立数据源 * @return */ @Bean(name="dataSource") public DataSource createDataSource(){ try { ComboPooledDataSource ds = new ComboPooledDataSource(); ds.setDriverClass(driver); ds.setJdbcUrl(url); ds.setUser(username); ds.setPassword(password); return ds; } catch (Exception e) { throw new RuntimeException(e); } } }
properties 配置的数据库参数:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=utf8 #不要用username做为key(获取到了我程序的做者标记了) jdbc.username=root jdbc.password=root
如上就是纯注解的配置形式,配置类的做用同 bean.xml
同样,因此相对的,也会有对应 xml 中配置的注解(每每能见名知意)。测试类原来是经过加载 xml 的方式也要变动为 Annotation 的,以下:
@Test public void testFindAll(){ // ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); //改成注解工厂 ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class); IAccountService service = context.getBean("accountService", IAccountService.class); List<Account> accounts = service.findAccounts(); for (Account account : accounts) { System.out.println(account.toString()); } }
纯注解的形式配置的工做量也不小,因此合理与 xml 搭配使用方能体现效率。
可使用在类或属性上以及方法形参前,用于解决有多个同类型 bean 的自动注入问题,经过 @Qualifier()
指定 bean id 来确认哪一个 bean 才是咱们须要注入的(设置的 value 值须要与目标 bean id 名相同)
@Primary
注解也用于解决自动注入时多个相同类型 bean 的问题,它定义了首选项,除非另有说明,不然将优先使用与@Primary
关联的 bean。
在上面的测试代码中都会有如下两行代码:
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); IAccountService service = context.getBean("accountService", IAccountService.class);
这两行代码的做用是获取容器,若是不写会提示空指针,因此不能轻易去掉。
针对问题,咱们须要 Spring 自动帮咱们建立容器,咱们就无序手动建立了,上面的问题也能解决。
首先 Junit 实现底层是集成了 main 方法,它没法知晓咱们是否使用了 Spring 框架,天然无可能帮咱们建立容器,不过 junit 给我吗暴露了一个注解,可让咱们替换掉它的运行器。
因此咱们须要依赖 spring 框架,由于它提供了一个运行器,能够读取配置文件(或注解)来建立容器。咱们只须要告诉它配置文件的位置便可。
添加 junit 必备的 jar 包依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.2.6.RELEASE</version> </dependency>
对于 Spring 5,需用 4.12 及以上 Junit jar 包
使用 @RunWith
注解替换原有运行器并使用@Autowired
给测试类中的变量注入数据
/** * RunWith:替换原有运行器 * @author YH * @create 2020-05-22 21:20 */ @RunWith(SpringJUnit4ClassRunner.class) public class MyTest { //由spring自动注入业务层对象 @Autowired IAccountService service; @Test public void testFindAll(){ List<Account> accounts = service.findAccounts(); for (Account account : accounts) { System.out.println(account.toString()); } } }
使用 @ContextConfiguration
指定 Spring 配置文件的位置
/** * RunWith:替换原有运行器 * ContextConfiguration * 属性: * location属性:用于指定配置文件的位置,若是是类路径下,须要用classpath:表名 * classes属性:用于指定注解的类,当不使用xml配置时,须要用此属性指定注解类的位置 * @author YH * @create 2020-05-22 21:20 */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes={SpringConfiguration.class}) //{}表示支持设置多个配置类 public class MyTest { @Autowired ApplicationContext context; @Autowired IAccountService service; @Test public void testFindAll(){ List<Account> accounts = service.findAccounts(); for (Account account : accounts) { System.out.println(account.toString()); } } }
其中@Autowired
会给测试类中的变量注入数据
首先,测试类配置到 xml 中确定是能够实现的,但为何不这样作?
缘由:
当咱们在 xml 中配置一个 bean ,spring 加载配置文件建立容器时,就会建立对象。
而测试仅仅起测试做用,在项目中它并不参与程序逻辑,也不会解决需求上的问题,因此建立完了,并无使用,那么存在容器中就会形成资源的浪费。
因此,基于以上两点,咱们不该该把测试类配置到 xml 中。
AOP(Aspect Oriented Programming)面向切面编程,经过预编译的方式和运行期动态代理实现程序功能的统一维护的一种技术。将程序中重复的功能代码抽象出来,在须要执行的时候使用动态代理在不修改源码的基础上,对咱们已有的方法进行加强。
从几个知识面做为学习 AOP 的突破口
修改上面的 CRUD 案例,首先原案例代码中的事务由 connection 对象的 setAutocommit(true)
而被自动控制。此方式控制事务,一次只执行一条 sql 语句,没有问题,但执行多条 sql 就没法实现功能。缘由是 sql 执行一次会获取一次数据库链接,统一 sql 语句的执行结果会被缓存,后面执行会直接读取缓存;而多条 sql 执行就须要各自或许链接并执行,持久层方法都是独立事务的,不符合事务的一致性,下面来探讨一下。
持久层代码:
package yh.dao.impl; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.BeanListHandler; import yh.dao.IAccountDao; import yh.pojo.Account; import java.sql.SQLException; import java.util.List; /** * @author YH * @create 2020-07-01 8:59 */ public class AccountDaoImpl implements IAccountDao { private QueryRunner runner; public void setRunner(QueryRunner runner){ this.runner = runner; } @Override public Account findName(String name) { try { List<Account> accounts = runner.query("select * from mybatis.account where name=?", new BeanListHandler<Account>(Account.class), name); if(accounts == null || accounts.isEmpty()){ return null; } if (accounts.size() > 1){ throw new RuntimeException("结果集不惟一,数据有问题"); } return accounts.get(0); } catch (Exception e) { e.printStackTrace(); } return null; } @Override public void update(Account account) { try { runner.update("update mybatis.account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId()); } catch (SQLException e) { e.printStackTrace(); } } }
业务层代码:
package yh.service.impl; import yh.dao.impl.AccountDaoImpl; import yh.pojo.Account; import yh.service.IAccountService; /** * @author YH * @create 2020-07-01 8:53 */ public class AccountServiceImpl implements IAccountService { private AccountDaoImpl accountDao; public void setAccountDao(AccountDaoImpl accountDao){ this.accountDao = accountDao; } @Override public void transfer(String sourceName, String targetName, Float money) { //根据帐户信息获取帐户对象 Account source = accountDao.findName(sourceName); Account target = accountDao.findName(targetName); //转出帐户减钱,转入帐户加钱 source.setMoney(source.getMoney() - money); target.setMoney(target.getMoney() + money); //提交更新 accountDao.update(source); int i = 1/0;//模拟程序出错 accountDao.update(target); } }
理想状况下,程序正常运行,转帐结果正确
一旦出错,前面执行后面的执行中断,即转出帐户减钱了,而收款帐户余额未增长,且事务没法回滚(由于它们有各自的事务)
下面就是新增在业务层的转帐方法,每一个执行方法都获取一次链接,都是独立的事务,一旦中途出现中断,就没法实现事务的回滚。
解决办法:
使用 ThreadLocal 对象把 Connection 和当前线程绑定,从而使一个线程中只有一个能控制事务的对象,原来的事务是在持久层,现需将事务应用在业务层。
持久层代码:
public class AccountDaoImpl implements IAccountDao { private QueryRunner runner; private ConnectionUtils connectionUtils; public void setRunner(QueryRunner runner){ this.runner = runner; } public void setConnectionUtils (ConnectionUtils connectionUtils){ this.connectionUtils = connectionUtils; } @Override public Account findName(String name) { try { //使用与线程绑定的链接 List<Account> accounts = runner.query(connectionUtils.getThreadConnection(),"select * from mybatis.account where name=?", new BeanListHandler<Account>(Account.class), name); if(accounts == null || accounts.isEmpty()){ return null; } if (accounts.size() > 1){ throw new RuntimeException("结果集不惟一,数据有问题"); } return accounts.get(0); } catch (Exception e) { e.printStackTrace(); } return null; } @Override public void update(Account account) { try { //使用与线程绑定的链接 runner.update(connectionUtils.getThreadConnection(),"update mybatis.account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId()); } catch (SQLException e) { e.printStackTrace(); } } }
业务层代码:
public class AccountServiceImpl implements IAccountService { private AccountDaoImpl accountDao; private TransactionManager transactionManager; public void setAccountDao(AccountDaoImpl accountDao){ this.accountDao = accountDao; } public void setTransactionManager(TransactionManager transactionManager){ this.transactionManager = transactionManager; } @Override public void transfer(String sourceName, String targetName, Float money) { //根据帐户信息获取帐户对象 Account source = accountDao.findName(sourceName); Account target = accountDao.findName(targetName); try {//开启事务 transactionManager.beginTransaction(); //转出帐户减钱,转入帐户加钱 source.setMoney(source.getMoney() - money); target.setMoney(target.getMoney() + money); //提交更新 accountDao.update(source); // int i = 1 / 0; accountDao.update(target); //提交事务 transactionManager.commit(); } catch (Exception e){ transactionManager.rollback(); e.printStackTrace(); }finally { //释放线程并解绑链接 transactionManager.release(); } } }
链接工具类代码:
package yh.utils; import javax.sql.DataSource; import java.sql.Connection; /** * 链接的工具类 * 从数据源中获取链接,并实现和线程的绑定 * @author YH * @create 2020-07-02 9:30 */ public class ConnectionUtils { private ThreadLocal<Connection> tl = new ThreadLocal<>(); private DataSource dataSource; public void setDataSource(DataSource dataSource){ this.dataSource = dataSource; } /** * 获取当前线程上的链接 * @return */ public Connection getThreadConnection(){ try { //1.先从Threadlocal上获取链接 Connection conn = tl.get(); //2.判断当前线程上是否有链接 if (conn == null) { //3.若是ThreadLocal上没有链接,那么从数据源获取链接并存入ThreadLocal conn = dataSource.getConnection(); tl.set(conn); } //4.返回当前线程链接 return conn; } catch(Exception e){ throw new RuntimeException(e); } } /** * 直接删除链接,让线程与链接解绑 */ public void removeConnection(){ tl.remove(); } }
事务管理工具类的代码:
package yh.utils; import java.sql.SQLException; /** * 事务管理相关的工具类 * 负责开启事务、提交事务、回滚事务、释放链接 * @author YH * @create 2020-07-02 9:57 */ public class TransactionManager { private ConnectionUtils connectionUtils; public void setConnectionUtils(ConnectionUtils connectionUtils){ this.connectionUtils = connectionUtils; } /** * 开启事务 */ public void beginTransaction(){ try { connectionUtils.getThreadConnection().setAutoCommit(false); } catch (Exception e) { e.printStackTrace(); } } /** * 提交事务 */ public void commit(){ try { connectionUtils.getThreadConnection().commit(); } catch (Exception e) { e.printStackTrace(); } } /** * 回滚事务 */ public void rollback(){ try { connectionUtils.getThreadConnection().rollback(); } catch (Exception e) { e.printStackTrace(); } } /** * 释放资源 并 解绑线程和链接 * 默认状况下线程回收到线程池其上依旧绑定了已经会受到链接池的链接, * 即链接时关闭的,再次启动线程时,能直接获取到链接,但这个链接显然 * 没法使用,顾需在线程关闭后让其与链接解绑 */ public void release(){ try { //回收到线程池 connectionUtils.getThreadConnection().close(); connectionUtils.removeConnection(); } catch (Exception e) { e.printStackTrace(); } } }
线程回收到线程池,而线程绑定的链接也会回到链接池,若是该线程在此运行,那么此时获取它的链接是能够获取到的,但这个链接已经关闭回到链接池中,这样显然不行。因此在线程关闭前还须要作线程解绑操做。
解决事务问题后,发现我只是增长一个功能,就要对原有代码进行这么大的改动,并且业务层和持久层对两个工具类方法有很强的依赖,显然这就是问题,有什么解决办法呢?
场景:有生产者(被代理类)与经销商(代理方)。生产者能够售出产品,经销商也能够销售产品,但由经销商销售的产品经销商从中收取百分之20的金额。即要对被代理的工厂增长代理的代码,使得代理经销商能收益。以下:
基于接口的代理
共同实现的接口:
/** * 定义一个代理类和被代理类共同要实现的接口 * 从而实现基于接口的代理 * @author YH * @create 2020-07-02 14:37 */ public interface IProducer { /** * 销售产品 * @param money */ public void saleProduct(float money); /** * 产品售后 * @param money */ public void afterProduct(float money); }
生产者(被代理对象):
public class Producer implements IProducer { public void saleProduct(float money){ System.out.println("销售产品,并拿到钱:" + money); } public void afterProduct(float money){ System.out.println("产品售后,并拿到钱:" + money); } }
模拟消费(代理对象):
public class Client { public static void main(String[] args){ //被代理对象(被内部类方法,须要声明为不可变的) final Producer producer = new Producer(); /** * 动态代理: * 特色:字节码随意调用,随用随加载 * 做用:不修改源码的基础上对方法加强 * 分类: * 基于接口的动态代理 * 基于子类的动态代理 * 基于接口的动态代理: * 涉及的类:Proxy * 提供者:官方JDK * 如何建立代理对象: * 使用Proxy类的newProxyInstance方法 * 建立代理对象的要求: * 被代理类至少实现一个接口,若是没有则不能使用 * newProxyInstance方法的参数: * lassLoader:类加载器。用于加载代理对象字节码的,和被代理对象使用相同的类加载器。固定写法 * Class<?>[]:字节码数组。传递被代理对象实现的接口信息,使得代理对象和被代理对象具备相同的方法。固定写法 * InvocationHandler:用于提供加强的代码。用于说明如何代理(通常写一些接口的实现类,一般是匿名内部类) */ IProducer proxyProducer = (IProducer)Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler(){ /** * 执行被代理对象的任何方法都会通过这里 * @param o 代理对象的引用 * @param method 当前执行的方法 * @param objects 当前执行方法所需的参数 * @return 和被代理对象有相同的返回值 * @throws Throwable */ public Object invoke(Object o, Method method, Object[] objects) throws Throwable { //提供加强的代码 //1.获取方法执行的参数 Float money = (Float)objects[0]; //2.判断当前方法是否是销售 if("saleProduct".equals(method.getName())) { return method.invoke(producer, money * 0.8f); } return null; } }); //测试调用被代理类的方法 proxyProducer.saleProduct(10000f); } }
最终实现了,在经销商处销售的商品工厂只能拿到8000。
基于接口的代理方式有一个缺陷就是必需要实现一个接口,没法实现接口要怎么办呢,那就是实现动态代理的另外一种方式:基于子类的动态代理
这种方式须要有第三方 jar 包: cglib
的支持
增长 pom.xml 文件依赖:
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>
模拟消费(代理类)
/** * 基于子类的动态代理 * @author YH * @create 2020-07-02 16:53 */ public class Client { public static void main(String[] args){ final Producer producer = new Producer(); /** * 基于接口的动态代理: * 涉及的类:Enhancer * 提供者:第三方cglib * 如何建立代理对象: * 使用Enhancer类的create()方法 * 建立代理对象的要求: * 被代理类不能是最终类 * create()方法的参数: * class:字节码。用于指定被代理对象的字节码 * Callback:用于提供加强的代码。即如何代理,通常用该接口的子类接口的实现类 MethodInterceptor */ Producer cglibProduct = (Producer ) Enhancer.create(producer.getClass(), new MethodInterceptor() { public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { //提供加强的代码 //1.获取方法执行的参数 Float money = (Float)objects[0]; //2.判断当前方法是否是销售 if("saleProduct".equals(method.getName())) { return method.invoke(producer, money * 0.8f); } return null; } }); //测试调用方法 cglibProduct.saleProduct(10000f); } }
生产者(被代理类)
public class Producer implements IProducer { public void saleProduct(float money){ System.out.println("销售产品,并拿到钱:" + money); } public void afterProduct(float money){ System.out.println("产品售后,并拿到钱:" + money); } }
结果相同:
持久层代码不变
业务层代码(被代理对象):
public class AccountServiceImpl implements IAccountService { private AccountDaoImpl accountDao; public void setAccountDao(AccountDaoImpl accountDao){ this.accountDao = accountDao; } @Override public void transfer(String sourceName, String targetName, Float money) { try { //根据帐户信息获取帐户对象 Account source = accountDao.findName(sourceName); Account target = accountDao.findName(targetName); //转出帐户减钱,转入帐户加钱 source.setMoney(source.getMoney() - money); target.setMoney(target.getMoney() + money); //提交更新 accountDao.update(source); // int i = 1 / 0; accountDao.update(target); } catch (Exception e){ //改成运行时异常,将异常抛给调用者(代理类)来处理,不然调用处后的回滚操做没法执行 // (固然被代理类中也能够不捕获异常,代理类捕获) throw new RuntimeException(e); } } }
代理工厂:
package yh.factory; import yh.service.IAccountService; import yh.utils.TransactionManager; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; /** * 用于建立Service的代理对象工厂 * @author YH * @create 2020-07-03 8:55 */ public class BeanFactory { private IAccountService accountService; private TransactionManager transactionManager; public void setAccountService(IAccountService accountService){ this.accountService = accountService; } public void setTransactionManager(TransactionManager transactionManager){ this.transactionManager = transactionManager; } public IAccountService getAccountService(){ return (IAccountService)Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() { /** * 获取AccountService的代理对象 * @param proxy * @param method * @param args * @return * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object returnValue = null; try { //1.开启事务 transactionManager.beginTransaction(); //2.执行操做 returnValue = method.invoke(accountService, args); //3.提交事务 transactionManager.commit(); //4.返回被代理对象 return returnValue; } catch(Exception e){ //5.回滚 transactionManager.rollback(); throw new RuntimeException(e); } finally { //6.释放资源 transactionManager.release(); } } }); } }
被代理对象实现了一个接口,顾使用了基于接口的动态代理方式。
至此,不管业务层中有多少个方法,都会由代理类为其增长事务管理,而不是每一个单独都要设置,在不增长业务类代码的状况下实现了功能的加强!
使用 AOP 就能够经过配置的方式实现上面案例的功能,这也是经过案例引入 AOP 的缘由。
AOP 相关术语
Joinpoint(链接点):
指那些被拦截到的点。在 Spring 中,这些点指的是方法,由于 Spring 只支持方法类型的链接点
Pointcut(切点):
切点的定义会匹配通知所要织入的一个或多个链接点,即定义拦截规则(一般使用明确的类和方法名称,可配合正则表达式使用)
Advice(通知/加强):
拦截到 Joinpoint 以后要作的事情(新增的功能)
通知的类型:前置通知、后置通知、异常通知、最终通知、环绕通知。对应到案例中以下:
Introduction(引入):
一种特殊的通知。在不修改类代码的前提下,Introduction 能够在运行期为类动态地添加一些方法或属性
Target(目标对象):
代理的目标对象
Weaving(织入):
把加强应用到目标对象并建立新的代理对象的过程。
Spring 采用动态代理织入(运行期);AspectJ 采用编译器织入和类装载期织入。
Proxy(代理):
一个类被 AOP 织入加强后,就会产生一个结果代理类
Aspect(切面):
切点和通知的结合
小结:
通知包含了须要应用于多个对象的横切行为;链接点是程序执行过程当中可以应用通知的全部点;切点定义了通知被应用的具体位置,即哪些链接点(方法),且定义了哪些链接点会获得通知。
注意
开发阶段(咱们作的)
运行阶段(Spring 框架作的)
Spring 会根据目标类是否实现了接口来决定采用哪一种动态代理方式。
动态代理中用到的
invoke()
方法有拦截功能。
添加依赖
<!--用于解析Spring表达式--> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.5</version> </dependency>
bean.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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- ioc配置,将Service配置进来--> <bean id="accountService" class="yh.service.impl.AccountServiceImpl"/> <!-- aop配置 1.把通知bean也交给spring管理 2.使用aop:config标签标示开始aop配置 3.使用aop:aspect变迁配置切面 id:给切面定义惟一的表示 ref:指定切面的通知类bean的id 4.内部标签中配置通知类型(前置通知为例) 使用aop:before表示配置前置通知 method:指定通知列中哪一个方法用于通知 pointcut:指定切入点表达式,表示对业务层中哪些方法进行加强 切入点表达式写法: 关键字:execution(表达式) 标准写法 execution(public void 全类名.方法名(参数列表)) 其中权限修饰符能够省略,返回值类型、全类名、方法名、形参列表均可以用通配符代替 全统配写法:* *..*.*(..) 多个包用 .. 表示一个包及其子包,形参列表.. 表示无参或多参 但实际开发中只会对业务层的实现类方法进行统配,写法:* 业务层包路径.*.*(..) --> <!-- 配置Logger类--> <bean id="logger" class="yh.utils.Logger"/> <!--配置aop--> <aop:config> <!--配置切面--> <aop:aspect id="loggerAdvice" ref="logger"> <!--配置通知类型且定义通知方法和切入点方法的关联--> <aop:before method="printLog" pointcut="execution(public void yh.service.impl.AccountServiceImpl.saveAccount())"/> </aop:aspect> </aop:config> </beans>
注意添加 aop 命名空间和约束
业务层接口
public interface IAccountService { /** * 模拟保存帐户 */ void saveAccount(); /** * 模式更新帐户 * @param i */ void updateAccount(int i); /** * 模拟删除帐户 * @return */ int deleteAccount(); }
业务层实现类
public class AccountServiceImpl implements IAccountService { public void saveAccount() { System.out.println("save account!"); } public void updateAccount(int i) { System.out.println("update account!"); } public int deleteAccount() { System.out.println("delete account!"); return 1; } }
通知类
public class Logger { /** * 输出日志:计划让其在切入点以前执行(即前置通知,在匹配的业务层方法前执行) */ public void printLog(){ System.out.println("输出日志..."); } }
测试
@Test public void aopTest(){ //1.获取容器 ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml"); //2.获取bean IAccountService as = (IAccountService)context.getBean("accountService"); //3.执行方法 as.saveAccount(); //spring表达式所匹配的链接点方法才会被应用通知 as.updateAccount(1); as.deleteAccount(); }
结果:
增长对应的方法,对四种通知进行配置:
<aop:config> <!--提取共用的表达式,供通知引用--> <aop:pointcut id="ref" expression="execution(* yh.service.impl.*.*(..))"/> <!--配置切面--> <aop:aspect id="loggerAdvice" ref="logger"> <!--前置通知:切入点方法执行前通知--> <aop:before method="beforeAdvice" pointcut-ref="ref"/> <!--后置通知:切入点方法执行后通知--> <aop:after-returning method="afterAdvice" pointcut-ref="ref"/> <!--异常通知:切入点方法抛出异常时通知--> <aop:after-throwing method="exceptionAdvice" pointcut-ref="ref"/> <!--最终通知:不管切入点方法是否正常执行,都会执行--> <aop:after method="finallyAdvice" pointcut-ref="ref"/> </aop:aspect> </aop:config>
使用所编写的逻辑将被通知的目标方法彻底包装起来(相似前面的动态代理对方法的加强),实现了一个方法中同时编写各种通知。
bean.xml中配置环绕通知
<!--配置环绕通知--> <aop:around method="aroundAdvice" pointcut-ref="ref"/>
通知类中定义环绕通知的方法:
/** * 环绕通知 * Spring提供了一个接口:ProceedingJoinPoint,改接口有一个 proceed() 方法,用于明确切入点方法 * 改接口可做为环绕通知方法的参数使用,由Spring建立 * 经过环绕通知咱们能够手动控制被加强方法在通知中执行的位置 */ public Object aroundAdvice(ProceedingJoinPoint pjp){ Object returnValue = null; try { System.out.println("我是前置通知"); //获得执行方法所需的参数 Object[] args = pjp.getArgs(); //执行切入点(业务类)方法 returnValue = pjp.proceed(args); System.out.println("我是后置通知"); } catch (Throwable throwable) { System.out.println("我是异常通知"); throwable.printStackTrace(); } finally { System.out.println("我是最终通知"); } return returnValue; }
相似代理类环绕加强被代理类,但明显更加简便明了,大多数事情被 spring 完成了,咱们能够在被通知方法执行先后定义想要增长的功能,从而实现各种通知,结果以下:
业务类要加上 @Service("accountService")
让 Spring 容器管理并指定标识 id
通知类
/** * 记录日志的工具类,定义通知的共用代码 * @author YH * @create 2020-07-04 7:27 * Component注解,指示Spring容器将建立管理当前类对象 * value:用于指定 bean 的 id。默认值为当前类名首字母小写 * (三层有各自的注解,但功能同样,是Component的子类) * Aspect注解:表示当前类是一个切面 */ @Component("logger") @Aspect public class Logger { /** * 经过注解定义可重用切点表达式,供通注解知引用 */ @Pointcut("execution(* yh.service.impl.*.*(..))") public void spe(){} /** * 前置通知 */ @Before("spe()") public void beforeAdvice(){ System.out.println("前置通知..."); } /** * 后置通知 */ @AfterReturning("spe()") public void afterAdvice(){ System.out.println("后置通知..."); } /** * 异常通知 */ @AfterThrowing("spe()") public void exceptionAdvice(){ System.out.println("异常通知..."); } /** * 最终通知 */ @After("spe()") public void finallyAdvice(){ System.out.println("最终通知..."); } /** * 环绕通知 * Spring提供了一个接口:ProceedingJoinPoint,改接口有一个 proceed() 方法,用于明确切入点方法 * 改接口可做为环绕通知方法的参数使用,由Spring建立 * 经过环绕通知咱们能够手动控制被加强方法在通知中执行的位置 */ @Around("spe()") public Object aroundAdvice(ProceedingJoinPoint pjp){ Object returnValue = null; try { System.out.println("我是前置通知"); //获得执行方法所需的参数 Object[] args = pjp.getArgs(); //执行切入点(业务类)方法 returnValue = pjp.proceed(args); System.out.println("我是后置通知"); } catch (Throwable throwable) { System.out.println("我是异常通知"); throwable.printStackTrace(); } finally { System.out.println("我是最终通知"); } return returnValue; } }
bean.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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 配置Spring建立容器时要扫描的包--> <context:component-scan base-package="yh"/> <!-- 开启注解aop的支持--> <aop:aspectj-autoproxy/> </beans>
使用注解,命名空间和约束都须要设置
纯注解获取 Spring 容器方式与经过 xml 配合不同,以下:
先定义一个 java 配置类:
@Configuration @ComponentScan("yh") //指定扫描的包 @EnableAspectJAutoProxy //开启基于注解AOP的支持 public class SpringConfiguration { }
/** * 测试纯注解配置 */ @Test public void annotationAopTest2(){ ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class); IAccountService service = (IAccountService)context.getBean("accountService"); service.saveAccount(); }
基于注解配置通知时,建议应用于环绕通知。其余通知的顺序可能不是想要的结果(如后置通知在最终通知以前执行)
基于 XML 配置
改动几乎都在 bean.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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!--配置service--> <bean id="accountService" class="yh.service.impl.AccountServiceImpl"> <property name="accountDao" ref="accountDao"/> </bean> <!--配置dao--> <bean id="accountDao" class="yh.dao.impl.AccountDaoImpl"> <property name="runner" ref="runner"/> <property name="connectionUtils" ref="connectionUtils"/> </bean> <!-- 配置QueryRunner--> <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"> <constructor-arg ref="dataSource"/> </bean> <!--配置数据源--> <!-- 读取数据源文件的位置--> <context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <!-- 配置事务管理工具类--> <bean id="transactionManager" class="yh.utils.TransactionManager"> <property name="connectionUtils" ref="connectionUtils"/> </bean> <!-- 配置链接工具类--> <bean id="connectionUtils" class="yh.utils.ConnectionUtils"> <property name="dataSource" ref="dataSource"/> </bean> <!-- AOP配置--> <aop:config> <!--提取共用的表达式,供通知引用--> <aop:pointcut id="pe1" expression="execution(* yh.service.impl.AccountServiceImpl.transfer(..))"/> <!--配置切面--> <aop:aspect id="tr" ref="transactionManager"> <!--肯定通知类型,定义通知方法和切入点方法的关联--> <aop:before method="beginTransaction" pointcut-ref="pe1"/> <aop:after-throwing method="rollback" pointcut-ref="pe1"/> <aop:after-returning method="commit" pointcut-ref="pe1"/> <aop:after method="release" pointcut-ref="pe1"/> </aop:aspect> </aop:config> </beans>
纯注解配置
基于注解配置中,因为 Spring 缘由,最终通知(@After)和后置通知(@AfterReturning)或异常通知(@AfterThrowing)的执行顺序没法控制,因此使用环绕通知:
持久层、业务层等工具列类只用加上组件注解(@Component 注解之类)以及其成员属性的注入注解(@Autowired 注解)便可
SpringConfiguration 配置类:
@Configuration //表名此类为配置类 @EnableAspectJAutoProxy //开启Spring AOP支持 @ComponentScan("yh") //指定spring建立容器要扫描的包 @Import(JdbcConfig.class) //导入子配置类 public class SpringConfiguration { @Bean(name = "runner") //将方法的返回值建立为bean 并存入Spring容器中 public QueryRunner createQueryRunner(DataSource dataSource){//形参可自动注入 return new QueryRunner(dataSource); } }
JDBC配置类:
@PropertySource("classpath:jdbc.properties") //引入外部properties属性文件 public class JdbcConfig { //@Value是@PropertySource的属性注解,用于读取配置文件中的key-value @Value("${driverClassName}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Bean(name="dataSource") public DataSource createDataSource(){ DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName(driver); ds.setUrl(url); ds.setUsername(username); ds.setPassword(password); return ds; } }
通知类:
/** * 事务管理相关的工具类 * 负责开启事务、提交事务、回滚事务、释放链接 * @author YH * @create 2020-07-02 9:57 */ @Component("txManager") @Aspect //指示此类是切面 public class TransactionManager { @Autowired private ConnectionUtils connectionUtils; @Pointcut("execution(* yh.service.impl.*.*(..))") public void spe(){} /** * 开启事务 */ public void beginTransaction(){ try { connectionUtils.getThreadConnection().setAutoCommit(false); System.out.println("开启事务"); } catch (Exception e) { e.printStackTrace(); } } /** * 提交事务 */ public void commit(){ try { connectionUtils.getThreadConnection().commit(); System.out.println("提交事务"); } catch (Exception e) { e.printStackTrace(); } } /** * 回滚事务 */ public void rollback(){ try { connectionUtils.getThreadConnection().rollback(); System.out.println("回滚事务"); } catch (Exception e) { e.printStackTrace(); } } /** * 释放资源 并 解绑线程和链接 * 默认状况下线程回收到线程池其上依旧绑定了已经会受到链接池的链接, * 即链接时关闭的,再次启动线程时,能直接获取到链接,但这个链接显然 * 没法使用,顾需在线程关闭后让其与链接解绑 */ public void release(){ try { //回收到线程池 connectionUtils.getThreadConnection().close(); connectionUtils.removeConnection(); System.out.println("关闭资源"); } catch (Exception e) { e.printStackTrace(); } } /** * 环绕通知,配置注解通知建议只使用环绕通知 */ @Around("spe()") public Object aroundAdvice(ProceedingJoinPoint pjp){ Object returnValue = null; try { this.beginTransaction(); Object[] args = pjp.getArgs(); returnValue = pjp.proceed(args); this.commit(); } catch (Throwable throwable) { this.rollback(); throwable.printStackTrace(); } finally { this.release(); } return returnValue; } }
properties属性文件:
driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/mybatis?ssl=true&useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=root
xml 引入外部属性文件的两种方式:
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="location" value="classpath:jdbc.properties"/> </bean><context:property-placeholder location="classpath:jdbc.properties"/>
Spring 框架提供了不少的操做模板类
关键依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.2.6.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-tx --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.2.6.RELEASE</version> </dependency>
基本配置
<?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:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!-- 配置JdbcTemplate--> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <!-- 配置数据源--> <!--引入外部属性文件--> <context:property-placeholder location="data.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> </beans>
data.properties
driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/mybatis?ssl=true&useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=root
import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import yh.domain.Account; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; /** * JdbcTemplate的简单用法 * @author YH * @create 2020-07-05 17:25 */ public class JdbcTemplate1 { public static void main(String[] args){ ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml"); JdbcTemplate jt = (JdbcTemplate)context.getBean("jdbcTemplate"); //保存 jt.update("insert into mybatis.account(name,money) values(?,?)","zzz",2000); //修改 jt.update("update mybatis.account set money=money+? where name=?",99,"aaa"); //删除 jt.update("delete from mybatis.account where id=?",7); //查询全部 List<Account> accountList = jt.query("select * from mybatis.account", new AccountRowMapper()); for(Account a : accountList){ System.out.println(a); } //查询一个 List<Account> accountList = jt.query("select * from mybatis.account where id=?", new AccountRowMapper(),3); System.out.println(accountList.isEmpty() ? "没有结果" : accountList.get(0)); //查询返回一行一列,经常使用于分页中获取总记录数 Integer total = jt.queryForObject("select count(*) from mybatis.account where id>?", Integer.class, 1); System.out.println(total); } /** * 处理查询结果集的封装 */ static class AccountRowMapper implements RowMapper<Account> { /** * @param resultSet 查询sql返回的结果集 * @param i 所查询表的行数 */ @Override public Account mapRow(ResultSet resultSet, int i) throws SQLException { Account account = new Account(); account.setId(resultSet.getInt("id")); account.setName(resultSet.getString("name")); account.setMoney(resultSet.getFloat("money")); return account; } }
dao 中使用 JdbcTemplate 有两种方式,普通作法,在 dao 中增长一个 JdbcTemplate 引用属性,交由 spring 注入,然后进行 update()、query() 调用。但当有多个 dao 时,每一个 dao 内都要重复定义代码:private JdbcTemplate jdbcTemplate;
第二种方式:使用 Spring 提供的 JdbcDaoSupport 抽象类,其内部封装了 JdbcTemplate 属性,只需给予一个 DataSource 给它就能够获取 JdbcTemplate 对象,让咱们的 dao 继承它就能够获取属性以及注入 DataSource:
、
持久层接口:
public interface IAccountDao { /** * 经过Id查帐户 * @param id * @return */ public Account findAccountById(Integer id); /** * 经过Id查帐户 * @param name * @return */ public Account findAccountByName(String name); /** * 修改帐户 * @param account */ public void updateAccount(Account account); }
持久层实现类:
public class AccountDaoImpl extends JdbcDaoSupport implements IAccountDao { /*注:继承父类所得到的属性可进行注入,数据源就是经过此特性注入(见bean.xml)*/ @Override public Account findAccountById(Integer id) { JdbcTemplate jt = getJdbcTemplate(); List<Account> list = jt.query("select * from mybatis.account where id=?", new AccountRowMapper(), id); return list.isEmpty() ? null : list.get(0); } @Override public Account findAccountByName(String name) { JdbcTemplate jt = getJdbcTemplate(); List<Account> list = jt.query("select * from mybatis.account where name=?", new AccountRowMapper(), name); if(list.size() > 1){ throw new RuntimeException("结果集不惟一,查询的对象有多个"); } return list.isEmpty() ? null : list.get(0); } @Override public void updateAccount(Account account) { JdbcTemplate jt = getJdbcTemplate(); jt.update("update mybatis.account set name=?,money=? where id=?", account.getName(),account.getMoney(),account.getId()); }
封装查询结果集的工具类:
public class AccountRowMapper implements RowMapper<Account> { @Override public Account mapRow(ResultSet resultSet, int i) throws SQLException { Account account = new Account(); account.setId(resultSet.getInt("id")); account.setName(resultSet.getString("name")); account.setMoney(resultSet.getFloat("money")); return account; } }
bean.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:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!-- 配置dao--> <bean id="accountDao" class="yh.dao.impl.AccountDaoImpl"> <!--给所继承的父类的属性注入值--> <property name="dataSource" ref="dataSource"/> </bean> <!-- 配置数据源--> <!--引入外部属性文件--> <context:property-placeholder location="data.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> </beans>
data.properties
driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/mybatis?ssl=true&useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=root
注意:第一种方式可使用注解或者 xml 配置;但第二种方式只能用 xml 配置
JavaEE 体系进行分层开发,事务处理位于业务层,Spring 提供了分层设计业务层的事务处理解决方案。Spring 提供了一组基于 AOP 的事务控制接口 ,能够经过编程或配置方式实现。
//获取事务状态信息 TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException; //提交事务 void commit(TransactionStatus var1) throws TransactionException; //回滚事务 void rollback(TransactionStatus var1) throws TransactionException;
开发中使用的是它额实现类对象对事务进行管理:
//使用 Spring JDBC 或 iBatis 进行持久化数据时使用 org.springframework.jdbc.datasource.DataSourceTransactionManager //使用 Hibernate 版本进行持久化数据时使用 org.springframework.orm.hibernate5.HibernateTransactionManager
//获取事务对象的名称 String getName(); //获取事务隔离级别 int getIsolationLevel(); //获取事务传播行为 int getPropagationBehavior(); //获取事务超时时间 int getTimeout(); //获取事务是否只读 boolean isReadOnly();
读写型事务:增长、删除、修改开启事务;
只读型事务:执行查询时,也会开启事务。
事务隔离级别
事务隔离级别反应了事务提交并发访问时的处理态度
ISOLATION_DEFAULT
:默认级别,归属下列某一类ISOLATION_READ_UNCOMMITTED
:能够读取未提交数据ISOLATION_READ_COMMITTED
:只能读取已提交数据,解决脏读问题(Oracle 默认级别)ISOLATION_REPEATABLE_READ
:是否读取其余事务提交修改后的数据,解决不可重复读取问题(MySQL默认级别)ISOLATION_SERIALIZABLE
:是否读取其余事务提交添加后的数据,解决幻影读问题事务的传播行为
REQUIRED:若是当前没有事务,就新建一个事务;若是已经存在一个事务,加入到这个事务中(默认值)
SUPPORTS:支持当前事务,若是当前没有事务,就以非事务方法执行(没有事务)
MANDATORY:使用当前的事务,若是当前没有事务,就抛出异常
REQUERS_NEW:新建事务,若是当前在事务中,把当前事务挂起。
NOT_SUPPORTED:以非事务方式执行操做,若是当前存在事务,就把当前事务挂起
NEVER:以非事务方式运行,若是当前存在事务,抛出异常
NESTED:若是当前存在事务,则在嵌套事务内执行。若是当前没有事务,则执行 REQUIRED 相似的操做
超时时间
默认值是 -1,没有超时限制;如需有,以秒为单位进行设置
是不是只读事务
建议查询时设置为只读
TransactionStatus 接口
/** * TransactionStatus接口描述了某个时间点上事务对象的状态信息,包含有6个具体的操做 */ //刷新事务 void flush(); //获取是否存在存储点 boolean hasSavepoint(); //获取事务是否完成 boolean isCompleted(); //获取事务是否为新的事务 boolean isNewTransaction(); //获取事务是否回滚 boolean isRollbackOnly(); //设置事务回滚 void setRollbackOnly();
必备依赖:spring-jdbc-xxx 和 spring-tx-xxx 等
建立 spring 的配置文件并导入约束
准备数据库表和实体类
编写业务层接口和实现类
编写 Dao 接口和实现类
以上按照项目需求编写,关键是配置,我的理解是上面所写的 AOP 事务的更强形式
编写 bean.xml 配置
<!--配置一个事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--注入数据源--> <property name="dataSource" ref="dataSource"/> </bean> <!--配置事务--> <!--配置事务的通知并引用事务管理器(用于管理事务)--> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <!--配置事务的属性--> <tx:attributes> <!--指定方法名称:是核心业务方法 read-only:是否只读事务,默认false isolation:指定隔离级别,默认值是使用的数据库默认隔离级别 propagation:指定事务的传播行为 timeout:指定超时时间,默认值-1表示永不超时 rollback-for:指定会进行回滚的异常类型,未指定表示任何异常都回滚 no-rollback-for:指定不进行回滚的异常类型,未指定表示任何异常都回滚 --> <tx:method name="*" read-only="false" propagation="REQUIRED"/> <tx:method name="find*" read-only="true" propagation="SUPPORTS"/> </tx:attributes> </tx:advice> <!--aop切点表达式--> <aop:config> <!--配置切入点表达式--> <aop:pointcut id="pt1" expression="execution(* yh.service.impl.*.*(..))"/> <!--配置切入点表达和事务通知的关系--> <aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"/> </aop:config>
对比前面指定通知位置或者使用环绕通知都或多或少须要手动去处理代理逻辑,从而控制控制事务的方法的执行顺序。
而使用 Spring 事务控制器,配置一个事务通知后,咱们只需关联切入点表达式和事务通知便可。
必备依赖:spring-jdbc-xxx 和 spring-tx-xxx 等
建立 spring 的配置文件并导入约束
准备数据库表和实体类
建立业务层接口及其实现类,并使用符合语义的注解让 spring 进行管理
建立持久层接口及其实现类,并使用符合语义的注解让 spring 进行管理
配置步骤
总 JavaConfig 类
@Configuration @Import(value={jdbcConfig.class,JdbcTemplateConfig.class,TransactionManager.class}) @ComponentScan("yh") //建立spring容器时扫描的包 @EnableTransactionManagement //开启基于注解的事务管理功能(与开启aop支持不要混淆) public class SpringConfiguration { }
建立事务管理器配置类并注入数据源
public class TransactionManager { @Bean(name="txManager") public PlatformTransactionManager createTxManager(DataSource dataSource){ return new DataSourceTransactionManager(dataSource); } }
数据源、JdbcTemplate 的 JavaConfig:
@PropertySource("classpath:jdbc.properties") //引入外部的properties属性文件 public class jdbcConfig { @Value("${driverClassName}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Bean(name="dataSource") public DataSource createDataSource(){ DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName(driver); ds.setUrl(url); ds.setUsername(username); ds.setPassword(password); return ds; } }
jdbc.properties 属性文件:
driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306?ssl=true&useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=root
public class JdbcTemplateConfig { @Bean(name="jdbcTemplate") public JdbcTemplate ceeateJdbcTemplate(DataSource dataSource){ return new JdbcTemplate(dataSource); } }
在业务层使用 @Transactional
注解
@Service("accountService") @Transactional(readOnly = true,propagation = Propagation.SUPPORTS) public class AccountServiceImpl implements IAccountService { /** * 获取dao对象 */ @Autowired private IAccountDao accountDao; /** * 转帐方法 * Transactional注解与<tx:Advice/>标签含义相同,配置事务通知 * 可用在接口、类、方法上,表示其支持事务 * 三个位置的优先级 方法>类>接口 */ @Override @Transactional(readOnly = false,propagation = Propagation.REQUIRED) public void transferAccount(String sourceName, String targetName, float money) { //获取帐户 Account source = accountDao.findByName(sourceName); Account target = accountDao.findByName(targetName); //修改帐户金额 source.setMoney(source.getMoney()-money); target.setMoney(target.getMoney()+money); //将修改后的帐户更新至数据库 accountDao.updateAccount(source); // int i = 1/0;//模拟异常 accountDao.updateAccount(target); } }
测试
@RunWith(SpringJUnit4ClassRunner.class) //替换原有运行器 @ContextConfiguration(classes = SpringConfiguration.class) //指定容器配置来源 public class MyTest { @Autowired private IAccountService service; @Test public void test1(){ service.transferAccount("aaa","ccc",100); } }
基于纯注解配置以上。
使用 Spring 事务管理,业务代码全称躺着任由摆布,各层级没有代码侵入问题。