Spring基础总结篇
Bean:可重用组件(JavaBean就是用Java语言编写的可重用组件)
1.获取 Spring 的 IOC 容器,并根据 bean 的 id 获取注入对象
1.通过 BeanFactory 获取 Bean
//加载类路径下的bean配置文件(xml)
Resource resource = new ClassPathResource("bean.xml");
//读取配置文件
BeanFactory factory = new XmlBeanFactory(resource);
//根据bean的id获取指定的bean,并指定获取到的bean的对象类型(也可以强制类型转换)
IAccountService accountService = factory.getBean("accountService",IAccountService.class);
BeanFactory:是Spring里面最低层的接口,提供了最简单的容器的功能,只提供了实例化对象和拿对象的功能;
它在创建核心容器时,创建对象采取的策略是采用延时加载的方式;也就是说,什么时候根据id获取对象,什么时候才真正创建对象。
优点:应用启动的时候占用资源很少;对资源要求较高的应用,比较有优势;
2.通过 ApplicationContext 应用上下文获取 Bean
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml"); //bean.xml放在根目录下
//ApplicationContext aa = new FileSystemXmlApplicationContext("path"); //从硬盘的绝对路径下获取配置文件
//2.根据id获取bean对象
//可以使用强制类型转换
//IAccountService as = (IAccountService) ac.getBean("accountService");
//也可以指定对象类型
IAccountService as = ac.getBean("accountService",IAccountService.class);
ApplicationContext:应用上下文,继承BeanFactory接口,它是Spring的一各更高级的容器,提供了更多的有用的功能;
它在创建核心容器时,创建对象采取的策略是采用立即加载的方式;也就是说,只要一读取完配置文件马上就创建配置文件中配置的对象(使用构造函数和断点的方式检测)。
优点:
-
所有的Bean在启动的时候都加载,系统运行的速度快;
-
在启动的时候所有的Bean都加载了,我们就能在系统启动的时候,尽早的发现系统中的配置问题
-
建议web应用,在启动的时候就把所有的Bean都加载了。(把费时的操作放到系统启动中完成)
注:几乎所有的应用场合我们都直接使用ApplicationContext 而非底层的BeanFactory。
2.Bean 详解
1.创建bean的三种方式
1)通过构造函数创建bean
-
无参构造函数
在spring配置文件中使用bean标签,配以id和class属性,且没有其他属性和标签时,即采用默认构造函数创建bean对象,此时若类中没有默认构造函数,则对象无法创建。
<bean id="accountService" class="com.simple.service.impl.AccountServiceImpl"></bean>
-
带参构造函数
<!-- 参数为对象 --> <bean id="accountService" class="com.simple.service.impl.AccountServiceImpl" > <constructor-arg ref="参数的bean id"></constructor-arg> </bean> <!-- 参数是string,int等等除bean对象之外的其他类型 --> <bean id="accountService" class="com.simple.service.impl.AccountServiceImpl" > el表达式: <constructor-arg value="#{T(其他各种类型,如string等等)}"></constructor-arg> </bean>
2)使用普通工厂中的方法创建对象(使用某个类中的方法创建对象,并存入spring容器)
普通工厂类:
public class InitFactory {
public IAccountService getAccountService(){
return new AccountServiceImpl();
}
}
xml配置:
<!-- 实例化普通工厂类 -->
<bean id="initFactory" class="com.simple.factory.InitFactory"></bean>
<!-- 调用普通工厂类的方法创建要创建的bean -->
<bean id="accountService" factory-bean="initFactory" factory-method="getAccountService"></bean>
3)使用工厂中的静态方法创建对象(或者使用某个类中的静态方法创建对象,并存入spring容器)
工厂类:
public class staticFactory {
public static IAccountService getAccountService(){
return new AccountServiceImpl();
}
}
xml配置:
<!-- 因为使用的是工厂中的静态方法,所以不需要实例化工厂 -->
<bean id="accountService" class="com.simple.factory.staticFactory" factory-method="getAccountService"></bean>
2.bean的作用范围(对应实例的作用范围)
bean的作用范围由 scope 属性控制,取值有以下5个:
- singleton:单例的(默认值),表示在整个bean容器中或者说是整个应用中只会有一个实例。
- prototype:多例的,表示每次从bean容器中都会获取到一个对应bean定义全新的实例。
- request:作用于web应用的请求范围,表示每一个HttpRequest生命周期内会有一个单独的实例,即每一个Http请求都会拥有一个单独的实例。
- session:作用于web应用的会话范围,表示每一个HttpSession生命周期内会有一个单独的实例,即每一个HttpSession下都会拥有一个单独的实例,即每一个用户都将拥有一个单独的实例。
- global-session:作用于集群环境的会话范围(全局会话范围),当不是集群环境时,它就是session。
配置 scope 属性:
xml配置或使用 @Scope("prototype")
注解
<bean id="accountService" class="com.simple.service.impl.AccountServiceImpl" scope="prototype"></bean>
3.bean的生命周期
-
单例bean对象:
出生:当容器创建时对象出生 活着:只要容器还在,对象就一直活着 死亡:容器销毁,对象消亡 总结:单例对象的生命周期和容器一致
xml配置:
<bean id="accountService" class="com.simple.service.impl.AccountServiceImpl" scope="singleton" init-method="init" destroy-method="destory"> </bean> <!-- init-method,destroy-method指定创建和销毁该bean时要执行的方法 -->
-
多例bean对象:
出生:当需要使用该bean对象时spring容器创建 活着:该对象在使用过程中一直活着 死亡:当该对象长时间不用且没有别的对象引用他时,由Java的垃圾回收机制进行回收
xml配置:
<bean id="accountService" class="com.simple.service.impl.AccountServiceImpl" scope="prototype" init-method="init" destroy-method="destory"> </bean>
3.Spring的DI–Dependency Injection(依赖注入)
1.控制反转(Inversion of Control,IOC)
了解 DI 之前先看看 IOC。
IOC,即控制反转,它不是什么技术,而是一种设计思想。在 Java 开发中,IOC 意味着将你设计好的对象交给容器控制,而不是传统的在程序中直接控制(new)。
- 控制详解:在传统的 Java 程序设计中,我们都是在程序中直接通过 new 去创建新对象,是程序主动去创建依赖对象;而 IOC 则是有一个专门的容器去创建这些对象,即 ioc 容器控制对象的创建。所有说 ioc 容器控制了对象(主要控制了外部资源的获取【不仅仅是对象包括其他比如资源文件等】)。
- 反转详解:有反转也有正转,传统应用程序是由对象主动直接控制依赖对象(new),也就是正转;而反转则是由容器去创建及注入依赖的对象。为什么叫反转呢?因为由容器帮程序查找及注入依赖对象,对象只是被动地接受依赖对象。(依赖对象的获取被反转了)
- IOC 的作用:IOC 不是一种技术,而一种设计思想(“主从换位”),一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难以测试;有了 ioc 容器后,把创建和查找依赖对象的控制权交给了容器,由容器注入依赖对象,所以对象与对象之间是松散耦合,也方便测试,利于功能的复用,使得程序的整个体系结构变得非常灵活。
2.依赖注入(DI)
DI,即“依赖注入”:组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。**依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。**通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
- **依赖:**应用程序依赖于 ioc 容器,应用程序需要 ioc 容器来提供对象需要的外部资源。
- **注入:**ioc 容器注入到应用程序的某个对象(应用程序依赖的对象),注入了该对象所需要的外部资源(包括对象、常量数据、其他资源等)
依赖注入的前提是有 ioc 容器,即 DI 需要在 IOC 的基础上来完成。
DI/IOC 其实是同一个概念的不同角度描述。
3.进行依赖注入
1)能够进行依赖注入的数据分为三种:
-
基本数据类型和 String
-
其他的 bean 对象
-
复杂类型/集合类型
通过 set 方法注入各种集合类型实例:
要注入的类:
public class AccountServiceImpl3 implements IAccountService { /** * 复杂类型/集合类型的注入 * 定义复杂类型和集合类型,通过set方法注入的方式注入复杂类型/集合类型 */ 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; } }
xml配置:
<!-- 复杂类型/集合类型的注入: 用于给 List 结构集合注入的标签: array list set 用于给 Map 结构集合注入的标签: map props --> <bean id="accountService3" class="com.simple.service.impl.AccountServiceImpl3" > <property name="myStrs"> <array> <value>AAA</value> <value>BBB</value> <value>CCC</value> <value>DDD</value> </array> </property> <property name="myList"> <list> <value>AAA</value> <value>BBB</value> <value>CCC</value> <value>DDD</value> </list> </property> <property name="mySet"> <set> <value>AAA</value> <value>BBB</value> <value>CCC</value> <value>DDD</value> </set> </property> <property name="myMap"> <map> <entry key="testA" value="AAA"></entry> <entry key="testB"> <value>BBB</value> </entry> </map> </property> <property name="myProps"> <props> <prop key="testC">CCC</prop> <prop key="testD">DDD</prop> </props> </property> </bean>
2)注入的方式:3种
1.通过构造函数注入:在实际开发中使用较少
要注入的类(对象):
public class AccountServiceImpl implements IAccountService {
/**
* 使用构造函数注入
* 注意以下参数的数据类型(如果是经常不一样的数据,并不适用于注入的方式)
*/
private String a;
private Integer b;
private Date c;
public AccountServiceImpl(String a, Integer b, Date c) {
this.a = a;
this.b = b;
this.c = c;
}
}
xml配置:
<!--
constructor-arg 标签中的属性:
name:用于指定给构造函数中指定名称的参数赋值
value:用于提供基本数据类型和String类型的参数直接赋值
ref:用于指定其他的bean类型数据
type:用于指定要注入的数据的数据类型
一般上面前3个够用了,type使用较少
-->
<bean id="accountService" class="com.simple.service.impl.AccountServiceImpl" >
<constructor-arg name="a" value="test"></constructor-arg>
<constructor-arg type="java.lang.Integer" name="b" value="18"></constructor-arg>
<constructor-arg name="c" ref="now"></constructor-arg>
</bean>
<!-- 配置一个日期对象 -->
<bean id="now" class="java.util.Date"></bean>
查看注入结果:
//1.获取核心容器对象
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//2.根据id获取bean对象
IAccountService as = ac.getBean("accountService",IAccountService.class);
一般来说,只有在数据注入是必须的情况下,才会使用构造函数注入(即不注入参数对象就无法创建成功)。构造函数注入的方式,若是不需要使用到这些构造函数参数,就显得很多余。
2.通过 set 方法注入:较为常用
要注入的类:
public class AccountServiceImpl2 implements IAccountService {
/**
* 使用set方法注入
* 注意以下参数的数据类型(如果是经常不一样的数据,并不适用于注入的方式)
*/
private String a;
private Integer b;
private Date c;
public void setAone(String a) {
this.a = a;
}
public void setBone(Integer b) {
this.b = b;
}
public void setCone(Date c) {
this.c = c;
}
}
xml配置:
<!--
property 标签中的属性:
name:用于指定注入时所调用的set方法名称(除了set的部分)
value:用于提供基本类型和string类型的数据
ref:用于指定其他的bean类型数据(即spring的IOC核心容器中出现过的bean对象)
-->
<bean id="accountService2" class="com.simple.service.impl.AccountServiceImpl2" >
<property name="aone" value="test"></property>
<property name="bone" value="21"></property>
<property name="cone" ref="now"></property>
</bean>
使用 set 方法注入,是通过默认构造函数创建对象的。
3.使用 注解注入
使用到的注解分为4类:
特别注意,使用注解时需要告知spring在创建容器时要扫描的包(xml配置) 。
<!-- 使用注解时需要告知spring在创建容器时要扫描的包 -->
<context:component-scan base-package="com.simple"></context:component-scan>
1.用于创建对象的注解:此类注解类似于创建 bean (<bean></bean>
标签) 实现的功能是一样的
@Component:
作用:用于把当前类对象存入spring容器中
属性:
value:用于指定bean的id。value属性的默认值是当前类名,且首字母改小写(如果只有一个 value属性时,value可以不写)
@Controller:一般用在表现层
@Service:一般用在业务层
@Repository:一般用在持久层
以上三个注解的作用和属性与 @Component 一模一样。它们三个是spring框架为我们提供明确的三层使用的注解,使我们的三层对象更加清晰
2.用于注入数据的注解:此类注解的作用就和在xml配置文件中的bean标签中写一个<property></property>
标签的作用是一样的
@Autowired:
作用:自动按照类型注入,只要容器中有唯一的一个bean对象类型和要注入的变量类型匹配,就可以注入成功
如果ioc容器中没有任何 bean的类型和要注入的变量类型匹配,则报错
如果ioc容器存在多个相同类型的bean对象,就根据id值和变量名进行匹配
出现位置:可以在变量上,也可以在方法上
注意:在使用注解注入时,settter,getter方法就不是必须的了
(java允许用户关闭【属性】或者【方法】的【访问控制权限】,反射中的方法更改访问权限)
@Qualifier:
作用:在按照类型注入的基础之上再按照名称注入。它在给类成员注入时不能单独使用,但在给方法参数注入时可以单独使用
属性:
value:用于指定注入bean的id
@Resource:
作用:直接按照bean的id注入,它可以独立使用
属性:
name:用于指定bean的id
以上三个注解都只能注入其他bean类型的数据,而基本类型和String类型无法使用上述注解实现注入,另外,复杂类型/集合类型只能通过xml实现注入
@Value:
作用:用于注入基本类型和String类型的数据
属性:
value:用于指定数据的值,它可以使用spring中的SpEL(Spring中的el表达式)
SpEL的写法:#{表达式 }
3.用于改变bean作用范围的注解:与 bean 标签中的 scope 属性实现的功能是一样的
@Scope:
作用:用于指定bean的作用范围,在类上面使用
属性:
value:指定范围的取值。常用的取值有:singleton prototype (默认是singleton)
4.和bean生命周期相关的注解:和在xml配置文件的bean标签中使用 init-method 和 destory-method的作用是一样的
以下两个注解了解即可
@PreDestory:
作用:用于指定销毁方法
@PostConstruct:
作用:用于指定初始化方法
4.Spring配置的注解
Spring 的配置方式可以分为三种:
- 在 xml 中进行显式配置(纯xml)
- 在 Java 中进行显式配置(基于 Java 配置类,较xml更为强大、类型安全并且对重构友好)
- 隐式的bean发现机制和自动配置(注解+配置类)
Spring 从两个角度来实现自动化配置:
- 组件扫描(component scanning):Spring会自动发现应用上下文中所创建的bean
- 自动装配(autowiring):Spring自动满足bean之间的依赖
1.配置常用的注解
自动配置的部分注解:
@Component
表明该类是一个组件类,并告知Spring要为这个类创建bean
@ComponentScan
表明在spring中启用了组件扫描(一般在配置类使用此注解,扫描带@Component的类,并自动创建一个bean)。
默认扫描与配置类相同的包中的类;可以在@ComponentScan的value属性中指明要扫描的包的名称(value可省略)
basePackages表明设置要扫描的包是基础包,basePackages = {"包1","包2"...};basePackageClasses={类1.class,类 2.class...},更常用,安全性更好
xml实现扫描包:
<context:component-scan base-package="com.simple"></context:component-scan>
base-package 指明要扫描的包路径
@Name
spring支持@Name作为@Component的替代,但使用较少
@ContextConfiguration
这个注解通常与@RunWith(SpringJUnit4ClassRunner.class)联合使用用来测试
当我们想要在某个测试类使用@Autowired注解来引入这些收集起来的bean时,只需要给这个测试类添加@ContextConfiguration注解来标注我们想要导入这个测试类的某些bean。
属性:
locations:用xml配置,指定xml文件的位置,加上 classpath 关键字,表示在类路径下
classes:用注解配置,指定注解类所在的位置
@Autowired
自动装配,可用在变量声明,构造器,setter方法上,@Autowired可用在类的任何方法上
不管是构造器、Setter方法还是其他的方法,Spring都会尝试满足方法参数上所声明的依赖。
查找bean的方式:根据类型查找,如果有多个,就根据id获取参数
@Inject
@Inject注解来源于Java依赖注入规范(@Name也是),大多数情况下,@Inject可与@Autowired替换
在Java中进行显式配置常用的注解:
@Configuration
表明该类是一个配置类,该类应该包含在Spring应用上下文中如何创建bean的细节
当配置类作为 AnnotationConfigApplicationContext 对象创建的参数时,该注解可用不写
@Bean
作用:用于把当前方法的返回值作为bean对象存入spring的ioc容器中
属性:
name:用于指定bean的id.默认值是当前方法的名称
此注解用在方法上,该方法方法体中包含了最终产生bean实例的逻辑,new一个对象。默认情况下,bean的ID与带有@Bean注解的方法名是一样的。如果你想为其设置成一个不同的名字的话,那么可以重命名该方法(设置name属性)
@Import
作用:用于导入其他的配置类(使用此注解后,不再需要额外扫描其他配置类)
效果等价于将其他的子配置类导入到总配置类中,只需要使用了总配置类,其他子配置类就不再需要扫描或额外的 @Configuration 注解
@Import({c1.class,c2.class})
@ImportResource
表示导入配置的xml文件@ImportResource("classpath:bean.xml")
@PropertySource
作用:用于指定properties文件的位置
属性:
value:指定文件的名称和路径
关键字:classpath 表示在类路径下
例如,通过配置类读取properties文件配置数据库连接
@Configuration
public class JDBCConfig {
//通过配置文件读取数据库相关信息
//@Value注解+EL表达式
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
/**
* 用于创建一个QueryRunner对象
* @param dataSource
* @return
*/
@Bean(name = "runner")
@Scope("prototype") //设置为多例
public QueryRunner createQueryRunner(DataSource dataSource){
return new QueryRunner(dataSource);
}
/**
* 用于创建一个 DataSource 对象
* @return
*/
@Bean(name = "dataSource")
public DataSource createDataSource(){
ComboPooledDataSource ds = new ComboPooledDataSource();
try {
ds.setDriverClass(driver);
ds.setJdbcUrl(url);
ds.setUser(username);
ds.setPassword(password);
} catch (PropertyVetoException e) {
e.printStackTrace();
}
return ds;
}
}
5.Spring的AOP–Aspect Oriented Programming(面向切面编程)
1.代理
静态代理,动态代理(基于JDK,基于CGlib),AOP详述
2.AOP
AOP是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高开发的效率。
作用:在程序运行期间,不修改源码对已有方法进行增强
优势:减少重复代码;提高开发效率;维护方便
实现方式:使用基于JDK和CGlib的动态代理技术
Spring 中的 AOP:通过配置的方式,减少编写动态代理的繁琐代码
AOP的基本术语:
1.切面(Aspect)
切面就是在一个怎么样的环境中工作。比如数据库的事务直接贯穿了整个代码层面,这就是一个切面,它能够在被代理对象的方法之前、之后,产生异常或者正常返回后切入你的代码,甚至代替原来被代理对象的方法,在动态代理中可以把它理解成一个拦截器。
2.切(入)点(Pointcut)
在动态代理中,被切面拦截的方法就是一个切点,切面将可以将其切点和被拦截的方法按照一定的逻辑织入到约定流程当中。
3.连接点(Join point)
连接点是一个判断条件,由它可以指定哪些是切点。对于指定的切点,Spring会生成代理对象去使用对应的切面对其拦截,否则就不会拦截它。
4.通知(Advice)
通知是切面开启后(切面的工作被称为通知),切面的方法。它根据在代理对象真实方法调用前、后的顺序和逻辑区分。
通知分类:
1)前置通知(Before):在切入点方法执行之前执行
2)后置通知(After-returning):无论切入点方法成功执行之后执行
3)异常通知(After-throwing):在切入点方法执行产生异常之后执行
4)最终通知(After):在切入点方法完成之后执行。后置通知和异常通知之间永远只能执行其中一个(finally)
5)环绕通知(Around):环绕通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
#通知描述了切面要完成的工作,解决了何时执行这个工作的问题;切点解决了何处执行这个工作的问题;切面则是通知和切点的结合。通知和切点共同定义了切面的全部内容--它在何时、何处完成其什么工作。
5.引入(Introduction)
引入允许我们向现有的类添加新方法或属性。
6.织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。
在目标对象的生命周期里有多个点可以进行织入:
1)编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
2)类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5的加载时织入(load-timeweaving,LTW)就支持以这种方式织入切面。
3)运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。
7.切入点表达式
语法(关键字):execution(表达式)
表达式组成:访问修饰符 返回值 包名.包名...类名.方法名(参数列表)
示例:对AccountServiceImpl中的saveAccount方法进行增强,其切入点表达式为
public void com.simple.service.impl.AccountServiceImpl.saveAccount()
通配符表达式:
1.访问修饰符可以省略
void com.simple.service.impl.AccountServiceImpl.saveAccount()
2.返回值可以使用通配符,表示对应任意返回值类型
* com.simple.service.impl.AccountServiceImpl.saveAccount()
3.包名可以使用通配符,表示任意包,但需要注意有几级包,就需要写几个*
* *.*.*.*.AccountServiceImpl.saveAccount()
4.包名可以使用*..表示当前包及其子包:
* *..AccountServiceImpl.saveAccount()
5.类名和方法名都可以使用*实现通配:
* *..*.saveAccount() 任意包下的saveAccount方法
* *..*.*() 任意包下的无参方法
6.方法参数:
可以直接写数据类型
基本类型直接写名称:int
引用类型写包名.类名的形式:java.lang.String
可以使用通配符*表示任意类型,但必须有参数
可以使用..表示有无参数均可,有参数可为任意类型
7.全调配写法:选择对所有任意方法进行增强
* *..*.*(..)
实际开发中切入点表达式的通常写法:
切入到业务层实现类下的所有方法:* com.simple.service.impl.*.*(..)
AOP并不是 Spring 框架特有的,Spring 只是支持 AOP 编程的框架之一。每一个框架对 AOP 的支持各有特点:有些AOP能够对方法的参数进行拦截,有些AOP对方法进行拦截。
Spring AOP是一种基于方法拦截的AOP,即 Spring 只能支持方法拦截的 AOP。在 Spring 中有4种方法实现 AOP 的拦截功能:
- 基于代理的经典Spring AOP
- 纯POJO切面(通过xml配置,声明式地将对象转换为切面)
- @AspectJ注解驱动的切面(注解驱动的AOP)
- 注入式AspectJ切面(适用于Spring各版本)
在Spring AOP的拦截方式中,真正常用的是用@AspectJ注解的方式实现的切面,有时候XML配置也有一定的辅助作用。对于基于代理的经典Spring AOP和注入式AspectJ切面这两种方式已经很少用了。
3.基于xml配置的AOP
Spring 中基于xml的AOP配置:
1.把需要通知的bean交给 ioc 容器管理
2.使用 aop:config 标签表明开始 AOP 配置(在 xmlns:aop 的命名空间下)
3.使用 aop:aspect 标签表明配置切面的通知
切面类:
/**
* 用于记录日志的工具类,提供了公共的代码
*/
public class Logger {
/**
* 前置通知
*/
public void beforePrintLog(){
System.out.println("前置通知!!!!!!!!!!!!!");
}
/**
* 后置通知
*/
public void afterReturningPrintLog(){
System.out.println("后置通知!!!!!!!!!!!!!");
}
/**
* 异常通知
*/
public void afterThrowingPrintLog(){
System.out.println("异常通知!!!!!!!!!!!!!");
}
/**
* 最终通知
*/
public void afterPrintLog(){
System.out.println("最终通知!!!!!!!!!!!!!");
}
/**
* 环绕通知:关键在于可以手动控制增强被代理对象的方法
* 问题:
* 当配置了环绕通知后,切入点方法并没有执行,而是环绕通知方法替代它执行了
* 分析:
* 对比动态代理中的环绕通知代码,发现动态代理中的环绕通知有明确的切入点方法调用,而直接配置环绕通知则没有
* 解决:
* spring框架提供了一个接口:ProceedingJoinPoint,该接口有一个方法proceed(),此方法就相当于明确调用切入点方法
* ProceedingJoinPoint接口可以作为环绕通知的方法参数;在程序执行时,spring框架会提供该接口的实现类供使用
* spring中的环绕通知:
* 它是spring框架提供的一种可以在代码中手动控制增强方法何时执行的方式
*/
public Object aroundPrintLog(ProceedingJoinPoint proceedingJoinPoint){
Object rtValue = null;
try {
System.out.println("环绕通知--前置通知!!!!!!!!!!!!!");
//得到切入点方法执行所需要的参数
Object[] args = proceedingJoinPoint.getArgs();
//明确调用切入点方法(可带有参数),可带有返回值
rtValue = proceedingJoinPoint.proceed();
System.out.println("环绕通知--后置通知!!!!!!!!!!!!!");
} catch (Throwable throwable) {
System.out.println("环绕通知--异常通知!!!!!!!!!!!!!");
throwable.printStackTrace();
}finally {
System.out.println("环绕通知--最终通知!!!!!!!!!!!!!");
}
return rtValue;
}
}
xml配置:
<!-- 配置Ioc:配置Service对象 -->
<bean id="accountService" class="com.simple.service.impl.AccountServiceImpl"></bean>
<!-- 配置Logger对象(通知类) -->
<bean id="logger" class="com.simple.utils.Logger"></bean>
<!-- 配置aop -->
<aop:config>
<!-- 配置切入点表达式:
id用于指定表达式的唯一标识,expression用于指定切入点表达式
此标签写在aop:aspect标签内只能当前切面使用;写在aop:aspect外,则全部切面可用(注意,需要写在所有切面之前,即所有的aop:aspect标签之前)
-->
<aop:pointcut id="pt1" expression="execution(* com.simple.service.impl.*.*(..))"/>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置前置通知:在切入点方法执行之前执行
<aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before> -->
<!-- 配置后置通知:在切入点方法正常执行之后执行。后置通知和异常通知之间永远只能执行其中一个
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning> -->
<!-- 配置异常通知:在切入点方法执行产生异常之后执行
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing> -->
<!-- 配置最终通知:无论切入点方法是否正常执行它都会在其后面执行
<aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after> -->
<!-- 配置环绕通知: -->
<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
上述示例中,AccountServiceImpl 是动态代理中的被代理类(委托类),Logger 是代理类。
在效果上,前置+后置+异常+最终通 == 环绕 。
4.基于注解配置的AOP
配置AOP的常用注解:
@Aspect 表明该类(使用在类上)不仅仅是一个POJO,还是一个切面类
@Pointcut 定义一个切入点表达式,示例:
@Pointcut("execution(切面表达式)")
public void pt(){}
该方法的方法名即为该切点表达式的名称,引用该切点表达式时,只需要引用该方法即可
有参数传递时(&& args(参数)) ,public void pt(参数){}
该方法本身只是一个标识,供@Pointcut注解依附
@Before 前置通知
@AfterReturning 后置通知
@AfterThrowing 异常通知
@After 最终通知
@Around 环绕通知
这些通知注解后面的括号内放切入点表达式,如:@Around("pt1()")
@EnableAspectJAutoProxy 在配置类中表示启用自动代理(开启注解版的AOP功能)
<aop:aspectj-autoproxy> xml启用自动代理的标签
切面类:
@Component("logger")
@Aspect //表示当前类是一个切面类
public class Logger {
/**
* 切入点表达式
*/
@Pointcut("execution(* com.simple.service.impl.*.*(..))")
public void pt1(){}
/**
* 前置通知
*/
//@Before("pt1()")
public void beforePrintLog(){
System.out.println("前置通知!!!!!!!!!!!!!");
}
/**
* 后置通知
*/
//@AfterReturning("pt1()")
public void afterReturningPrintLog(){
System.out.println("后置通知!!!!!!!!!!!!!");
}
/**
* 异常通知
*/
//@AfterThrowing("pt1()")
public void afterThrowingPrintLog(){
System.out.println("异常通知!!!!!!!!!!!!!");
}
/**
* 最终通知
*/
//@After("pt1()")
public void afterPrintLog(){
System.out.println("最终通知!!!!!!!!!!!!!");
}
/**
* 使用注解配置通知时,一般使用环绕通知,而不是前面四种通知;因为在注解通知中,前面四种通知的执行顺序容易出问题
* 别忘记调用proceed()方法。如果不调这个方法的话,那么你的通知实际上会阻塞对被通知方法的调用(有可能这就是你想要的效果)
* 关于ProceedingJoinPoint参数:在通知中通过它来调用被通知的方法(ProceedingJoinPoint的proceed()方法)
*/
@Around("pt1()")
public Object aroundPrintLog(ProceedingJoinPoint proceedingJoinPoint){
Object rtValue = null;
try {
System.out.println("环绕通知--前置通知!!!!!!!!!!!!!");
//得到切入点方法执行所需要的参数
Object[] args = proceedingJoinPoint.getArgs();
//明确调用切入点方法(可带有参数),可带有返回值
rtValue = proceedingJoinPoint.proceed();
System.out.println("环绕通知--后置通知!!!!!!!!!!!!!");
} catch (Throwable throwable) {
System.out.println("环绕通知--异常通知!!!!!!!!!!!!!");
throwable.printStackTrace();
}finally {
System.out.println("环绕通知--最终通知!!!!!!!!!!!!!");
}
return rtValue;
}
}
配置类+xml配置:
@Configuration
@ComponentScan(basePackages = "com.simple") //扫描的包
@EnableAspectJAutoProxy //开启注解版的AOP功能
public class AopConfig {
}
<!-- 配置spring创建容器时需要扫描的包(也可以使用一个配置类) -->
<context:component-scan base-package="com.simple"></context:component-scan>
<!-- 配置spring开启注解aop的支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
6.Spring中的 JdbcTemplate
7.Spring中的声明式事务管理(tx)
1.事务概念
事务管理是企业级应用程序开发中必不可少的技术,用来确保数据的完整性和一致性。
事务就是一系列的动作,它们被当做一个单独的工作单元。这些动作要么全部完成,要么全部不起作用。
事务的四个关键属性(ACID):
- 原子性(atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用
- 一致性(consistency):一旦事务所有动作完成,事务就别提交。数据和资源就处于一种满足业务规则的一致性状态中
- 隔离性(isolation):可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开,防止数据损坏
- 持久性(durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响。通常情况下,事务的结果被写到持久化存储器中
Spring既支持编程式事务管理,也支持声明式事务管理:
编程式事务管理:将事务管理代码嵌入到业务方法中来控制事务的提交和回滚,在编程式事务中,必须在每个业务操作中包含额外的事务管理代码。
**声明式事务管理:**大多数情况下比编程式事务管理更好用。它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。事务管理作为一种横切关注点,可以通过AOP方法模块化。Spring通过Spring AOP框架支持声明式事务管理。
事务的隔离级别:
隔离级别是指若干个并发的事务之间的隔离程度。TransactionDefinition 接口中定义了五个表示隔离级别的常量:
ISOLATION_DEFAULT
这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是TransactionDefinition.ISOLATION_READ_COMMITTED。ISOLATION_READ_UNCOMMITTED
该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读和不可重复读,因此很少使用该隔离级别。ISOLATION_READ_COMMITTED
该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。ISOLATION_REPEATABLE_READ
该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读。ISOLATION_SERIALIZABLE
所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
事务传播行为:
所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在TransactionDefinition定义中包括了如下几个表示传播行为的常量:
TransactionDefinition.PROPAGATION_REQUIRED
:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。它是默认的传播属性。TransactionDefinition.PROPAGATION_REQUIRES_NEW
:创建一个新的事务,如果当前存在事务,则把当前事务挂起。TransactionDefinition.PROPAGATION_SUPPORTS
:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。TransactionDefinition.PROPAGATION_NOT_SUPPORTED
:以非事务方式运行,如果当前存在事务,则把当前事务挂起。TransactionDefinition.PROPAGATION_NEVER
:以非事务方式运行,如果当前存在事务,则抛出异常。TransactionDefinition.PROPAGATION_MANDATORY
:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。TransactionDefinition.PROPAGATION_NESTED
:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
这里需要指出的是,前面的六种事务传播行为是 Spring 从 EJB 中引入的,他们共享相同的概念。而 PROPAGATION_NESTED是 Spring 所特有的。以 PROPAGATION_NESTED 启动的事务内嵌于外部事务中(如果存在外部事务的话),此时,内嵌事务并不是一个独立的事务,它依赖于外部事务的存在,只有通过外部的事务提交,才能引起内部事务的提交,嵌套的子事务不能单独提交。如果熟悉 JDBC 中的保存点(SavePoint)的概念,那嵌套事务就很容易理解了,其实嵌套的子事务就是保存点的一个应用,一个事务中可以包括多个保存点,每一个嵌套子事务。另外,外部事务的回滚也会导致嵌套子事务的回滚。
事务超时(timeOut):
所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒。
事务的只读属性(readOnly=true):
事务的只读属性是指,对事务性资源进行只读操作或者是读写操作。所谓事务性资源就是指那些被事务管理的资源,比如数据源、 JMS 资源,以及自定义的事务性资源等等。如果确定只对事务性资源进行只读操作,那么我们可以将事务标志为只读的,以提高事务处理的性能。在 TransactionDefinition 中以 boolean 类型来表示该事务是否只读。
事务的回滚规则:
通常情况下,如果在事务中抛出了未检查异常(继承自 RuntimeException 的异常),则默认将回滚事务。如果没有抛出任何异常,或者抛出了已检查异常,则仍然提交事务。这通常也是大多数开发者希望的处理方式,也是 EJB 中的默认处理方式。但是,我们可以根据需要人为控制事务在抛出某些未检查异常时仍然提交事务,或者在抛出某些已检查异常时回滚事务。
并发事务所导致的问题:
在同一个应用程序或者不同应用程序中的多个事务在同一个数据集上并发执行时,可能会出现许多意外的问题。
并发事务所导致的问题可以分为以下三类:
- 脏读:脏读发生在一个事务读取了另一个事务改写但尚未提交的数据时。如果改写在稍后被回滚了,那么第一个事务获取的数据就是无效的。
- 不可重复读:不可重复读发生在一个事务执行相同的查询两次或两次以上,但是每次都得到不同的数据时。这通常是因为另一个并发事务在两次查询期间更新了数据
- 幻读:幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录
2.xml配置实现声明式事务管理
AccountDaoImpl 类:
/**
* 账户的持久层实现类
*
* JdbcDaoSupport父类是spring框架中存在的用于简化JdbcTemplate的(不需要额外创建,但是原理是一样的)
*
* 若继承了JdbcDaoSupport则一般使用xml配置
*/
public class AccountDaoImpl extends JdbcDaoSupport implements IAccountDao {
/**
//在多个dao实现类的情况下,容易出现重复代码,可编写一个父类整合重复代码
private JdbcTemplate jdbcTemplate;
//注入
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
*/
public Account findAccountById(Integer id) {
List<Account> accounts = super.getJdbcTemplate().query("select * from account where id = ?",new BeanPropertyRowMapper<Account>(Account.class),id);
return accounts.isEmpty()?null:accounts.get(0);
}
public Account findAccountByName(String name) {
List<Account> accounts = super.getJdbcTemplate().query("select * from account where name= ?",new BeanPropertyRowMapper<Account>(Account.class),name);
if (accounts.isEmpty()) {
return null;
}
if(accounts.size()>1){
throw new RuntimeException("结果集不唯一");
}
return accounts.get(0);
}
public void updateAccount(Account account) {
super.getJdbcTemplate().update("update account set name = ?,money = ? where id = ?",account.getName(),account.getMoney(),account.getId());
}
}
AccountServiceImpl 类:
/**
* 账户的业务层实现类:初始的,供给动态代理进行代理
*/
public class AccountServiceImpl implements IAccountService {
private IAccountDao accountDao;
public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}
public Account findAccountById(Integer id) {
return accountDao.findAccountById(id);
}
public void transfer(String sourceName, String targetName, Float money) {
//2.1.根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
//2.2.根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
//2.3.转出账户减钱
source.setMoney(source.getMoney()-money);
//2.4.转入账户加钱
target.setMoney(target.getMoney()+money);
//2.5.更新转出账户
accountDao.updateAccount(source);
//若出现中断,则事务出现问题(减钱的减了,加钱的没加)
//int n = 1/0;
//2.6.更新转入账户
accountDao.updateAccount(target);
}
}
xml配置:
<!-- 配置数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/test?serverTimezone=UTC"></property>
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
</bean>
<!-- 配置dao实现类 -->
<bean id="accountDao" class="com.simple.dao.impl.AccountDaoImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置service实现类 -->
<bean id="accountService" class="com.simple.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
</bean>
<!-- spring中基于xml的声明式事务控制配置:
1.配置事务管理器
2.配置事务的通知(此前需要导入事务的约束 xmlns:tx命名空间和约束)
tx:advice 标签配置事务通知
属性:
id:该事务通知的唯一标识
transaction-manager:给事务通知提供一个事务管理器引用
3.配置aop
配置通用切入点表达式
4.建立事务通知和切入点表达式的对应关系
5.配置事务的属性
在事务的通知tx:advice标签内部
-->
<!-- 配置事务管理器:spring内部的事务管理器有事务提交和事务回滚两种方法 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事务的通知和事务管理器 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 配置事务的属性:
name:方法的名称
isolation:用于指定事务的隔离级别。默认值是DEFAULT,表示使用数据库的默认隔离级别
propagation:用于指定事务的传播行为。默认值是REQUIRED,表示一定会有事务,是增删改的选择;查询一般选择SUPPORTS
read-only:用于指定事务是否只读,只有查询才能设置为true,默认值是false,表示可以读写
timeout:用于指定事务的超时时间。默认值是-1,表示永不超时;如果指定了数值,则以秒为单位
rollback-for:用于指定一个异常,当产生该异常时,事务回滚,产生其他异常时,事务不回滚。默认表示任何异常都回滚
no-rollback-for:用于指定一个异常,当产生该异常时,事务不回滚,产生其他异常时,事务回滚。默认表示任何异常都回滚
-->
<tx:attributes>
<!-- <tx:method name="transfer" propagation="REQUIRED" read-only="false"/> -->
<!-- 通配符对所有的增删改指定propagation为REQUIRED,所有的查询(find开头的方法)指定propagation为SUPPORTS -->
<tx:method name="*" propagation="REQUIRED" ></tx:method>
<tx:method name="find*" propagation="SUPPORTS"></tx:method>
</tx:attributes>
</tx:advice>
<!-- 配置aop -->
<aop:config>
<!-- 配置通用切入点表达式 -->
<aop:pointcut id="pt1" expression="execution(* com.simple.service.impl.*.*(..))"/>
<!-- 建立事务通知和切入点表达式的对应关系 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor>
</aop:config>
3.注解配置实现声明式事务管理
常用注解:
@EnableTransactionManagement 开启事务注解的支持
@Transactional(propagation = Propagation.SUPPORTS,readOnly = true) //只读型事务的配置(查找类方法),此类中所有的事务配置为只读型事务
@Transactional(propagation = Propagation.REQUIRED,readOnly = false) //读写型事务的配置(增删改),更改此方法为读写型事务
@Transactional 可以作用于接口、接口方法、类以及类方法上。当作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。
AccountDaoImpl 类:
@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
//使用注解只能单独定义一个JdbcTemplate,而不能继承JdbcDaoSupport父类(无法注入)
@Autowired
private JdbcTemplate jdbcTemplate;
public Account findAccountById(Integer id) {
List<Account> accounts = jdbcTemplate.query("select * from account where id = ?",new BeanPropertyRowMapper<Account>(Account.class),id);
return accounts.isEmpty()?null:accounts.get(0);
}
public Account findAccountByName(String name) {
List<Account> accounts = jdbcTemplate.query("select * from account where name= ?",new BeanPropertyRowMapper<Account>(Account.class),name);
if (accounts.isEmpty()) {
return null;
}
if(accounts.size()>1){
throw new RuntimeException("结果集不唯一");
}
return accounts.get(0);
}
public void updateAccount(Account account) {
jdbcTemplate.update("update account set name = ?,money = ? where id = ?",account.getName(),account.getMoney(),account.getId());
}
}
AccountServiceImpl 类:
@Service("accountService")
@Transactional(propagation = Propagation.SUPPORTS,readOnly = true) //只读型事务的配置(查找类方法),此类中所有的事务配置为只读型事务
public class AccountServiceImpl implements IAccountService {
@Autowired
private IAccountDao accountDao;
public Account findAccountById(Integer id) {
return accountDao.findAccountById(id);
}
@Transactional(propagation = Propagation.REQUIRED,readOnly = false) //读写型事务的配置(增删改),更改此方法为读写型事务
public void transfer(String sourceName, String targetName, Float money) {
//2.1.根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
//2.2.根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
//2.3.转出账户减钱
source.setMoney(source.getMoney()-money);
//2.4.转入账户加钱
target.setMoney(target.getMoney()+money);
//2.5.更新转出账户
accountDao.updateAccount(source);
//若出现中断,则事务出现问题(减钱的减了,加钱的没加),检测事务
//int n = 1/0;
//2.6.更新转入账户
accountDao.updateAccount(target);
}
}
xml配置:
<!-- 配置spring创建容器时要扫描的包 -->
<context:component-scan base-package="com.simple"></context:component-scan>
<!-- 配置数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/test?serverTimezone=UTC"></property>
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
</bean>
<!-- 配置JdbcTemplate对象注入 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- spring中基于注解的声明式事务控制配置:
1.配置事务管理器
2.开启spring对注解事务的支持,指定事务管理器
3.在需要事务支持的类使用 @Transactional 注解:事务的属性在该注解中配置
-->
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 开启spring对注解事务的支持,指定事务管理器 -->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>