spring
Spring是一个轻量级Java开发框架,由Rod Johnson创建,目的是为了解决企业级应用开发的业务逻辑层和其他各层的耦合问题。它是一个JavaSE/JavaEE分层的full-stack(一站式)轻量级开源框架,为开发Java应用程序提供全面的基础架构支持。
优点:
- 方便解耦,简化开发(IOC/DI):Spring就是一个大工厂,可以将所有对象创建和依赖的关系维护,交给Spring管理。
- AOP编程的支持:Spring提供面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等功能。
- 声明式事务的支持:只需要通过配置就可以完成对事务的管理,而无需手动编程。
- 方便程序的测试:Spring对Junit5支持,可以通过注解方便的测试Spring程序。
- 方便集成各种优秀框架:Spring不排斥各种优秀的开源框架,其内部提供了对各种优秀框架的直接支持(如:Struts、Hibernate、MyBatis等)。
降低JavaEE API的使用难度:Spring对JavaEE开发中非常难用的一些API(JDBC、JavaMail、远程调用等),都提供了封装,使这些API应用难度大大降低。
核心的理念是**控制反转(IOC/DI)**和**面向切面编程(AOP)**,以及**声明式事务**。IOC是spring的基础,AOP则是其重要的功能,最为典型的当属数据库事务的使用。
体系结构
不同的功能放在不同的的模块中,如果程序中要使用什么功能,导入响应的依赖即可。
spring环境搭建
使用Maven搭建spring环境,在pom.xml中进行以下依赖即可:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
IoC/DI:控制反转/依赖注入
控制反转是软件设计大师 Martin Fowler在 2004 年发表的《Inversion of Control Containers and the Dependency Injection pattern》提出的。这篇文章系统阐述了控制反转的思想,提出了控制反转有依赖查找和依赖注入实现方式。
控制反转是一种通过描述(XML或注解)并通过第三方去产生或获取特定对象的方式。使用控制反转带来的最大好处就是降低对象之间的耦合。
程序中对象的产生是基于IoC容器,而不是开发者主动的行为。开发者主动创建的模式,责任归于开发者,在使用IoC容器被动创建的模式下,责任归于IoC容器。基于这样的被动形式,我们就说对象被控制反转了。
获取spring IOC容器的三种方式
- 解析类路径下的XML文件创建(ClassPathXmlApplicationContext),要求配置文件必须存在于类路径下;
- 解析系统文件路径下的XML文件创建(FileSystemXmlApplicationContext),配置文件可以在系统的任意路径中;
- 解析注解创建(AnnotationConfigApplicationContext);
创建IOC容器的三种方式
1、读取类路径下的配置文件
2、读取文件系统中配置文件
3、读取配置类
//
//// ClassPathXmlApplicationContext applicationContext =
//// new ClassPathXmlApplicationContext("applicationContext.xml");
//// User user = applicationContext.getBean("user", User.class);
//// System.out.println(user);
BeanFactory和ApplicationContext的区别:
- BeanFactory创建对象是延迟加载的,即什么时候调用对象,什么时候创建;
- ApplicationContext创建对象则更智能,会根据对象的单例还是多例,来选择是否延迟加载(单例立即加载,多例延迟加载);
实例化Bean的三种方式
使用构造方法创建
入门案例中使用的就是构造方法创建,使用的是默认的无参构造。
使用工厂创建
在实际应用开发过程中,会使用到很多的第三方类库,第三方类库中的类都是class字节码文件,无法通过修改源码的形式提供构造方法,此时类的对象一般情况下是通过工厂模式来提供的,spring核心容器也提供了根据工厂模式来创建和管理类的方法。
实例工厂创建
public class SubjectFactory {
public Subject getSubject(int choice){
switch (choice){
case 1://数学
return new Math();
case 2://语文
return new Chinese();
default:
return null;
}
}
}
<!--通过实例获取对象-->
<!-- <bean id="factory" class="com.woniuxy.domain.SubjectFactory"></bean>-->
<!-- <bean id="subject" factory-bean="factory" factory-method="getSubject">-->
<!-- <constructor-arg name="choice" value="2"/>-->
<!-- </bean>-->
静态工厂创建
public class SubjectFactory {
public static Subject getSubject(int choice){
switch (choice){
case 1://数学
return new Math();
case 2://语文
return new Chinese();
default:
return null;
}
}
}
spring依赖注入的三种方式
spring依赖注入有以下三种方式:
- 构造器注入:依赖于类的构造方法实现,构造方法可有参也可无参;
- setter注入:依赖于类的setter方法实现,灵活且可读性高,这是spring中最主流的注入方式;
- 接口注入:当注入的资源并非来自本系统,而是来自于系统外部,比如数据库连接资源在Tomcat下配置,并通过JNDI的方式去获取,此时数据库资源就属于外部资源,可以使用接口注入方式获取它;
接口注入模式因为具备侵入性,它要求组件必须与特定的接口相关联,因此并不被看好,实际使用有限。
构造器注入
需求:使用带参构造为spring管理的Subject实例注入相应属性值。
步骤1:修改src/com/woniuxy/domain/Subject.java
public class Subject implements Serializable {
private Integer subNo;
private String subName;
public Subject(Integer subNo, String subName) {
this.subNo = subNo;
this.subName = subName;
}
/*toString*/
}
步骤2:修改核心配置文件src/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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="subject" class="com.woniuxy.domain.Subject">
<constructor-arg name="subNo" value="1"/>
<constructor-arg name="subName" value="HTML"/>
</bean>
</beans>
标签属性详解
- type:为构造方法中的某个类型的参数赋值;
- index:指定为构造方法中某个索引的参数赋值,索引从0开始计数;
- name:指定为构造方法中某个名称为name属性取值的参数赋值,一般使用该方式;
- value:被赋值的数据。
- ref:指定为构造方法中某个参数赋值,该值的类型不是java基本数据类型和String类型,而是一个在spring容器中已被注册的bean;
setter注入
需求:使用setter方法为spring管理的Subject实例注入相应属性值。
步骤1:修改src/com/woniuxy/domain/Subject.java
package com.woniuxy.domain;
import java.io.Serializable;
public class Subject implements Serializable {
private Integer subNo;
private String subName;
public Integer getSubNo() {
return subNo;
}
public void setSubNo(Integer subNo) {
this.subNo = subNo;
}
public String getSubName() {
return subName;
}
public void setSubName(String subName) {
this.subName = subName;
}
/*toString*/
}
步骤2:修改核心配置文件src/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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="subject" class="com.woniuxy.domain.Subject">
<property name="subNo" value="1"/>
<property name="subName" value="html"/>
</bean>
</beans>
与 类似,但没有type和index属性。
依赖注入的三种类型
基本数据类型、String
基本数据类型和String可以直接在value处赋值,spring容器会自动完成类型转换并将值注入到对象属性中。
在容器中注册过的bean
在<property>或<constructor-arg>中使用ref属性引用在spring容器中注册过的bean。
复杂类型(集合类型)
spring容器可以注入的复杂类型有数组、List、Set、Map、Properties类型。
需求:一个学生学习多个科目,分别使用数组、List、Set、Map、Properties进行实现。
步骤1:修改实体类src/com/woniuxy/domain/Student.java
public class Student implements Serializable {
private Integer sid;
private String sname;
private Integer ssex;
private Integer sage;
private String saddress;
private Date sbirthday;
private Integer cid;
//private Subject[] subs;
//private List<Subject> subs;
//private Set<Subject> subs;
//private Map<Integer,Subject> subs;
private Properties subs;
/*setter/getter/toString*/
}
步骤2:修改核心配置文件src/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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="student" class="com.woniuxy.domain.Student">
<property name="sid" value="1"/>
<property name="sname" value="张三"/>
<property name="ssex" value="1"/>
<property name="sage" value="35"/>
<property name="saddress" value="北京海淀"/>
<property name="sbirthday" ref="sqlDate"/>
<property name="cid" value="3"/>
<property name="subs">
<!--
数组:<array>
List:<list>
Set:<set>
测试时更改标签即可。
<array>
<bean class="com.woniuxy.domain.Subject">
<property name="subNo" value="1"/>
<property name="subName" value="html"/>
</bean>
<bean class="com.woniuxy.domain.Subject">
<property name="subNo" value="2"/>
<property name="subName" value="java"/>
</bean>
<bean class="com.woniuxy.domain.Subject">
<property name="subNo" value="3"/>
<property name="subName" value="python"/>
</bean>
</array>-->
<!--
Map:<map>和<entry>
Properties:<pros>和<prop>
应注意的是,properties不能保存对象,只能保存值。
<map>
<entry key="1">
<bean class="com.woniuxy.domain.Subject">
<property name="subNo" value="1"/>
<property name="subName" value="html"/>
</bean>
</entry>
<entry key="2">
<bean class="com.woniuxy.domain.Subject">
<property name="subNo" value="2"/>
<property name="subName" value="java"/>
</bean>
</entry>
<entry key="3">
<bean class="com.woniuxy.domain.Subject">
<property name="subNo" value="3"/>
<property name="subName" value="python"/>
</bean>
</entry>
</map>-->
<!--properties的用法-->
<props>
<prop key="1">html</prop>
<prop key="2">java</prop>
<prop key="3">python</prop>
</props>
</property>
</bean>
<bean id="sqlDate" class="java.sql.Date">
<constructor-arg name="year" value="95"/>
<constructor-arg name="month" value="5"/>
<constructor-arg name="day" value="25"/>
</bean>
</beans>
注:
- list、set、array可以互换;
- 在集合中保存的不是对象时,map和props可以互换;
factoryBean:
一个能生产或修饰对象生成的工厂Bean,类似于设计模式中的工厂模式和装饰器模式。它能在需要的时候生产一个对象,且不仅仅限于它自身,它能返回任何Bean的实例。
自定义factoryBean实现
public class MyFactoryBean implements FactoryBean<User> {
@Override
public User getObject() throws Exception {//获取工厂Bean处理的类型的实例对象
User user = new User();
user.setUsername("tom");
user.setPassword("111");
return user;
}
@Override
public Class<?> getObjectType() {//获取工厂Bean处理的类型的Class对象
return User.class;
}
@Override
public boolean isSingleton() {//设置当前处理的对象是否是单例的
return true;
}
}
<!--自定义的factoryBean-->
<bean id="factory" class="com.woniuxy.utils.MyFactoryBean"></bean>
Object object1 = applicationContext.getBean("factory");//获取的是factoryBean中getObject()的返回值
Object object2 = applicationContext.getBean("&factory");//获取的是factoryBean自身
spring提供的factoryBean
list\set\map\properties。。。。
<!-- spring提供的一系列factoryBean -->
<!-- listFactoryBean -->
<bean id="list" class="org.springframework.beans.factory.config.ListFactoryBean">
<!-- 集合类型 -->
<property name="targetListClass" value="java.util.ArrayList"/>
<!-- 集合内容 -->
<property name="sourceList">
<list>
<value>星期一</value>
<value>星期二</value>
<value>星期三</value>
<value>星期四</value>
</list>
</property>
</bean>
<!-- mapFactoryBean -->
<bean id="map" class="org.springframework.beans.factory.config.MapFactoryBean">
<property name="targetMapClass" value="java.util.HashMap"/>
<property name="sourceMap">
<map>
<entry key="a" value="1"/>
<entry key="b" value="2"/>
<entry key="c" value="3"/>
<entry key="d" value="4"/>
</map>
</property>
</bean>
<!-- propertiesFactoryBean -->
<bean id="properties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
<!-- location:用于指定配置文件路径 -->
<property name="location" value="jdbc.properties"/>
</bean>
// ArrayList list = applicationContext.getBean("list", ArrayList.class);
// System.out.println(list);
// Object map = applicationContext.getBean("map");
// System.out.println(map);
Properties properties = (Properties) applicationContext.getBean("properties");
System.out.println(properties.getProperty("username"));
命名空间:p c util
p 对应于 setter注入
引入命名空间:将xmlns:p="http://www.springframework.org/schema/p"放到beans标签中
<!--如果要使用p命名空间,需要去引入 对应setter注入-->
<!-- <bean id="user" class="com.woniuxy.domain.User"-->
<!-- p:username="tom" p:password="111"/>-->
c 对应于构造注入
引入命名空间:将xmlns:c="http://www.springframework.org/schema/c"放到beans标签中
<!--如果要使用p命名空间,需要去引入 对应setter注入-->
<!-- <bean id="user" class="com.woniuxy.domain.User"-->
<!-- c:username="tom" c:password="111"/>-->
util:引入命名空间,注入复杂类型
引入命名空间:将xmlns:util="http://www.springframework.org/schema/util"放到beans标签中
将http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd放到beans标签的xsi:schemaLocation属性中。
<util:list id="list">
<value>1</value>
<value>2</value>
<value>3</value>
</util:list>
<util:map id="map">
<entry key="a" value="1"/>
<entry key="b" value="2"/>
<entry key="c" value="3"/>
</util:map>
bean的生命周期
bean的生命周期图示
面试相关:
面试时有些人会被问到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步即可。
Bean 的完整生命周期
在传统的Java应用中,bean的生命周期很简单,使用Java关键字 new 进行Bean 的实例化,然后该Bean 就能够使用了。一旦bean不再被使用,则由Java自动进行垃圾回收。
相比之下,Spring管理Bean的生命周期就复杂多了,正确理解Bean 的生命周期非常重要,因为Spring对Bean的管理可扩展性非常强,下面展示了一个Bean的构造过程
Bean 的生命周期
如上图所示,Bean 的生命周期还是比较复杂的,下面来对上图每一个步骤做文字描述:
- Spring启动,查找并加载需要被Spring管理的bean,进行Bean的实例化
- Bean实例化后对将Bean的引入和值注入到Bean的属性中
- 如果Bean实现了BeanNameAware接口的话,Spring将Bean的Id传递给setBeanName()方法
- 如果Bean实现了BeanFactoryAware接口的话,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入
- 如果Bean实现了ApplicationContextAware接口的话,Spring将调用Bean的setApplicationContext()方法,将bean所在应用上下文引用传入进来。
- 如果Bean实现了BeanPostProcessor接口,Spring就将调用他们的postProcessBeforeInitialization()方法。
- 如果Bean 实现了InitializingBean接口,Spring将调用他们的afterPropertiesSet()方法。类似的,如果bean使用init-method声明了初始化方法,该方法也会被调用
- 如果Bean 实现了BeanPostProcessor接口,Spring就将调用他们的postProcessAfterInitialization()方法。
- 此时,Bean已经准备就绪,可以被应用程序使用了。他们将一直驻留在应用上下文中,直到应用上下文被销毁。
- 如果bean实现了DisposableBean接口,Spring将调用它的destory()接口方法,同样,如果bean使用了destory-method 声明销毁方法,该方法也会被调用。
bean的作用域:
<bean id="" class="" scope="singleton|prototype|request|session|globalsession"></bean>
在 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容器保持一致。**
**多例对象**:
出生:容器创建时,多例对象延迟加载,直到使用该对象时spring容器执行创建操作;
- 存活:只要对象被使用,对象就一直存在;
- 死亡:多例对象不会随着容器销毁而销毁,它的销毁由java垃圾回收机制决定;
init-method,destroy-method
需求:测试bean的生命周期与作用域之间的关系。
步骤1:修改src/com/woniuxy/domain/Subject.java
public class Subject implements Serializable {
......
public void init(){
System.out.println("subject对象被创建了。。。");
}
public void destroy(){
System.out.println("subject对象被销毁了。。。");
}
......
}
步骤2:修改核心配置文件src/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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="subject" class="com.woniuxy.domain.Subject"
scope="prototype" init-method="init" destroy-method="destroy">
<property name="subNo" value="1"/>
<property name="subName" value="html"/>
</bean>
</beans>
步骤3:修改测试类
public class Test {
public static void main(String[] args) {
//1、读取配置文件,生成spring核心容器
//注意,此处不使用多态,因为ApplicationContext接口中没有close()方法
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
//2、根据bean标签id获取spring核心容器管理的bean实例
Subject sub = (Subject) ac.getBean("subject");
System.out.println(sub);
//3、关闭spring容器
ac.close();
}
}
运行测试类可以发现:当<bean>标签scope属性为singleton时,spring容器关闭时,subject对象被销毁;当<bean>标签scope属性为prototype时,spring容器关闭时,subject对象没有被销毁。
自动装配:
通过bean标签的autowire属性来设置:
byName 根据bean标签的id属性
byType 根据类型:bean标签的class属性
default 默认跟随上一级标签设置的自动装配策略
constructor 本质也是byType
no 不自动装配
<!-- 自动装配的实现:bean标签的autowire实现 -->
<bean id="user" class="com.woniuxy.domain.User" autowire="byName">
</bean>
<bean id="phone" class="com.woniuxy.domain.Phone">
<property name="phoneCode" value="18888888888"/>
</bean>
<bean id="phone1" class="com.woniuxy.domain.Phone">
<property name="phoneCode" value="16666666666"/>
</bean>
注解:
包扫描:context命名空间提供
xmlns:context="http://www.springframework.org/schema/context"
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
<!--base-package属性指定要扫描的包-->
<context:component-scan base-package="com.woniuxy"/>
// @Configuration 放在配置类上
// @Component 无特殊语境的位置
// @Service 业务层实现类上
// @Repository 执久层实现类上
// @Controller 控制层类上
扫描包下的注解后,会将被注解的类的类名首字母小写后,作为id保存到容器中。
赋值的注解:@Value
@Value("tom")
private String username;
自动装配的注解:
@Autowired byType
@Resource 先byName查找,没找到再byType查找
如果以byType的方式在容器中找到多个相同类型的bean,此时会报错,如何处理?
方式一:@Primary 指定优先级,放在要优先装配的类上
方式二:@Qualifier(“cat”) 选择性的装配一个bean进来,参数是bean的id。
指定作用域的注解:@Scope
使用注解替换核心配置文件
spring提供了自定义配置类来替换spring核心配置文件的功能。
需求:使用注解实现自定义配置类替换spring核心配置文件。
@Configuration注解的类会被spring认为是一个配置类,相当于创建了一个核心配置文件。
@ComponentScan表示进行扫描,有两个配置项:basePackages\basePackageClasses。
- 当不写任何配置项时,默认扫描被注解的ApplicationConfig所在包中的所有被@Component注解的类;
- 当配置basePackages时,basePackages属性取值为一个包的完全限定名数组,表示spring容器会把数组中存在的包下所有被@Component注解的类装配成bean;
当配置basePackageClasses时,basePackageClasses属性取值为一个类或者接口的完全限定名数组,表示spring容器会把数组中存在的类或者接口的实现类装配成bean;
basePackages和basePackageClasses可以同时存在,spring会进行区分,不会重复配置生成多个对象。basePackages等同于value,查看源码可知,是通过别名来实现的。
@Bean注解用于将某个方法返回的对象注册到spring容器中,一般用于第三方类库的类(比如数据库连接池等)。该注解有两个等效的配置项:value和name,是一个String类型的数组,其值用于设定注册到spring容器的bean名称,如果不设,默认将当前方法名作为注册到spring容器中的bean的id。
步骤3:修改测试类
public class Test {
public static void main(String[] args) {
//1、通过反射生成spring核心容器
ApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class);
//2、根据bean标签id获取spring核心容器管理的bean实例
DataSource ds = ac.getBean("dataSource", DataSource.class);
try {
Connection conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement("select * from t_subject");
ResultSet rs = ps.executeQuery();
while (rs.next()) {
System.out.print(rs.getInt("sub_no")+"\t");
System.out.println(rs.getString("sub_name"));
}
rs.close();
ps.close();
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
使用注解实现的自定义配置类代替了核心配置文件,因此获取spring容器的方式也需进行变化,需要使用
AnnotationConfigApplicationContext对象来获取spring容器,其构造方法是一个可变长度的参数数组。
另外,@Configuration可以省略,如果不配,则必须将自定义配置类作为参数传递给AnnotationConfigApplicationContext的构造方法,否则spring不认为该类为配置类,就不会去扫描类中的注解。
项目中可以存在多个配置文件,比如在项目的主配置文件中存放公共配置,某些具体的配置(比如说获取数据库连接)则会单独创建一个配置文件。非主配置文件上一般都会使用@Configuration注解,因为在使用AnnotationConfigApplicationContext获取容器时使用参数过多,会造成代码阅读困难,且容易遗漏。
存在多个配置文件时,也可以使用@Import注解,将非主配置类导入主配置类。@Import注解只有一个value属性可以配置,是一个Class类型的数组,用于指定其他配置类的字节码。
需求:创建多个配置文件,使用@Import将对应配置导入主配置类。
步骤1:创建配置类src/config/JdbcConfig.java,将ApplicationConfig中获取数据源的代码转移到本类中。
@Configuration
public class JdbcConfig {
@Bean(name="dataSource")
public DataSource getDataSource(){
DruidDataSource dataSource = new DruidDataSource();
// dataSource.setDriverClassName();
// dataSource.setUrl();
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}
步骤2:修改配置类src/config/ApplicationConfig.java
@Configuration
@ComponentScan(value = {"com.woniuxy","config"})
@Import(JdbcConfig.class)
public class ApplicationConfig {
}
运行测试类,可以正常运行。
上例中的JdbcConfig类中通过硬编码,将获取数据源的jdbc属性写在源码中,不合理。一般会使用软编码,即用属性文件保存jdbc连接所需要的值,可以使用@PropertySource注解读取属性文件获取对应的值。
需求:使用Properties文件保存jdbc连接属性。
步骤1:创建属性文件src/jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/taotao
jdbc.username=root
jdbc.password=root
步骤2:修改src/config/JdbcConfig.java
@Configuration
//使用@PropertySource读取属性文件,值为属性文件路径加名称
//classpath为关键字,表明属性文件在类路径下
//ignoreResourceNotFound用于设置当属性文件未找到时使用的策略,
//值为true时,spring没找到属性文件会忽略,
//值为false时,spring没找到属性文件会抛出异常
@PropertySource("classpath:jdbc.properties",ignoreResourceNotFound = true)
public class JdbcConfig {
//使用@Value注解通过spring的EL表达式获取属性文件中的值
//表达式为属性文件中的key值
//注意:读取属性文件使用$,而使用SpEL则使用的是#
@Value("${jdbc.driver}")
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 getDataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}
AOP
aop:面向切面编程,通过预编译和运行期动态代理,实现在不修改程序源代码的基础上统一动态和给程序添加某些功能的技术。
相关术语
通知(advice):代理对象的方法。分为前置通知,最终通知,返回通知,异常通知,环绕通知五类。以反射执行目标对象方法为基准:
- 前置通知:在目标对象方法执行之前执行;
- 最终通知:在目标对象方法执行完成后,无论在执行过程中是否出现异常,都会执行;
- 异常通知:目标对象方法执行过程中产生异常后执行;
- 返回通知:目标对象方法执行过程中不产生异常时执行;
环绕通知:特殊的通知,不以目标对象方法为基准,可以同时实现前置和后置通知。环绕通知保留了调度目标对象原有方法的功能,也就是说,使用环绕通知甚至可以不必要调用目标对象方法,可以取代目标对象的方法。因此环绕通知非常强大,而且灵活,但可控性差,一般不需要大量改变业务逻辑的情况下,不会使用环绕通知。
**织入(weaving)**:将通知组合到目标的过程。
**连接点(join point)**:能被代理对象增强的方法。
**切入点(pointcut)**:真正被增强的方法。
注意:只有被切入点表达式匹配的连接点,才能成为切入点。切入点一定是连接点,而连接点不一定是切入点,因为连接点有可能不需要进行增强。
**切面(aspect)**:通知与切点的结合。
xml实现aop
1、导入依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
2、编写通知类
package com.woniuxy.interceptor;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* 进行事务管理的类
*/
public class TransactionManager {
public void beginTransaction(){
System.out.println("开启事务");
}
public void commit(){
System.out.println("提交事务");
}
public void rollback(){
System.out.println("回滚事务");
}
public void close() {
System.out.println("释放资源");
}
}
3、编写核心配置文件(引入aop命名空间)
<?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
">
<bean id="userService" class="com.woniuxy.service.impl.UserServiceImpl"/>
<!--配置通知-->
<bean id="transactionManager" class="com.woniuxy.interceptor.TransactionManager"></bean>
<!--aop相关的配置-->
<aop:config>
<!--
配置切入点
访问修饰符 方法返回值 方法名(参数列表)
-->
<aop:pointcut id="pointCut" expression="execution(* com.woniuxy.service.impl.*.*(..))"/>
<!--配置切面-->
<aop:aspect ref="transactionManager">
<aop:before method="beginTransaction" pointcut="execution(* com.woniuxy.service.impl.*.*(..))"/>
<aop:after-returning method="commit" pointcut-ref="pointCut"/>
<aop:after-throwing method="rollback" pointcut-ref="pointCut"/>
<aop:after method="close" pointcut-ref="pointCut"/>
</aop:aspect>
</aop:config>
</beans>
注解实现aop
1、导入依赖
2、创建配置类
@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = {"com.woniuxy"})
public class ApplicationConfig {
}
3、修改通知类
/**
* 进行事务管理的类
*/
@Component
@Aspect
public class TransactionManager {
@Pointcut("execution(* com.woniuxy.service.impl.*.*(..))")
public void pointCut(){}
@Before("pointCut()")//前置通知
public void beginTransaction(){
System.out.println("开启事务");
}
@AfterReturning("pointCut()")//返回通知
public void commit(){
System.out.println("提交事务");
}
@AfterThrowing("pointCut()")//异常通知
public void rollback(){
System.out.println("回滚事务");
}
@After("pointCut()")//最终通知
public void close() {
System.out.println("释放资源");
}
}
QueryRunner
使用queryRunner解决对象引用的问题
1、导入依赖 commons-dbutils
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.6</version>
</dependency>
2、将queryrunner对象交由容器管理
@Bean
public QueryRunner queryRunner(){
return new QueryRunner();
}
3、dao层注入queryrunner,并使用该对象进行增删改查
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {
//使用一个工具QueryRunner commons-dbutils
//JdbcTemplate 也可以操作数据库
@Resource
private ConnectionUtil connectionUtil;
@Resource
private QueryRunner queryRunner;
@Override
public Account findByUsername(String username) throws SQLException {
//参数1:数据库连接 参数2:要执行的sql语句 参数3:查询结果使用哪种类型进行封装 参数4:sql语句执行时,需要填充的参数
//query(Connection conn, String sql, ResultSetHandler<T> rsh, Object... params)
Account account = queryRunner.query(connectionUtil.getConnection(),
"select * from account where username=?",
new BeanHandler<Account>(Account.class), username);
return account;
}
@Override
public int updateAccount(Account account) throws SQLException {
int i = queryRunner.update(connectionUtil.getConnection(),
"update account set balance=? where username=?",
account.getBalance(), account.getUsername());
return i;
}
}
环绕通知
通过环绕通知可以解决spring5.2.6及以下版本中,使用注解实现aop时出现通知执行顺序问题(前置->最终->返回或异常),5.2.7版本及以上,这个bug已被修复。
@Component
@Aspect
public class TransactionManager {
//真正的去完成事务控制,不能再使用伪代码
@Resource
private ConnectionUtil connectionUtil;
@Pointcut("execution(* com.woniuxy.service.impl.*.*(..))")
public void pointCut(){}
// @Before("pointCut()")
public void beginTransaction(){
try {
connectionUtil.getConnection().setAutoCommit(false);
System.out.println("事务开启");
} catch (SQLException e) {
e.printStackTrace();
}
}
// @AfterReturning("pointCut()")
public void commit(){
try {
connectionUtil.getConnection().commit();
System.out.println("事务提交");
} catch (SQLException e) {
e.printStackTrace();
}
}
// @AfterThrowing("pointCut()")
public void rollback(){
try {
connectionUtil.getConnection().rollback();
System.out.println("事务回滚");
} catch (SQLException e) {
e.printStackTrace();
}
}
// @After("pointCut()")
public void close(){
try {
connectionUtil.close();
} catch (Exception e) {
e.printStackTrace();
}
}
//环绕通知和四大通知的方式任选其一
@Around("pointCut()")//环绕通知的注解
public Object around(ProceedingJoinPoint pjp){
Object[] args = pjp.getArgs();//获取真实对象方法的参数列表
Object obj = null;
try {
//开启事务
beginTransaction();
obj = pjp.proceed(args);//类似于动态代理中的method.invoke(),是在调用真实对象的方法
//提交事务
commit();
} catch (Throwable throwable) {
throwable.printStackTrace();
//回滚事务
rollback();
} finally {
//释放资源
close();
}
return obj;
}
}
声明式事务
spring提供了声明式事务的接口PlatformTransactionManager接口,使用其实现类DatasourceTransactionManager即可实现声明式事务。
声明式事务中不关心事务开启和资源释放,只关心commit和rollback。
xml实现声明式事务
使用XML实现spring声明式事务的步骤:
0.导入依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.22</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.43</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.18</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
</dependencies>
- 配置事务管理器;
- 配置事务通知:为业务层方法配置事务,并设置事务属性;
- 配置aop:配置切入点表达式,并让切入点表达式与事务通知产生关联;
<?xml version="1.0" encoding="UTF-8"?>
<!--配置业务层实现类-->
<bean id="accountService" class="com.woniuxy.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"/>
</bean>
<!--配置数据库连接池-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///bank?characterEncoding=UTF-8&useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
<!--配置jdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置事务通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--
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: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="pt" expression="execution(* com.woniuxy.service.impl.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt"/>
</aop:config>
<!--
声明式事务的实现步骤:
1、导入依赖 spring-tx(必须) spring-context(核心容器+aop) spring-jdbc mysql驱动 druid
2、编写核心配置文件
2.1配置事务管理器:datasourcetransactionmanager
2.2配置事务通知:tx:advice transaction-manager属性引用配置好的事务管理器
tx:attributes
tx:method name="*" propagation="REQUIRED" read-only="false" 给增删改的业务层方法使用
tx:method name="find*" propagation="SUPPORTS" read-only="true" 给业务层的查询方法使用
2.3配置aop:
配置切入点
配置tx:advisor 引用事务通知 引用切入点
-->
常用注解
注解名 | 作用 |
---|---|
@Configuration |
用在配置类上 |
@Component |
无语义的类上 |
@Service |
用在业务层实现类上 |
@Repository |
用在数据访问层实现类上 |
@Controller |
用在控制层的类上 |
@Value |
用于给类的属性赋值 |
@Autowired |
根据类型自动注入,用于进行自动注入的成员变量上 |
@Resource |
先byName,再byType。用于进行自动注入的成员变量上 |
@Primary |
用于在要优先注入的类上 |
@Qualifier |
用于在成员变量上,用于指定注入一个容器中的bean |
@Bean |
用于将第三方的类注册到spring容器中,用在某个方法上,将方法返回值注册到容器 |
@ComponentScan |
用在配置类上,用于指定扫描的包 |
@PropertySource |
用于在配置类上,用于加载类路径下的properties文件 |
@Scope |
用在类上,用于指定当前类的作用范围,一般用于配置多例或单例 |
@Aspect |
用在切面类上, |
@PointCut |
用在方法上,用于配置切入点表达式 |
@Before@After@AfterReturning@AfterThrowing@Around |
用在切面类的方法上,五大通知 |
@EnableAspectJAutoProxy | 用在配置类上,用于开启aspectj的自动代理(识别aspectj的注解) |
@EnableTransactionManagement | 用在配置类上,用于开启注解事务 |
@Transactional |
用在业务层实现类上或实现类的方法上,用于指定事务策略 |
@Import |
用在配置类上,用于导入另外一个配置类 |
注解实现声明式事务
1、编写配置类
@Configuration
@EnableTransactionManagement//开启注解事务管理
@ComponentScan(basePackages = {"com.woniuxy"})
public class AppConfig {
@Bean("dataSource")
public DataSource dataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql:///bank?characterEncoding=UTF-8&useSSL=false");
dataSource.setUsername("root");
dataSource.setPassword("root");
return dataSource;
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
@Bean
public DataSourceTransactionManager dataSourceTransactionManager(){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource());
return dataSourceTransactionManager;
}
}
2、使用@Transactional注解进行事务控制
@Transactional注解用在类上,表示类中所有方法使用同一种事务策略
也可以将其放在方法上,表示当前被注解的方法使用特定的事务策略。
@Service("accountService")
@Transactional
public class AccountServiceImpl implements AccountService {
@Resource
private AccountDao accountDao;
@Transactional(propagation = Propagation.SUPPORTS,readOnly = true)
public Account findByUserName(String username){
return null;
}
@Override
public void transfer(String source, String target, Double money) throws SQLException {
//查询要进行转帐操作的两个人的帐户信息
Account sourceAccount = accountDao.findByUsername(source);
Account targetAccount = accountDao.findByUsername(target);
//判断被转出人的帐户余额是否足够
if (sourceAccount.getBalance()>money) {
//真正执行转账操作
sourceAccount.setBalance(sourceAccount.getBalance()-money);
targetAccount.setBalance(targetAccount.getBalance()+money);
accountDao.updateAccount(sourceAccount);
// System.out.println(1/0);
accountDao.updateAccount(targetAccount);
}else{
throw new RuntimeException("余额不足");
}
}
}