Spring中的AOP
目录
一.AOP思想
在学习Spring的AOP之前,我们先看看另外两个概念,面向过程编程和面向对象编程。
面向过程编程(Procedure Oriented Programming):简称OPP,是一种以过程为中心的编程思想,围绕当前需要的功能和目标具体化的,流程化的去实现。很直接,想要干什么就直接去干。
编程角度:编程者把要做的功能当成一个目标(其他什么都不考虑),一步一步去实现,去完成,去达到。
例如:Pascal,C等语言就是这样的编程思想。
面向对象编程(Object Oriented Programming):简称OOP,是一种以对象为中心的编程思想。要完成一个功能,一般需要一个对象或多个对象的特性和行为去完成。是在面向过程的基础上进行了抽象。
编程角度:编程者认为世界中的任何事物都是一个个体,一个单体,一个对象,都有特点(属性)和行为(方法)。
例如:C++、C#、Java等语言就是这样的编程思想。
面向切面切面编程(Aspect Oriented Programming):简称AOP,是一种以横切为中心的编程思想。面向对象编程是把任何东西都看成一个整体,一个对象的。往往这个整体之间又需要一些通用的,附加的东西,那我们就需要把这个整体切开,把需要的通用的东西放进去,这样去实现一个功能。
可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
二.AOP应用
下面说到的全部实例,都是基于一个搭好springMVC项目完成的:这个项目名称是spring_demo1,是一个前后端分离的实现了简单用户登录的web项目,详情可点击:spring_demo1
1.简单的例子
(1).基于配置
需求:我们要在每次调用Controller层前,打印一句话,“2018-11-04 19:44:30 调用了controller”,里面的绿色字体时间是调用时的当前系统时间。
a.在com.cdd包下新建一个包aspect,在com.cdd.aspect包编写切面组件PrintLog,代码入下:
package com.cdd.aspect;
import java.text.SimpleDateFormat;
import java.util.Date;
public class PrintLog {
public void callTime(){
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:MM:ss");
String str_date = sdf.format(date);
System.out.println(str_date + "调用controller了");
}
}
b.配置切面组件
在spring容器配置文件applicationContext.xml中配置切面组件,先把PrintLog类注册为bean,再在aop配置中将该bena配置成切面组,调用在进入com.cdd.controller包下组件之前(切点),先调用切面组件的callTime方法 ,配置部分如下:
c.测试。
在浏览器地址栏输入:http://localhost:8080/spring_demo1/login.html 回车,再响应页面输入用户名:cdd,密码:1234,点击登录后(当然其他能调controller层的方式都可以,比如单元测试,postman发请求等),查看控制台,如下图:
(2).基于注解
需要:在调用业务层service完成后,控制台打印“2018-11-04 19:44:30 sercice层调用完了”,这句话。
a.开启aop注解支持
在spring容器配置文件applicationContext.xml中配置,开启aop注解支持。配置部分如下:
b.编写切面组件PrintLog1,代码如下:
package com.cdd.aspect;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Component //将该组件注册到spring容器中
@Aspect //将该组件配置为切面组件
public class PrintLog1 {
@After("within(com.cdd.serviceImpl..*)") //配置通知和切点
public void callTime1(){
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String str_date = sdf.format(date);
System.out.println(str_date+" sercice层调用完了");
}
}
c.测试
在浏览器地址栏输入:http://localhost:8080/spring_demo1/user/login.do?name=cdd&pwd=1234,点击回车(或者其他方法,只要能调到service层就行),查看控制台如下,就说这个切面组件配置成功了
注意:
如果在上面的例子中,控制台报: java.lang.NoClassDefFoundError: org/aspectj/lang/JoinPoint,如下图:
原因是:虽然 我们直接将Spring 依赖的所有的jar 包 导入进来 但是 我们因为使用了 表达式语言 我们需要引入一个 第三方的jar 包,com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar,那么想lib文件夹下引入该包,并且通过Build Path添加到项目中,重新启动tomcat后,测试就正常了。
2.要点总结
(1).切面组件 Aspect
功能:干什么
要切入什么功能,比如说打印一份调用日志或发生异常时,将异常信息保存到一个文件中,这往往是某个类的某个方法。
例如,刚才写的PrintLog中的callTime方法、PrintLog类中的callTime1()方法
(2).通知 Advice
时间:啥时候干
通知就是这个切面组件切入的时机,通俗点说,就是什么时候出现这个功能,发生这件事。重点是时机。
AOP有5大通知,AspectJ 支持 5 种类型的通知注解:
@Before: 前置通知, 在方法执行之前执行
@After: 后置通知, 在方法执行之后执行
@AfterRunning: 最终通知, 在方法返回结果之后执行
@AfterThrowing: 异常通知, 在方法抛出异常之后
@Around: 环绕通知, 围绕着方法执行
其中,环绕通知 = 前置通知 + 后置通知
例如,在一个catch块中:
try{
前置通知@Before
//目标方法处理
后置通知@AfterReturning
}catch(e){
异常通知@AfterThrowing
}finally{
最终通知@After
}
(3).切点 Point
地点:在哪儿干
就是往哪些组件上切入这样的功能。
在配置切点时,有两种表达式:分别是类型限定表达式、方法限定表达式
a.类型限定表达式
--类型限定表达式(对组件作用):within( 类型 )
例:
within(org.service.UserService) //单个组件UserService
within(org.service.*) //org.service包下的组件(有一个点)
within(org.service..*) //org.service 包及其子包下的所有组件(有两个点)
b.方法限定表达式
--方法限定表达式:execution( 修饰符 返回类型 方法名(参数) )
表达式中:
修饰符(可有可无),返回类型(没返回的话是void、有返回的话,返回什么类型就是类型、都适应的话用 * )。
方法名(例:find* () 即,以find开头的方法)。
参数(没有就不写,有什么类型就写什么类型, ,,两个点表示0个或多个参数)。
例:
execution( * find*(..) ) //对以find开头的方法作用
execution( * org.service.UserService.regist(..) ) //对指定包下的指定方法作用
execution( * org.service..*.*() ) //对org.service包下及其子包下的所有放作用,相当于within(org.service.*)
补充:
上述表达式可以使用! 、&& 、|| 运算符连接,
例如:execution( * find ecution( *org.service..*.*() )
3.案例实战
需求:在登录时,如果发生异常,把这个异常信息保存到本地中,文件路径为,D:\\project\\test\\exception_log.txt文件中。
(1).在Spring容器中配置aop
在Spring容器配置文件applicationContext.xml中开启aop支持(如果已经配置了,可以忽略本步操作),如下图:
(2).编写切面组件
在com.cdd.aspect包下编写切面组件,SvaeException类,代码如下:
package com.cdd.aspect;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class SaveException {
@AfterThrowing(throwing="ex",pointcut="within(com.cdd.controller.*)")
public void exceptionMessage(Exception ex){
//控制台打印下这个异常信息
System.out.println(ex);
try {
FileWriter fw = new FileWriter("D:\\project\\test\\exception_log.txt");
PrintWriter pw = new PrintWriter(fw);
ex.printStackTrace(pw);
pw.flush();
pw.close();
fw.close();
} catch (IOException e) {
e.printStackTrace();
System.out.println("保存异常信息发送异常");
}
}
}
(3).人为构造异常
在UserServiceImpl组件的login()方法里构造一个数组下标越界异常(要记得测试完成后去掉这个异常),代码如下:
package com.cdd.serviceImpl;
import java.security.NoSuchAlgorithmException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.cdd.dao.UserDao;
import com.cdd.entity.DataResult;
import com.cdd.entity.User;
import com.cdd.service.UserService;
import com.cdd.util.FirstUtil;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
public DataResult Login(String name, String pwd) {
System.out.println("正在service层");
DataResult dataResult = new DataResult();
//给密码进行md5加密
try {
pwd = FirstUtil.md5(pwd);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
Integer isExist = userDao.checkLogin(name, pwd);
if(isExist==null || isExist==0){
dataResult.setStatus(-1);
dataResult.setMsg("用户名或密码错误");
}else{
dataResult.setStatus(0);
dataResult.setMsg("登录成功");
}
//构造一个数组下标越界异常
int[] a = {1};
System.out.println(a[2]);
return dataResult;
}
}
(4).测试
在浏览器地址栏输入:http://localhost:8080/spring_demo1/user/login.do?name=cdd&pwd=1234,点击回车,(其他方式也行,只要能从Controller层调用这个登录功能就行)。
a.查看控制台:
b.查看异常信息是否保存到exception_log.txt文件中了
到这里说明我们的认为完成了。
三.参考资料
aop思想:https://www.cnblogs.com/hongwz/p/5764917.html
aop通知:https://blog.csdn.net/sinat_28978689/article/details/62215513