大家好,我是一个爱举铁的程序员Shr。
本文介绍Spring框架中AOP(Aspect Oriented Programming面向切面编程)的概念。阅读本篇文章可能需要30分钟。
Spring使用AspectJ实现了AOP,提供XML配置方式和注解配置方式。
AOP中用到了代理模式。
先讲几个概念。
参考:https://docs.spring.io/spring/docs/4.3.17.RELEASE/spring-framework-reference/htmlsingle/#aop-introduction-defn
Aspect(切面):横切关注点的抽象。切入点加通知形成了切面。
Join point(连接点):被拦截到的点。在Spring中,这些点指的是方法,因为Spring只支持方法类型的连接点,实际上Join point还可以是field或类构造器。
Advice(通知):拦截到Join point之后要做的事情就是通知。通知分为前置通知,后置通知,异常通知,最终通知,环绕通知。用来区别在Joint point之前、之后执行代码。
Pointcut(切入点):切入点是指对连接点进行拦截的定义。指定一个规则,拦截哪些方法。
Introduction(引入):不修改类代码的前提下,在运行期间为类动态地添加一些方法或字段。
Target object(目标对象):代理的目标对象。
AOP proxy(AOP代理):由AOP框架创建的对象。AOP代理是JDK动态代理或CGLIB代理。
Weaving(编织):将aspects应用到target对象并导致proxy对象创建的过程。
在上面几个概念中。
连接点可以直接理解成方法。
切入点就是拦截这些方法的规则。
通知就是拦截到方法后所做的事情。
切面就是切入点加通知,因为事情可能是在被拦截到的方法之前做的或者之后做的。
接下来通过一段代码来更深刻得了解一下这些概念。
package com.shrmus.spring.aop.pojo;
public class Student {
private int id;
private String name;
private int age;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
package com.shrmus.spring.aop.service;
public interface StudentService {
public void addStudent();
public void getStudentList();
public void updateStudent();
}
package com.shrmus.spring.aop.service.impl;
import com.shrmus.spring.aop.service.StudentService;
/**
* Target object 目标对象
* Title:StudentServiceImpl
* Description:
* @author Shr
* @date 2018年7月31日上午1:09:48
* @version
*/
public class StudentServiceImpl implements StudentService{
/**
* Joint point 连接点
* 添加一个学生的信息
*/
public void addStudent() {
System.out.println("addStudent()");
}
/**
* Joint point 连接点
* 查找所有学生信息
*/
public void getStudentList() {
System.out.println("getStudentList()");
}
/**
* Joint point 连接点
* 修改学生信息
*/
public void updateStudent() {
System.out.println("updateStudent()");
}
}
参考官方文档https://docs.spring.io/spring/docs/4.3.17.RELEASE/spring-framework-reference/htmlsingle/#aop-api-advice-types实现一个通知。
package com.shrmus.spring.aop.advice;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
/**
* Advice 通知
* Title:TxAdvice
* Description:
* @author Shr
* @date 2018年7月31日上午1:31:32
* @version
*/
public class MyAdvice implements MethodInterceptor{
/**
* 在被拦截到的方法前后做相应的操作,也就是前置通知,后置通知等
* @param invocation 在方法调用的时候截取程序
*/
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("调用方法前....");
System.out.println("调用方法:" + invocation.getMethod().getName());
// 获取正在调用的方法
Object proceed = invocation.proceed();
System.out.println("调用方法后...");
return proceed;
}
}
这里值得一提的是方法拦截器org.aopalliance.intercept.MethodInterceptor是org.aopalliance.aop.Advice的子接口。
而方法调用org.aopalliance.intercept.MethodInvocation是org.aopalliance.intercept.Joinpoint的子接口。
如果是单独实现前置通知或者后置通知等,则需要实现相应的接口,如前置通知的接口org.springframework.aop.MethodBeforeAdvice。具体请参考文档。
package com.shrmus.spring.aop.test;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.JdkRegexpMethodPointcut;
import com.shrmus.spring.aop.advice.MyAdvice;
import com.shrmus.spring.aop.service.StudentService;
import com.shrmus.spring.aop.service.impl.StudentServiceImpl;
/**
* 测试类
* Title:AOPTest
* Description:
* @author Shr
* @date 2018年7月31日上午4:43:08
* @version
*/
public class AOPTest {
public static void main(String[] args) {
// 1、Pointcut 切入点,指定一个规则,拦截哪些方法。
JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
pointcut.setPatterns(".*add.*",".*update.*");
// NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
// pointcut.addMethodName("add*");
// pointcut.addMethodName("update*");
// // 2、Advice 通知,表示对拦截到的方法加上新功能(如日志,事务等)
MyAdvice advice = new MyAdvice();
//
// // 3、Pointcut 切入点 + Advice 通知 = Aspect 切面(横切关注点)
Advisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
// 4、AOP proxy AOP代理
// 创建目标对象
StudentService studentService = new StudentServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(studentService);
// 添加通知或切面,可以添加多个
// proxyFactory.addAdvice(advice);
proxyFactory.addAdvisor(advisor);
// 生成代理对象
StudentService studentServiceProxy = (StudentService) proxyFactory.getProxy();
// 5、使用代理对象
studentServiceProxy.addStudent();
System.out.println();
studentServiceProxy.getStudentList();
System.out.println();
studentServiceProxy.updateStudent();
}
}
第一步:
参考官方文档https://docs.spring.io/spring/docs/4.3.17.RELEASE/spring-framework-reference/htmlsingle/#aop-api-pointcuts-regex设置一个Pointcut(切入点),添加拦截方法的规则。另外在注释中还有另一种方式添加切入点。
第二步:
实例化一个通知对象。
第三步:
参考官方文档https://docs.spring.io/spring/docs/4.3.17.RELEASE/spring-framework-reference/htmlsingle/#aop-api-advisor实例化一个切面。
第四步:
参考官方文档https://docs.spring.io/spring/docs/4.3.17.RELEASE/spring-framework-reference/htmlsingle/#aop-prog生成代理对象。
第五步:
使用代理对象调用方法。
控制台输出:
调用方法前....
调用方法:addStudent
addStudent()
调用方法后...
getStudentList()
调用方法前....
调用方法:updateStudent
updateStudent()
调用方法后...
AOP的几个概念就讲到这里了,AOP常用作记录日志,事务管理等。
引入aop和tx的命名空间,这样就能使用aop和tx开头的标签了。
参考官方文档https://docs.spring.io/spring/docs/4.3.17.RELEASE/spring-framework-reference/htmlsingle/#aop-pointcuts-examples设置拦截方法的规则。
拦截所有public修饰的方法:
execution(public * *(..))
拦截任何set开头的方法:
execution(* set*(..))
拦截com.shrmus.spring.aop.service.impl包下的所有方法:
execution(* com.shrmus.spring.aop.service.impl.*.*(..))
拦截com.shrmus.spring.aop包及其子包下的所有方法:
execution(* com.shrmus.spring.aop..*.*(..))
公式就是:execution(修饰符 返回值类型 类的全限定名.方法名(参数))
返回值类型可不写。
在com.shrmus.spring.aop.advice.MyAdvice类中添加几个通知类型的方法。
public void before() {
System.out.println("前置通知...");
}
public void around() {
System.out.println("环绕通知...");
}
public void after() {
System.out.println("最终通知...");
}
public void afterReturning() {
System.out.println("后置通知...");
}
public void afterThrowing() {
System.out.println("异常通知...");
}
package com.shrmus.spring.aop.test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.shrmus.spring.aop.service.StudentService;
public class StudentTest {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext-aop.xml");
StudentService userService = (StudentService) ac.getBean("studentService");
userService.addStudent();
System.out.println();
userService.getStudentList();
System.out.println();
userService.updateStudent();
}
}
控制台打印结果:
调用方法前....
调用方法:addStudent
前置通知...
环绕通知...
最终通知...
后置通知...
调用方法后...
调用方法前....
调用方法:getStudentList
前置通知...
环绕通知...
最终通知...
后置通知...
调用方法后...
调用方法前....
调用方法:updateStudent
前置通知...
环绕通知...
最终通知...
后置通知...
调用方法后...
可以看到控制台没有再打印调用方法时,方法内部打印的语句了,是因为配置了环绕通知。把配置文件中配置环绕通知的语句注释掉。
目标对象是com.shrmus.spring.aop.service.impl.StudentServiceImpl,现在我们要测试异常通知。修改getStudentList()方法。
/**
* Joint point 连接点
* 查找所有学生信息
*/
public void getStudentList() {
System.out.println("getStudentList()");
int i = 1/0;
}
测试类的代码不需要修改,直接运行,控制台打印结果:
调用方法前....
调用方法:addStudent
前置通知...
addStudent()
最终通知...
后置通知...
调用方法后...
调用方法前....
调用方法:getStudentList
前置通知...
getStudentList()
异常通知...
最终通知...
Exception in thread "main" java.lang.ArithmeticException: / by zero
可以看到注释掉环绕通知后,被调用的方法内部的语句打印出来了。
而第二个被调用的方法出现异常之后没有再调用后置通知。
这些就是通知的类型了。
前面说过,AOP常用于日志记录和事务处理,接下来讲一讲事务处理。
CREATE TABLE `jdbc01_student` (
`stu_id` int(11) NOT NULL auto_increment,
`stu_name` varchar(20) default NULL,
`stu_age` int(11) default NULL,
`stu_gender` int(11) default NULL,
PRIMARY KEY (`stu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
package com.shrmus.spring.tx.pojo;
import java.io.Serializable;
public class Student implements Serializable{
private int id;
private String name;
private int age;
private int gender;
@Override
public String toString() {
return "Student [id=" + id + ", name=" + name + ", age=" + age + ", gender=" + gender + "]";
}
public Student() {
}
public Student(String name, int age, int gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public Student(int id, String name, int age, int gender) {
super();
this.id = id;
this.name = name;
this.age = age;
this.gender = gender;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getGender() {
return gender;
}
public void setGender(int gender) {
this.gender = gender;
}
}
jdbc.url=jdbc:mysql://localhost:3306/design-pattern-20180602?characterEncoding=utf8
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.username=root
jdbc.password=shrmus
配置文件名是applicationContext-tx.xml
package com.shrmus.spring.tx.dao;
import java.util.List;
import com.shrmus.spring.tx.pojo.Student;
public interface StudentDao {
public List getStudentList();
public void addStudent(Student student);
}
package com.shrmus.spring.tx.dao.impl;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import com.shrmus.spring.tx.dao.StudentDao;
import com.shrmus.spring.tx.pojo.Student;
/**
*
* Title:StudentDao
* Description:
* @author Shr
* @date 2018年7月31日下午7:12:54
* @version
*/
public class StudentDaoImpl implements StudentDao{
private JdbcTemplate jdbcTemplate;
/**
* 查找所有学生
* @return
*/
public List getStudentList(){
String sql = "select stu_id,stu_name,stu_age,stu_gender from jdbc01_student";
List studentList = jdbcTemplate.query(sql, new RowMapper() {
@Override
public Student mapRow(ResultSet rs, int rowNum) throws SQLException {
Integer id = Integer.parseInt(rs.getString(1));
String name = rs.getString(2);
Integer age = Integer.parseInt(rs.getString(3));
Integer gender = Integer.parseInt(rs.getString(4));
return new Student(id,name,age,gender);
}
});
return studentList;
}
/**
* 添加一条记录
* @param student
* @return
*/
public void addStudent(Student student) {
String sql = "insert into jdbc01_student(stu_name,stu_age,stu_gender) "
+ "values(?,?,?)";
jdbcTemplate.update(sql, student.getName(),+student.getAge(),student.getGender());
}
public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
package com.shrmus.spring.tx.service;
import java.util.List;
import com.shrmus.spring.tx.pojo.Student;
public interface StudentService {
public void addStudent(Student student);
public List getStudentList();
public void updateStudent();
}
package com.shrmus.spring.tx.service.impl;
import java.util.List;
import com.shrmus.spring.tx.service.StudentService;
import com.shrmus.spring.tx.dao.StudentDao;
import com.shrmus.spring.tx.pojo.Student;
/**
* 事务
* Title:StudentServiceImpl
* Description:
* @author Shr
* @date 2018年7月31日下午7:13:13
* @version
*/
public class StudentServiceImpl implements StudentService{
private StudentDao studentDao;
/**
* 添加一个学生的信息
*/
public void addStudent(Student student) {
studentDao.addStudent(student);
}
/**
* 查找所有学生信息
*/
public List getStudentList() {
List studentList = studentDao.getStudentList();
return studentList;
}
/**
* 修改学生信息
*/
public void updateStudent() {
// TODO 待实现
}
public StudentDao getStudentDao() {
return studentDao;
}
public void setStudentDao(StudentDao studentDao) {
this.studentDao = studentDao;
}
}
package com.shrmus.spring.tx.test;
import java.util.List;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.shrmus.spring.tx.pojo.Student;
import com.shrmus.spring.tx.service.StudentService;
public class StudentTest {
@Test
public void addStudent() {
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext-tx.xml");
StudentService userService = (StudentService) ac.getBean("studentService");
Student student = new Student("hehe",20,1);
userService.addStudent(student);
}
@Test
public void getStudentList() {
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext-tx.xml");
StudentService userService = (StudentService) ac.getBean("studentService");
List studentList = userService.getStudentList();
System.out.println(studentList);
}
}
先来看看数据库原来的数据
只有两条数据。
执行测试类中的getStudentList()方法,控制台打印结果:
[Student [id=1, name=张三, age=20, gender=1], Student [id=2, name=李四, age=33, gender=0]]
查找是没有问题的。
执行测试类中的addStudent()方法,控制台是没有打印结果的,让我们看看数据库。
数据库中添加了一条数据,说明添加也没问题。
但是我们要验证事务是否正常。
在Service的实现类中修改addStudent(Student student)方法。
public void addStudent(Student student) {
studentDao.addStudent(student);
int i = 1/0;
}
还是用原来的方法制造一个异常,然后执行测试类中的addStudent()方法。
再查看数据库。
还是3条数据。
把制造异常的那条语句注释掉。
// int i = 1/0;
修改测试类的addStudent()方法中的数据。
@Test
public void addStudent() {
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext-tx.xml");
StudentService userService = (StudentService) ac.getBean("studentService");
Student student = new Student("haha",22,1);
userService.addStudent(student);
}
执行之后再看数据库。
可以看到主键已经不是连续自增的了,就是刚刚那个异常导致添加数据失败。
好了,事务管理就到这里了。
首先感谢你能看到总结,说明你还是有耐心的嘛,哈哈哈。
这篇文章主要是回顾之前学Spring的时候懵逼的知识,本人之前在学Spring的时候,配置文件都是直接复制粘贴,什么意思都不明白,现在有点时间再慢慢消化这些知识。
在做事务处理中用到了JdbcTemplate,之前没学过这个,只是听说过,正好在官方文档看到了这个东西,又看了一下。
Spring的IoC和AOP就结束了,如果后面又看了新东西,会继续补充。
AOP源代码:
https://github.com/ShrMus/Design-Pattern/tree/master/design-pattern-20180602/src/main/java/com/shrmus/spring/aop
事务管理源代码:
https://github.com/ShrMus/Design-Pattern/tree/master/design-pattern-20180602/src/main/java/com/shrmus/spring/tx