如果转载文章请注明出处, 谢谢 !
本系列文章是学习完 Spring4.3.8 后的详细整理, 如果有错误请向我指明, 我会及时更正~😝
Spring4.3.8
Spring4.3.8学习[一]
Spring4.3.8学习[二]
4. 面向切面编程
这里有一张网上看到的图我觉得也蛮适合理解的, 可以看完下面的代理之后返回来仔细看看这张图能够更容易理解:
AOP
4.1 代理模式
我们在 Hibernate 中处理增删改功能都要添加事务, 代码如下:
public class UserDaoImpl implements UserDao {
@Override
public void insertUser() {
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
System.out.println("插入用户");
transaction.commit();
session.close();
}
@Override
public void delete(Integer uid) {
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
System.out.println("删除用户");
transaction.commit();
session.close();
}
}
事务和逻辑的处理混杂在一起, 如果再增添方法还需要继续重复有关事务的重复代码.
或者需求发生了变化,要求项目中所有的类在执行方法时输出执行耗时。最直接的办法是修改源代码
缺点:
1、工作量特别大,如果项目中有多个类,多个方法,则要修改多次。
2、违背了一些设计原则:
开闭原则(OCP)[对扩展开放,对修改关闭,而为了增加功能把每个方法都修改了,也不便于维护。]
单一职责(SRP)[每个方法除了要完成自己本身的功能,还要计算耗时、延时;每一个方法引起它变化的原因就有多种。]
4.1.1 静态代理
1、定义UserDao接口, 抽象主题
public interface UserDao{
void insertUser();
void delete(Integer uid);
}
2、实现UserDao接口, 被代理的目标对象, 真实主题
public class UserDaoImpl implements UserDao {
@Override
public void insertUser() {
System.out.println("插入用户");
}
@Override
public void delete(Integer uid) {
System.out.println("删除用户");
}
}
3、代理类, 静态代理类
public class UserDaoProxy implements UserDao{
//被代理的对象
private UserDao userDao;
private Transaction transaction;
public UserDaoProxy(UserDao userDao, Transaction transaction) {
this.userDao = userDao;
this.transaction = transaction;
}
@Override
public void insertUser() {
transaction.beginTransaction();
userDao.insertUser();
transaction.commitTransaction();
}
@Override
public void delete(Integer uid) {
transaction.beginTransaction();
userDao.insertUser();
transaction.commitTransaction();
}
}
4、测试
public class ProxyTest {
@Test
public void testProxy(){
UserDao dao = new UserDaoImpl();
Transaction transaction = new Transaction();
UserDaoProxy proxy = new UserDaoProxy(dao, transaction);
proxy.insertUser();
}
}
通过静态代理可以解决这些设计原则问题, 但是不能解决:
如果项目中有多个类,则需要编写多个代理类,工作量大,不好修改,不好维护,不能应对变化.
如果要解决上面的问题,可以使用动态代理
4.1.2 JDK动态代理
JDK的动态代理主要涉及 java.lang.reflect 包中的两个类:Proxy和InvocationHandler。其中,InvocationHandler是一个接口,可以通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态地将横切逻辑和业务逻辑编织在一起。而Proxy利用InvocationHandler动态创建一个符合某一接口的实例,生成目标类的代理对象。
只需要一个代理类,而不是针对每个类编写代理类。
1、在上一个示例中修改代理类 UserDaoProxy 如下:
JDK动态代理需要实现 InvocationHandler 接口, 这个类其实应该是拦截器类
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 拦截器:
* 1. 目标类导入
* 2. 事务导入
* 3. invoke完成:
* 1.开启事务
* 2.调用目标对象方法
* 3.事务提交
*/
public class UserDaoProxy implements InvocationHandler {
private Object target;
private Transaction transaction;
public UserDaoProxy(Object target, Transaction transaction) {
this.target = target;
this.transaction = transaction;
}
/**
* 当用户调用对象中的每个方法时都通过下面的方法执行,方法必须在接口
* proxy 被代理后的对象
* method 将要被执行的方法信息(反射)
* args 执行方法时需要的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (!methodName.equals("getUser")){
this.transaction.beginTransaction();
method.invoke(target, args); // 目标对象调用目标方法
this.transaction.commitTransaction();
} else {
method.invoke(target, args);
}
return null;
}
/**
* 获得被代理后的对象
* @param object 被代理的对象
* @return 代理后的对象
* loader:一个ClassLoader对象,定义了由哪个ClassLoader对象来生成代理对象进行加载
* interfaces:一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了
* h:一个InvocationHandler对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上,间接通过invoke来执行
*/
public Object getProxyObject(Object object){
this.target = object;
return Proxy.newProxyInstance(
target.getClass().getClassLoader(), //类加载器
target.getClass().getInterfaces(),//获得被代理对象的所有接口
this); //拦截器: InvocationHandler对象
}
}
2、测试一下:
public class JDKProxyTest {
@Test
public void testProxy(){
/*
1. 创建一个目标类
2. 创建一个事务
3. 创建一个拦截器
4. 动态产生代理对象
*/
Object target = new UserDaoImpl();
Transaction transaction = new Transaction();
List<Interceptor> interceptorList = new ArrayList<>();
interceptorList.add(transaction);
UserDaoProxy interceptor = new UserDaoProxy(target, transaction);
UserDao dao = (UserDao) interceptor.getProxyObject(target);
dao.delete("10");
}
}
使用内置的Proxy实现动态代理有一个问题:被代理的类必须实现接口,未实现接口则没办法完成动态代理。
如果项目中有些类没有实现接口,则不应该为了实现动态代理而刻意去抽出一些没有实例意义的接口,通过cglib可以解决该问题。
4.1.3 CGLIB动态代理
CGLIB(Code Generation Library)是一个开源项目,是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口,通俗说cglib可以在运行时动态生成字节码。
1、引用 cglib jar 包,通过maven或者直接下载 jar 包添加
下载地址: http://download.csdn.net/download/javawebxy/6849703
maven:<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>2.2.2</version> </dependency>
2、修改 拦截器类
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/*
* 动态代理类
* 实现了一个方法拦截器接口
*/
public class MyInterceptor implements MethodInterceptor {
// 被代理对象
private Object target;
private Transaction transaction;
public MyInterceptor(Transaction transaction) {
this.transaction = transaction;
}
//动态生成一个新的类,使用父类的无参构造方法创建一个指定了特定回调的代理实例
public Object getProxyObject(Object object) {
this.target = object;
// 代码增强类
Enhancer enhancer = new Enhancer();
// 回调方法
enhancer.setCallback(this); // 参数就是拦截器
//设置生成类的父类类型
enhancer.setSuperclass(target.getClass());// 生成的代理类父类是目标类
//动态生成字节码并返回代理对象
return enhancer.create();
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
this.transaction.beginTransaction();
method.invoke(target, objects);
this.transaction.commitTransaction();
return null;
}
}
3、测试
/**
* 通过 CGLIB 产生的代理类是目标类的子类
*/
public class CGLIBProxyTest {
@Test
public void testCGLIB(){
Object target = new UserDaoImpl();
Transaction transaction = new Transaction();
MyInterceptor interceptor = new MyInterceptor(transaction);
UserDaoImpl dao = (UserDaoImpl) interceptor.getProxyObject(target);
dao.insertUser();
//另一个被代理的对象,不再需要重新编辑代理代码
PersonDaoImpl personDao = (PersonDaoImpl) interceptor.getProxyObject(new PersonDaoImpl());
personDao.delete();
}
}
4.2 AOP编程
4.2.1概念
使用 JDK 动态代理的代码进行对应说明
1. Aspect(切面)
比如说事务、权限等,与业务逻辑没有关系的部分
2. joinPoint(连接点)
目标类的目标方法。(由客户端在调用的时候决定)
3. pointCut(切入点)
所谓切入点是指我们要对那些拦截的方法的定义.
被纳入spring aop中的目标类的方法。
4. Advice(通知)
所谓通知是指拦截到joinpoint之后所要做的事情就是通知.通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能)
5. Target(目标对象)
代理的目标对象
6. Weaving(织入)
是指把切面应用到目标对象来创建新的代理对象的过程.切面在指定的连接点织入到目标对象
对比表格查看
JDKProxy代理 | SpringAop |
---|---|
目标对象 | 目标对象 Target |
拦截器类 | 切面 Aspect |
拦截器类中的方法 | 通知 Advice |
被拦截到的目标类中方法的集合 | 切入点 pointCut |
在客户端调用的方法(目标类目标方法) | 连接点 joinPoint |
代理类 | AOP代理 AOP Proxy |
代理类的代理方法生成的过程 | 织入 Weaving |
4.2.2 Spring AOP实现的两种模式
4.2.2.1 xml形式
还以事务为例, 准备 Transaction 切面类, 目标接口类 UserDao, 目标类 UserDaoImpl
/**
* 切面
*/
public class Transaction {
public void beginTransaction(){
System.out.println("Begin Transaction");
}
public void commit(){
System.out.println("Commit Transaction");
}
}
- - - - - - - -
public interface UserDao {
void saveUser();
}
- - - - - - - -
public class UserDaoImpl implements UserDao {
@Override
public void saveUser() {
System.out.println("save Person");
}
}
剩余的我们需要配置 appliactionContext.xml
1、引入 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-4.3.xsd" >
2、配置 两个 bean
<bean id="userDao" class="com.lanou.spring.aop.transaction.UserDaoImpl"/>
<bean id="transaction" class="com.lanou.spring.aop.transaction.Transaction"/>
3、aop 的配置
<!-- aop 配置-->
<aop:config>
<!-- 切入点表达式, 确认目标类 -->
<aop:pointcut id="pointcut" expression="execution(* com.lanou.spring.aop.transaction.UserDaoImpl.*(..))"/>
<!-- ref 指向的对象就是切面 -->
<aop:aspect ref="transaction">
<!-- 前置通知-->
<aop:before method="beginTransaction" pointcut-ref="pointcut" />
<!-- 正常返回通知-->
<aop:after-returning method="commit" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
4、测试
public class TransactionTest {
@Test
public void testTransaction(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserDao userDao = (UserDao) context.getBean("userDao");
userDao.saveUser();
}
}
5、报错, Spring缺少 aspectjweaver.jar 包, 添加 jar 包重新运行
Caused by: java.lang.ClassNotFoundException: org.aspectj.weaver.reflect.ReflectionWorld$ReflectionWorldException
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 70 more
Spring AOP 原理:
- 当spring容器启动的时候,加载两个bean,对两个bean进行实例化
- 当spring容器对配置文件解析到
<aop:config>
的时候 - 把切入点表达式解析出来,按照切入点表达式匹配spring容器内容的bean
- 如果匹配成功,则为该bean创建代理对象
- 当客户端利用context.getBean获取一个对象时,如果该对象有代理对象,则返回代理对象; 如果没有代理对象,则返回对象本身
- 切入点表达式如与springbean 没有一个匹配就会报错
4.2.2.2 注解形式
- 首先需要导入两个 jar 包 : aspectjrt & aspectjweaver . 需要注意如果你的 jdk 是1.7 就找这两个 jar 包的1.7.+版本; 我是jdk1.8, 使用了aspectjrt1.8.5 & aspectjweaver1.8.5
- 在 applicationContext.xml 中添加context, 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: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/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd" >
<!-- 扫描包 -->
<context:component-scan base-package="com.lanou.transaction"/>
<!-- 自动创建代理 -->
<aop:aspectj-autoproxy/>
</beans>
- 添加 UserDaoImpl, Transaction 的注解
@Repository("userDao")
public class UserDaoImpl implements UserDao {...}
--------
@Component
public class Transaction {...}
- 在 Transaction 切面类中添加注解
@Component
@Aspect
public class Transaction {
@Pointcut("execution(* com.lanou.transaction.UserDaoImpl.*(..))")
private void method(){} //方法签名
@Before("method()")
public void beginTransaction(JoinPoint joinpoint){
System.out.println("Begin Transaction");
}
@AfterReturning(value = "method()", returning = "returnVal")
public void commit(JoinPoint joinpoint, Object returnVal)
{
System.out.println("返回值: " + returnVal);
System.out.println("Commit Transaction");
}
@After("method()")
public void finallyMethod(JoinPoint joinpoint){
System.out.println("Finally Method");
}
@AfterThrowing(value = "method()", throwing = "throwable")
public void throwingMethod(JoinPoint joinpoint, Throwable throwable){
System.out.println("异常: " + throwable.getMessage());
}
}
4.2.3 切入点表达式
屏幕快照 2017-06-08 下午4.38.24.pngexecution(public * *(..)) : 任意公共方法的执行
execution(* set*(..)) : 任何一个名字以 set 开始的方法的执行
execution(* com.lanou.spring.aop.transaction.service.*.*(..)) : 在 service 包中定义的任意方法的执行
execution(* com.lanou.spring.transaction.service..*.*(..)) : 在 service 包或其子包中定义的任意方法的执行
execution(* com.lanou.spring.aop..service..*.*(..)) : 在 aop 包及子包一直到 service 包,再子包下的所有类所有方法
4.2.4 通知
4.2.4.1 通知种类
名称 | 解释 | 使用 |
---|---|---|
前置通知 [Before advice] |
在连接点前面执行,前置通知不会影响连接点的执行, 除非此处抛出异常 |
<aop:before method="before" pointcut-ref="pointcut"/> |
正常返回通知 [After returning advice] |
在连接点正常执行完成后执行, 如果连接点抛出异常,则不会执行 |
<aop:after-returning method="afterReturning" pointcut-ref="pointcut"/> |
异常返回通知 [After throwing advice] |
在连接点抛出异常后执行 | <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut"/> |
返回通知 [After (finally) advice] |
在连接点执行完成后执行, 不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容 |
<aop:after method="after" pointcut-ref="pointcut"/> |
环绕通知 [Around advice] |
环绕通知围绕在连接点前后,比如一个方法调用的前后。 这是最强大的通知类型,能在方法调用前后自定义一些操作。 环绕通知还需要负责决定目标方法的执行 |
<aop:around method="around" pointcut-ref="pointcut"/> |
4.2.4.2 通知使用细节
- 前置通知: 每种通知都能够添加连接点参数, 可以获取连接点信息
/*
前置通知
1.在目标方法执行之前执行
2.获取不到目标方法返回值
*/
public void beginTransaction(JoinPoint joinpoint){
System.out.println("连接点名称: "+joinpoint.getSignature().getName());
System.out.println("目标类" + joinpoint.getTarget().getClass());
System.out.println("Begin Transaction");
}
- 后置通知可以获取返回值类型, 但当目标方法产生异常, 后置通知将不再执行
public class UserDaoImpl implements UserDao {
@Override
public String saveUser() {
/* 制造异常
int a = 1/0;
*/
System.out.println("save User");
return "11111111";
}
}
<!-- 正常返回通知
1. 可以获取目标方法的返回值
2. 当目标方法产生异常, 后置通知将不再执行
-->
<aop:after-returning method="commit" pointcut-ref="pointcut" returning="returnVal"/>
</aop:aspect>
/*
后置通知, 在目标方法执行之后执行, 返回值参数的名称与 xml 中保护一致
*/
public void commit(JoinPoint joinpoint, Object returnVal){
System.out.println("目标方法返回值: " + returnVal);
System.out.println("Commit Transaction");
}
- 最终通知
/*
最终通知
无论目标方法是否发出异常都将执行
*/
public void finallyMethod(JoinPoint joinpoint){
System.out.println("Finally Method");
}
<!-- 最终通知 -->
<aop:after method="finallyMethod" pointcut-ref="pointcut"/>
- 异常通知
/*
异常通知
接受目标方法抛出的异常
*/
public void throwingMethod(JoinPoint joinPoint, Throwable throwable){
System.out.println("异常: " + throwable.getMessage());
}
<!--异常通知-->
<aop:after-throwing method="throwingMethod" pointcut-ref="pointcut" throwing="throwable"/>
- 环绕通知
/*
环绕通知
ProceedingJoinPoint: 子接口
控制目标方法的执行
前置通知和后置通知也能在目标方法的前后添加内容,但是不能控制目标方法的执行
*/
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("before..before..before");
joinPoint.proceed(); // 调用目标方法
System.out.println("after..after.after");
}
<!-- 环绕通知 -->
<aop:around method="around" pointcut-ref="pointcut"/>
</aop:aspect>
4.2.5 SpringAOP 细节
- 如果目标类实现了接口, 则采用 JDKProxy; 如果没有实现接口, 采用 CGLIBProxy [Spring 内部做的]
- 目标类实现了接口, 但还想要采用 CGLIBProxy, 作如下更改:
Spring4.3.8学习之 与 Struts2 整合[四]
Spring4.3.8学习之与Hibernate4 整合[五]
Spring4.3.8学习之S2SH 整合[六]
网友评论