Spring之AOP
AOP 全程Aspect Oriented Programming,直译就是面向切面编程。和POP、OOP相似,它也是一种编程思想。OOP强调的是封装、继承、多态,也就是功能的模块化。而AOP则是OOP的补充,它强调的是切面,在运行时动态地将代码切入到类的指定方法、指定位置上的编程思想,也就是将业务代码和业务前后的代码分离出来(解耦),将日志、权限验证等功能抽取出来然后重用。
在Spring中,采用动态代理的方式来表达AOP。(并非所有的AOP都是使用动态代理来,比如AspectJ采用编译时创建代理对象,比运行时创建效率更高)
动态代理一般有两种实现方式,一种是JDK原生动态代理,要求被代理对象必须实现接口,并且只能代理接口中的方法(本质是创建一个实现接口的代理对象)。另一种是CGlib,可以代理所有的方法(本质是创建一个代理对象,继承被代理对象)。Spring中使用的是CGlib的方式。
废话不多说,直接介绍Spring中的AOP。
Spring AOP相关术语
Joinpoint
(连接点):任何可以被增强的方法,都称为连接点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点。Pointcut
(切入点):将要被增强的方法。即我们要对哪些Joinpoint进行拦截的定义。Advice
(通知/增强):所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。Introduction
(引介):Adivice是对方法进行增强的,而Introdution是针对类进行增强的Target
(目标对象):被代理的目标对象。Weaving
(织入):是指把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。Proxy
(代理):一个对象被jdk代理或者cglib代理后的对象,称为代理Aspect
(切面):多个通知和切入点的配置关系。
基于xml的AOP配置
首先是依赖:
AOP依赖
<!--IOC相关依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
<version>4.12</version>
</dependency>
<!-- AOP相关依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<!-- spring集合junit的依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
然后配置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"
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/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 待增强的bean -->
<bean id="aspectService" class="com.bilibili.service.impl.AspectServiceImpl"></bean>
<!-- 增强的功能bean -->
<bean id="logger" class="com.bilibili.common.Logger"></bean>
<!-- 配置AOP -->
<aop:config>
<!-- 配置切面
id:唯一标识
ref:引用的通知类(bean id)
-->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置前置通知
method:配置通知的方法(即增强的功能)
pointcut:配置切面,也就是对哪个方法进行增强(使用AspectJ表达式)
execution:使用AspectJ切入点表达式
-->
<aop:before method="printLog" pointcut="execution(public void com.bilibili.service.impl.AspectServiceImpl.update())"></aop:before>
</aop:aspect>
</aop:config>
</beans>
execution表达式的匹配方式:
execution:匹配方法的执行(常用)
execution(表达式)
表达式语法:execution([修饰符] 返回值类型 包名.类名.方法名(参数))
写法说明:
全匹配方式:
public void cn.bilibili.service.impl.AccountServiceImpl.saveAccount(cn.bilibili.domain.Account)
访问修饰符可以省略
void cn.bilibili.service.impl.AccountServiceImpl.saveAccount(cn.bilibili.domain.Account)
返回值可以使用*号,表示任意返回值
* cn.bilibili.service.impl.AccountServiceImpl.saveAccount(cn.bilibili.domain.Account)
包名可以使用*号,表示任意包,但是有几级包,需要写几个*
* *.*.*.*.AccountServiceImpl.saveAccount(cn.bilibili.domain.Account)
使用..来表示当前包,及其子包
* cn..AccountServiceImpl.saveAccount(cn.bilibili.domain.Account)
类名可以使用*号,表示任意类
* cn..*.saveAccount(cn.bilibili.domain.Account)
方法名可以使用*号,表示任意方法
* cn..*.*( cn.bilibili.domain.Account)
参数列表可以使用*,表示参数可以是任意数据类型,但是必须有参数
* cn..*.*(*)
参数列表可以使用..表示有无参数均可,有参数可以是任意类型
* cn..*.*(..)
全通配方式:
* *..*.*(..)
注:
通常情况下,我们都是对业务层的方法进行增强,所以切入点表达式都是切到业务层实现类。
execution(* cn.bilibili.service.impl.*.*(..))
注意:多个execution可以使用 || && 连接
AOP 常用标签
<aop:config>
:用于声明开始aop配置<aop:aspect>
:切面
属性:id
:给切面提供一个唯一标识。ref
:引用配置好的通知类bean的id
<aop:point>
:切点,方便一个切点多次使用
属性:id
:切点的唯一标识expression
:定义切点的表达式
<aop:aspect>
:前置通知
属性:method
:通知的方法(即增强的功能)pointcut
:AspectJ表达式pointcut-ref
:引用切点(和pointcut不可同时使用)
<aop:after-returning>
:后置通知<aop:after-throwing>
:异常通知<aop:after>
:最终通知(相当于finally)<aop:around>
:环绕通知,一般单独使用,该通知(增强的方法)接收一个类型为ProceedingJoinPoint
的参数,该类型有一个proceed()
方法,用来调用被代理方法。
基于注解的AOP配置
在主配置文件中开启包扫描和注解AOP:
<!-- 开启注解扫描的包 -->
<context:component-scan base-package="com.bilibili"></context:component-scan>
<!-- 开启注解AOP -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
把被代理对象注册到容器中:
AccountServiceImpl类
@Service("accountService")
public class AccountServiceImpl implements AccountService {
public void update() {
System.out.println("更新操作");
}
public void save() {
System.out.println("保存操作");
}
public void delete() {
System.out.println("删除操作");
}
}
在通知类上添加@Component()
进行注册,@Aspect
表示切面类。方法上添加@Before()
前置通知、@AfterReturning()
后置通知、@AfterThrowing()
异常通知、@After()
最终通知。@Pointcut()
注解空方法表示切面。
通知类
@Component("logger")
@Aspect//声明当前是一个切面类(通知类)
public class Logger {
//注解前置通知,value属性就是切点的AspectJ表达式
@Before("execution(* com.bilibili.service.impl.AccountServiceImpl.update())")
public void beforePrintLog(){
System.out.println("<aop:before>标签配置前置通知,即增强的功能在目标方法之前");
}
//切面
@Pointcut("execution(* com.bilibili.service.impl.AspectServiceImpl.update())")
public void pt1() {
}
//注解后置通知,引用切面
@AfterReturning("pt1()")
public void afterReturningPrintLog(){
System.out.println("<aop:after-returning>标签配置后置通知,即增强的功能在目标方法之后");
}
//下面就不一个一个标注了。
public void afterThrowingPrintLog(){
System.out.println("<aop:after-throwing>标签配置异常通知,即目标方法出现异常的时候执行");
}
public void afterPrintLog(){
System.out.println("<aop:after>标签配置最终通知。即不管是否出现异常,都会执行,类似finally");
}
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object obj = null;
try {
System.out.println("环绕通知,手动在代码中定义何时执行");
obj = pjp.proceed();//目标方法执行
System.out.println("环绕通知,手动在代码中定义何时执行");
} catch (Throwable throwable) {
System.out.println("环绕通知,手动在代码中定义何时执行");
throwable.printStackTrace();
}finally {
System.out.println("环绕通知,手动在代码中定义何时执行");
}
return obj;
}
}
纯注解配置
只需在IoC的纯注解配置类上添加@EnableAspectJAutoProxy()
开启AOP即可。
注解AOP
//声明当前类是一个spring的配置类,用来替代xml配置文件
//获取容器时需要使用AnnotationApplicationContext(@Configuration标注的类.class)
@Configuration
//用于配置容器初始化时需要扫描的包
//和xml配置中<context:component-scan base-package="com.bilibili"/>作用一致
@ComponentScan("com.bilibili")
//导入其他配置类
@Import(JdbcConfig.class)
//开启AOP
@EnableAspectJAutoProxy
public class SpringConfig {
}
JdbcDaoSupport
继承该类后可以不用手动获取JdbcTemplate对象。
- dao层的实现类只需要继承JdbcDaoSupport,然后通过getJdbcTemplate()方法获取jdbcTemplate对象
- 在spring的applicationContext.xml中,只需要给dao的实现类注入dataSource数据源即可。因为JdbcDaoSupport中的setDataSource()方法自动创建jdbcTemplate对象。
使用这种方式无法用注解注入DataSource,只能通过xml注入(注入给子类也可以)
Spring 事务
事务处理位于业务层,Spring提供了一个spring-tx包来进行控制事务,事务是基于AOP,原理也比较好理解。
PlatformTransactionManager
PlatformTransactionManager:平台事务管理器,是Spring真正管理事务的对象,是一个接口,常用实现类有如下两个:
- DataSourceTransactionManager:针对JDBC和mybatis事务管理
- HibernateTransactionManager:针对Hibernate事务管理
Spring主要通过两个重要的接口来描述一个事务:
- TransactionDefinition:事务定义的对象,用来定义事务的隔离级别、传播行为、是否只读、超时信息等等
- TransactionStatus:事务状态信息的对象,用来获取事务是否保存、是否完成等。
Spring框架进行事务的管理,首先使用TransactionDefinition对事务进行定义。通过PlatformTransactionManager根据TransactionDefinition的定义信息进行事务的管理。在事务管理过程中产生一系列的状态:保存到TransactionStatus中。
TransactionDefinition接口具有以下常用方法:
String getName()
:获取事务对象名称int getIsolationLevel()
:获取事务隔离级别int getPropagationBehavior()
:获取事务传播行为int getTimeout()
:获取事务超时时间boolean isReadOnly()
获取事务是否只读
事务隔离级别:
ISOLATION_DEFAULT
:默认级别,会根据不同数据库自动变更(MySQL为可重复读,Oracle和Access为读已提交)ISOLATION_READ_UNCOMMITTED
:读未提交(会产生脏读)ISOLATION_READ_COMMITTED
:读已提交(解决脏读)ISOLATION_REPEATABLE_READ
:可重复读ISOLATION_SERIALIZABLE
:串行化
事务的传播行为
传播行为解决的问题: 一个业务层事务 调用 另一个业务层事务时,事务之间关系如何处理
事务传播行为PROPAGATION的取值:
REQUIRED 支持当前事务,如果不存在,就新建一个(默认的传播行为)
* 删除客户 删除订单, 处于同一个事务,如果 删除订单失败,删除客户也要回滚
SUPPORTS 支持当前事务,如果不存在,就不使用事务
MANDATORY 支持当前事务,如果不存在,抛出异常
REQUIRES_NEW 如果有事务存在,挂起当前事务,创建一个新的事务
* 生成订单, 发送通知邮件, 通知邮件会创建一个新的事务,如果邮件失败, 不影响订单生成
NOT_SUPPORTED 以非事务方式运行,如果有事务存在,挂起当前事务
NEVER 以非事务方式运行,如果有事务存在,抛出异常
NESTED 如果当前事务存在,则嵌套事务执行
* 依赖于 JDBC3.0 提供 SavePoint 技术
* 删除客户 删除订单, 在删除客户后, 设置SavePoint, 执行删除订单,删除订单和删除客户在同一个事务 ,删除部分订单失败, 事务回滚 SavePoint , 由用户控制是事务提交 还是 回滚
三个代表:
REQUIRED 一个事务, 要么都成功,要么都失败
REQUIRES_NEW 两个不同事务,彼此之间没有关系 一个事务失败了 不影响另一个事务
NESTED 一个事务, 在A事务 调用 B过程中, B失败了, 回滚事务到 之前SavePoint , 用户可以选择提交事务或者回滚事务
超时时间:默认值是-1,没有超时限制。如果有,以秒为单位进行设置。
是否是只读事务:建议查询时设置为只读。
TransactionStatus:事务的运行状态,常用方法如下:
void flush()
:刷新事务boolean hasSavePoint()
:是否存在存储点boolean idComplated()
:事务是否完成boolean isNewTransaction()
:是否为新事物boolean isRollbackOnly()
:事务是否回滚void setRollbackOnly()
设置事务回滚
xml方式配置事务
首先添加依赖:
一堆依赖
主要是spring-tx、spring-aspects这两个不要漏了。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.9</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
配置事务管理器的bean
<!--
bean的名字叫做transactionManager,因为在配置事务策略的时候需要指定的事务管理器的默认名字就是transactionManager,如果是其他名字,在配置事务策略的时候,需要手动指定。
-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
配置事务策略:
<!-- 配置事务策略 -->
<tx:advice id="tx">
<tx:attributes>
<!--
指定对那些方法使用事务
name:需要进行事务管理的方法名 *代表所有方法,这里需要填方法的名字即可,不是aspectj那种包名加类名方法名。
isolation:事务隔离级别
propagation:事务传播行为
timeout:超时时间
ready-only:设置事务是否只读
rollback-for:指定对哪种异常进行回滚
no-rollback-for:指定对那种异常不进行回滚
-->
<tx:method name="*" />
</tx:attributes>
</tx:advice>
配置事务AOP:
<!-- 配置aop -->
<aop:config>
<aop:pointcut id="pt1" expression="execution(* com.bilibili.service.impl.*.*(..))"></aop:pointcut>
<!-- 配置事务策略运用到事务管理器 -->
<aop:advisor advice-ref="tx" pointcut-ref="pt1"></aop:advisor>
</aop:config>
注解AOP
在spring主配置文件中:
<!-- 开启spring的注解扫描 -->
<context:component-scan base-package="com.bilibili"></context:component-scan>
<!-- 开启事务的注解扫描 -->
<tx:annotation-driven></tx:annotation-driven>
然后只在方法或者类或者接口上配置@Transactional
即可开启事务
纯注解配置
在配置类上添加@EnableTransactionManagement
开启注解事务管理:
纯注解配置类
@Configuration //声明当前是一个配置类,用来代替applicationContext.xml文件
@ComponentScan("com.bilibili") //开启注解包扫描
@PropertySource("classpath:jdbc.properties") // 加载外部配置文件
@EnableTransactionManagement // 开启注解事务管理
public class SpringConfig {
@Value("${jdbc.url}")//引入外部配置文件中的资源
private String url;
@Value(("${jdbc.driverClass}"))
private String driverClass;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean("dataSource")//将bean装配到spring容器中
public DataSource getDataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setDriverClassName(driverClass);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
@Bean("jdbcTemplate")
public JdbcTemplate getJdbcTemplate(@Qualifier("dataSource") DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
return jdbcTemplate;
}
@Bean("transactionManager")
public DataSourceTransactionManager getDataSourceTransactionManager(@Qualifier("dataSource") DataSource dataSource){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource);
return dataSourceTransactionManager;
}
}
Spring与Web之监听器
在Tomcat中,由于Servlet是Tomcat创建的,无法放入Spring中,当Servlet需要使用Service的时候是不太方便的,此时就可以使用监听器来自动创建ApplicationContext。
spring监听器原理:监听servletContext创建,创建ApplicationContext并将其放入上下文域。
自己实现:
创建一个servletContext监听器
@WebListener()
public class MyListener implements ServletContextListener {
/**
* 在ServletContext对象创建的时候,创建spring容器。
* 1.创建spring容器,需要配置文件的名字,名字并不是固定的,所以可以配置在web.xml中
* 2.spring容器创建之后,需要能够被所有的servlet来使用,那么需要将spring容器保存起来,保存到哪里?ServletContext域对象中
*
*/
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
//获取servletContext域对象
ServletContext servletContext = servletContextEvent.getServletContext();
//读取web.xml中的配置参数 -- 即spring的核心配置文件的名字
String contextConfig = servletContext.getInitParameter("contextConfig");
//创建spring容器
ApplicationContext ac = new ClassPathXmlApplicationContext(contextConfig);
//将spring容器保存到servletContext对象中
servletContext.setAttribute("ac",ac);
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
}
}
web.xml中配置spring配置文件位置:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<!-- 配置spring核心配置文件的名字 -->
<context-param>
<param-name>contextConfig</param-name>
<param-value>applicationContext.xml</param-value>
</context-param>
</web-app>
然后在servlet中就可以获取servletContext中保存的ApplicationContext了。
使用Spring的监听器:
引入依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
web.xml中配置spring主文件位置和监听器:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<!-- spring核心配置文件的位置
key:是固定的
value:格式固定,classpath:文件名
-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!-- 告诉tomcat 用于创建spring容器的监听器的位置 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>
然后就可以在servlet中使用下面的方式获取ApplicationContext:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
ApplicationContext ac = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
userService = (UserService ) ac.getBean("userService");
userService.register();
}
其实用了SpringMVC之后不会这么麻烦23333?