⼗三、JdbcTemplate
JdbcTemplate是Spring提供的⼀个JDBC模板类,是对JDBC的封装,简化JDBC代码。
当然,你也可以不⽤,可以让Spring集成其它的ORM框架,例如:MyBatis、Hibernate等。
接下来我们简单来学习⼀下,使⽤JdbcTemplate完成增删改查。
13.1 环境准备
数据库表:t_user
新建模块:spring
6
-
009
-jdbc
引⼊相关依赖:
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.31</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.0</version>
</dependency>
</dependencies>
准备实体类:表t_user对应的实体类User。
User类:
private Integer id;
private String realName;
private Integer age;
public User(){}
public User(Integer id, String realName, Integer age) {
this.id = id;
this.realName = realName;
this.age = age;
}
//后面加set,get,toString方法
}
编写Spring配置⽂件:
JdbcTemplate是Spring提供好的类,这类的完整类名是:
org.springframework.jdbc.core.JdbcTemplate
我们怎么使⽤这个类呢?new对象就可以了。怎么new对象,Spring最在⾏了。直接将这个类配置到Spring配置⽂件中,纳⼊Bean管理即可。
<!--配置jdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="ds"/>
</bean>
MyDataSource类:
public class MyDataSource implements DataSource {
private String driver;
private String url;
private String username;
private String password;
public void setDriver(String driver) {
this.driver = driver;
}
public void setUrl(String url) {
this.url = url;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "MyDataSource{" +
"driver='" + driver + '\'' +
", url='" + url + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
@Override
public Connection getConnection() throws SQLException {
try {
//注册驱动
Class.forName(driver);
//获取数据库连接对象
Connection connection = DriverManager.getConnection(url, username, password);
return connection;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
//其他实现方法
}
写完数据源,我们需要把这个数据源传递给JdbcTemplate。因为JdbcTemplate中有⼀个DataSource属性:
<!--配置自己写的数据源-->
<!--当然也可以集成其他人或者组织开发的数据源,例如:c3p0,dbcp,druid-->
<bean id="ds" class="com.powernode.spring6.bean.MyDataSource">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/spring6"/>
<property name="username" value="root"/>
<property name="password" value="r38153"/>
</bean>
<!--配置jdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="ds"/>
</bean>
测试:
@Test
public void testJdbc() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
System.out.println(jdbcTemplate);//org.springframework.jdbc.core.JdbcTemplate@b3ca52e
}
到这⾥环境就准备好了。
13.2 新增
@Test
public void testInsert(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
//insert语句
String sql="insert into t_user(real_name,age) values(?,?)";
//注意: 在JdbcTemplate当中,只要是insert delete update语句的,都是调用update方法
int count = jdbcTemplate.update(sql, "沸羊羊", 25);
System.out.println(count);
}
update⽅法有两个参数:
●第⼀个参数:要执⾏的SQL语句。(SQL语句中可能会有占位符 ? )
●第⼆个参数:可变⻓参数,参数的个数可以是0
个,也可以是多个。⼀般是SQL语句中有⼏个问号,则对应⼏个参数。
13.3 修改
@Test
public void testUpdate(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
String sql ="update t_user set real_name=?,age=? where id=?";
int count = jdbcTemplate.update(sql, "喜羊羊", 25, 3);
System.out.println(count);
}
13.4 删除
@Test
public void testDelete(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
String sql ="delete from t_user where id=?";
int count = jdbcTemplate.update(sql, 2);
System.out.println(count);
}
13.5 查询⼀个对象
@Test
public void testQueryOne(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
String sql = "select id,real_name,age from t_user where id =?";
User user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), 1);
System.out.println(user);
//User{id=1, realName='李星云', age=23}
}
queryForObject⽅法三个参数:
●第⼀个参数:sql语句
●第⼆个参数:Bean属性值和数据库记录⾏的映射对象。在构造⽅法中指定映射的对象类型。
●第三个参数:可变⻓参数,给sql语句的占位符问号传值。
13.6 查询多个对象
@Test
public void testQueryAll(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
String sql = "select id,real_name,age from t_user";
List<User> users = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
for (User user:users) {
System.out.println(user);
// User{id=1, realName='李星云', age=23}
// User{id=3, realName='喜羊羊', age=25}
// User{id=4, realName='沸羊羊', age=25}
}
}
13.7 查询⼀个值
@Test
public void testQueryOneValue(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
String sql="select count(1) from t_user"; //返回总记录条数是一个int类型
Integer total = jdbcTemplate.queryForObject(sql, int.class);
System.out.println("总记录条数:"+total);
//3
}
13.8 批量添加
@Test
public void testBatchInsert(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
String sql ="insert into t_user(real_name,age) values(?,?)";
//准备数据
Object[] objs1={"姬如雪",24};
Object[] objs2={"张子凡",24};
Object[] objs3={"陆林轩",24};
Object[] objs4={"尤川",24};
//添加到List集合
List<Object[]> list = new ArrayList<>();
list.add(objs1);
list.add(objs2);
list.add(objs3);
list.add(objs4);
//执行sql语句
int[] count = jdbcTemplate.batchUpdate(sql, list);
System.out.println(Arrays.toString(count));//[1, 1, 1, 1]
}
13.9 批量修改
@Test
public void testBatchUpdate(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
String sql ="update t_user set real_name = ?, age=? where id=?";
//准备数据
Object[] objs1={"美羊羊",21,3};
Object[] objs2={"懒羊羊",23,4};
//添加到List集合
List<Object[]> list = new ArrayList<>();
list.add(objs1);
list.add(objs2);
//执行sql语句
int[] count = jdbcTemplate.batchUpdate(sql, list);
System.out.println(Arrays.toString(count));//[1, 1]
}
13.10 批量删除
@Test
public void testBatchDelete(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
String sql ="delete from t_user where id=?";
//准备数据
Object[] objs1={9};
Object[] objs2={10};
Object[] objs3={11};
Object[] objs4={12};
//添加到List集合
List<Object[]> list = new ArrayList<>();
list.add(objs1);
list.add(objs2);
list.add(objs3);
list.add(objs4);
//执行sql语句
int[] count = jdbcTemplate.batchUpdate(sql,list);
System.out.println(Arrays.toString(count));//[1, 1, 1, 1]
}
13.11 使⽤回调函数
@Test
public void testCallBack(){
//如果你想写JDBC代码,你可以使用callback回调函数
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
//准备Sql语句
String sql="select id,real_name,age from t_user where id=?";
//注册回调函数,当execute方法执行的时候,回调函数中的doInPreparedStatement()会被调用
User user = jdbcTemplate.execute(sql, new PreparedStatementCallback<User>() {
@Override
public User doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException {
User user = null;
ps.setInt(1, 8);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
int id = rs.getInt("id");
String realName = rs.getString("real_name");
int age = rs.getInt("age");
user = new User(id, realName, age);
}
return user;
}
});
System.out.println(user);//User{id=8, realName='尤川', age=24}
}
13.12 使⽤德鲁伊连接池
之前数据源是⽤我们⾃⼰写的。也可以使⽤别⼈写好的。例如⽐较⽜的德鲁伊连接池。
第⼀步:引⼊德鲁伊连接池的依赖。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.8</version>
</dependency>
第⼆步:将德鲁伊中的数据源配置到spring配置⽂件中。和配置我们⾃⼰写的⼀样。
spring.xml文件:
<!--引入德鲁伊连接池-->
<bean id="ds" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/spring6"/>
<property name="username" value="root"/>
<property name="password" value="r38153"/>
</bean>
<!--配置jdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="ds"/>
</bean>
⼗四、GoF之代理模式
14.1 对代理模式的理解
⽣活场景
1
:⽜村的⽜⼆看上了隔壁村⼩花,⽜⼆不好意思直接找⼩花,于是⽜⼆找来了媒婆王妈妈。这⾥⾯就有⼀个⾮常典型的代理模式。⽜⼆不能和⼩花直接对接,只能找⼀个中间⼈。其中王妈妈是代理类,⽜⼆是⽬标类。王妈妈代替⽜⼆和⼩花先⻅个⾯。(现实⽣活中的婚介所)【在程序中,对象A和对象B⽆法直接交互时。】
⽣活场景
2
:你刚到北京,要租房⼦,可以⾃⼰找,也可以找链家帮你找。其中链家是代理类,你是⽬标类。你们两个都有共同的⾏为:找房⼦。不过链家除了满⾜你找房⼦,另外会收取⼀些费⽤的。(现实⽣活中的房产中介)【在程序中,功能需要增强时。】
⻄游记场景:⼋戒和⾼⼩姐的故事。⼋戒要强抢⺠⼥⾼翠兰。悟空得知此事之后怎么做的?悟空幻化成⾼⼩姐的模样。代替⾼⼩姐与⼋戒会⾯。其中⼋戒是客户端程序。悟空是代理类。⾼⼩姐是⽬标类。那天夜⾥,在⼋戒眼⾥,眼前的就是⾼⼩姐,对于⼋戒来说,他是不知道眼前的⾼⼩姐是悟空幻化的,在他内⼼⾥这就是⾼⼩姐。所以悟空代替⾼⼩姐和⼋戒亲了嘴⼉。这是⾮常典型的代理模式实现的保护机制。
代理模式中有⼀个⾮常重要的特点:对于客户端程序来说,使⽤代理对象时就像在使⽤⽬标对象⼀样。
【在程序中,⽬标需要被保护时】
业务场景:系统中有A、B、C三个模块,使⽤这些模块的前提是需要⽤户登录,也就是说在A模块中要编写判断登录的代码,B模块中也要编写,C模块中还要编写,这些判断登录的代码反复出现,显然代码没有得到复⽤,可以为A、B、C三个模块提供⼀个代理,在代理当中写⼀次登录判断即可。代理的逻辑是:请求来了之后,判断⽤户是否登录了,如果已经登录了,则执⾏对应的⽬标,如果没有登录跳转到登录⻚⾯。【在程序中,⽬标不但受到保护,并且代码也得到了复⽤。】
代理模式是GoF
23
种设计模式之⼀。属于结构型设计模式。
代理模式的作⽤是:为其他对象提供⼀种代理以控制对这个对象的访问。在某些情况下,⼀个客户不想或者不能直接引⽤⼀个对象,此时可以通过⼀个称之为“代理”的第三者来实现间接引⽤。代理对象可以在客户端和⽬标对象之间起到中介的作⽤,并且可以通过代理对象去掉客户不应该看到的内容和服务或者添加客户需要的额外服务。 通过引⼊⼀个新的对象来实现对真实对象的操作或者将新的对象作为真实对象的⼀个替身,这种实现机制即为代理模式,通过引⼊代理对象来间接访问⼀个对象,这就是代理模式的模式动机。
代理模式中的⻆⾊:
●
代理类(代理主题)
●
⽬标类(真实主题)
●
代理类和⽬标类的公共接⼝(抽象主题):客户端在使⽤代理类时就像在使⽤⽬标类,不被客户端所察觉,所以代理类和⽬标类要有共同的⾏为,也就是实现共同的接⼝。
代理模式的类图:

代理模式在代码实现上,包括两种形式:
●
静态代理
●
动态代理
14.2 静态代理
使用普通方式解决问题:
现在有这样⼀个接⼝和实现类:
OrderService接口:
//订单业务接口
public interface OrderService {
//生成订单
void generate();
//修改订单
void modify();
//查看订单详情
void detail();
}
解决方式一:
OrderServiceImpl类:
/*
* 项目经理提出一个新的需求:要统计所有业务接口中每一个业务方法的耗时。
* 解决方案一:硬编码,在每一个业务接口中的每一个业务方法中直接添加统计耗时的程序。
* 这种方案的缺点:
* 缺点一:违背OCP开闭原则。
* 缺点二:代码没有得到复用。(相同的代码写了很多遍。)
*
* 解决方案二:编写业务类的子类,让子类继承业务类,对每个业务方法进行重写。
* 缺点一:虽然解决了OCP开闭原则。但是这种方式会导致耦合度很高,因为采用了继承关系。继承关系是一种耦合度非常高的关系,不建议使用。
* 缺点二:代码没有得到复用。(相同的代码写了很多遍。)
*
* 解决方案三:代理模式。
* 优点1:解决了OCP问题。
* 优点2:采用代理模式的has a,可以降低耦合度。
*
* 目前我们使用的是静态代理,这个静态代理的缺点是什么?
* 类爆炸。假设系统中有1000个接口,那么每个接口都需要对应代理类,这样类会急剧膨胀。不好维护。
* 怎么解决类爆炸问题?
* 可以使用动态代理来解决这个问题。
*
* 动态代理还是代理模式,只不过添加了字节码生成技术,可以在内存中为我们动态的生成一个class字节码,这个字节码就是代理类。
* 在内存中动态的生成字节码代理类的技术,叫做:动态代理。
*/
public class OrderServiceImpl implements OrderService{
@Override
public void generate() {
long begin =System.currentTimeMillis();
//模拟生成订单的延时
try {
Thread.sleep(1234);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已生成!");
long end =System.currentTimeMillis();
System.out.println("执行耗时:"+(end-begin));
}
@Override
public void modify() {
long begin =System.currentTimeMillis();
//模拟修改订单的延时
try {
Thread.sleep(234);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已修改!");
long end =System.currentTimeMillis();
System.out.println("执行耗时:"+(end-begin));
}
@Override
public void detail() {
long begin =System.currentTimeMillis();
//模拟查看订单的延时
try {
Thread.sleep(434);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请查看订单详情!");
long end =System.currentTimeMillis();
System.out.println("执行耗时:"+(end-begin));
}
}
解决方式二:
OrderServiceImplSub类:
public class OrderServiceImplSub extends OrderServiceImpl{
@Override
public void generate() {
long begin = System.currentTimeMillis();
super.generate();
long end =System.currentTimeMillis();
System.out.println("执行耗时:"+(end-begin));
}
@Override
public void modify() {
long begin = System.currentTimeMillis();
super.modify();
long end =System.currentTimeMillis();
System.out.println("执行耗时:"+(end-begin));
}
@Override
public void detail() {
long begin = System.currentTimeMillis();
super.detail();
long end =System.currentTimeMillis();
System.out.println("执行耗时:"+(end-begin));
}
}
解决方式三:
OrderServiceProxy类:
//代理对象(代理对象和目标对象要具有相同的行为,就要实现同一个或同一些接口)
//客户端在使用对象的时候就像在使用目标对象一样
//代理对象
public class OrderServiceProxy implements OrderService{
// 将目标对象作为代理对象的一个属性。这种关系叫做关联关系。比继承关系的耦合度低。
// 代理对象中含有目标对象的引用。关联关系。has a
// 注意:这里要写一个公共接口类型。因为公共接口耦合度低。
private OrderService target;// 这就是目标对象。目标对象一定是实现了OrderService接口的。
//创建代理对象的时候,传一个目标对象给代理对象
public OrderServiceProxy(OrderService target) {
this.target = target;
}
@Override
public void generate() {//代理方法
//增强
long begin =System.currentTimeMillis();
//调用目标对象的目标方法
target.generate();
long end =System.currentTimeMillis();
System.out.println("执行耗时:"+(end-begin));
}
@Override
public void modify() {
//增强
long begin =System.currentTimeMillis();
//调用目标对象的目标方法
target.modify();
long end =System.currentTimeMillis();
System.out.println("执行耗时:"+(end-begin));
}
@Override
public void detail() {
//增强
long begin =System.currentTimeMillis();
//调用目标对象的目标方法
target.detail();
long end =System.currentTimeMillis();
System.out.println("执行耗时:"+(end-begin));
}
}
Test类:
public class Test {
public static void main(String[] args) {
//解决方式一
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.generate();
orderService.modify();
orderService.detail();
//解决方式二
OrderServiceImplSub orderServiceImplSub = new OrderServiceImplSub();
orderServiceImplSub.generate();
orderServiceImplSub.modify();
orderServiceImplSub.detail();
//解决方式三
//创建目标对象
OrderService target = new OrderServiceImpl();
//创建代理对象
OrderService proxy = new OrderServiceProxy(target);
//调用代理对象的代理方法
proxy.generate();
proxy.modify();
proxy.detail();
}
}
以上就是代理模式中的静态代理,其中OrderService接⼝是代理类和⽬标类的共同接⼝。 OrderServiceImpl是⽬标类。OrderServiceProxy是代理类。
14.3 动态代理
在程序运⾏阶段,在内存中动态⽣成代理类,被称为动态代理,⽬的是为了减少代理类的数量。解决代码复⽤的问题。
在内存当中动态⽣成类的技术常⻅的包括:
●
JDK动态代理技术:只能代理接⼝。
●
CGLIB动态代理技术:CGLIB(Code Generation Library)是⼀个开源项⽬。是⼀个强⼤的,⾼性能,⾼质量的Code⽣成类库,它可以在运⾏期扩展Java类与实现Java接⼝。它既可以代理接⼝,⼜可以代理类,
底层是通过继承的⽅式实现的
。性能⽐JDK动态代理要好。
(底层有⼀个⼩⽽快的字节码处理框架ASM。)
●
Javassist动态代理技术:Javassist是⼀个开源的分析、编辑和创建Java字节码的类库。是由东京⼯业⼤学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加⼊了开放源代码JBoss 应⽤服务器项⽬,通过使⽤Javassist对字节码操作为JBoss实现动态"AOP"框架。
14.3.1 JDK动态代理
我们还是使⽤静态代理中的例⼦:⼀个接⼝和⼀个实现类。
OrderService接口:
//订单业务接口
public interface OrderService { // 代理对象和目标对象的公共接口。
String getName();
//生成订单
void generate();
//修改订单信息
void modify();
//查看订单详情
void detail();
}
OrderServiceImpl类:
public class OrderServiceImpl implements OrderService{ // 目标对象
@Override
public String getName() {
System.out.println("getName()方法执行了");
return "张三";
}
@Override
public void generate() { // 目标方法
// 模拟生成订单的耗时
try {
Thread.sleep(1234);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已生成.");
}
@Override
public void modify() { // 目标方法
// 模拟修改订单的耗时
try {
Thread.sleep(456);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已修改.");
}
@Override
public void detail() { // 目标方法
// 模拟查询订单的耗时
try {
Thread.sleep(111);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请看订单详情.");
}
}
TimeInvocationHandler类:
/*
* 专门负责计时的一个调用处理器对象。
* 在这个调用处理器当中编写计时相关的增强代码。
* 这个调用处理器只需要写一个就行了。
**/
public class TimerInvocationHandler implements InvocationHandler {
// 目标对象
private Object target;
public TimerInvocationHandler(Object target) {
// 赋值给成员变量。
this.target = target;
}
/*
1. 为什么强行要求你必须实现InvocationHandler接口?
因为一个类实现接口就必须实现接口中的方法。
以下这个方法必须是invoke(),因为JDK在底层调用invoke()方法的程序已经提前写好了。
注意:invoke方法不是我们程序员负责调用的,是JDK负责调用的。
2. invoke方法什么时候被调用呢?
当代理对象调用代理方法的时候,注册在InvocationHandler调用处理器当中的invoke()方法被调用。
3. invoke方法的三个参数:
invoke方法是JDK负责调用的,所以JDK调用这个方法的时候会自动给我们传过来这三个参数。
我们可以在invoke方法的大括号中直接使用。
第一个参数:Object proxy 代理对象的引用。这个参数使用较少。
第二个参数:Method method 目标对象上的目标方法。(要执行的目标方法就是它。)
第三个参数:Object[] args 目标方法上的实参。
invoke方法执行过程中,使用method来调用目标对象的目标方法。
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 这个接口的目的就是为了让你有地方写增强代码。
//System.out.println("增强1");
long begin = System.currentTimeMillis();
// 调用目标对象上的目标方法
// 方法四要素:哪个对象,哪个方法,传什么参数,返回什么值。
Object retValue = method.invoke(target, args);
//System.out.println("增强2");
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
// 注意这个invoke方法的返回值,如果代理对象调用代理方法之后,需要返回结果的话,invoke方法必须将目标对象的目标方法执行结果继续返回。
return retValue;
}
}
ProxyUtil类:
public class ProxyUtil {
//封装一个工具方法,可以通过这个方法获取代理对象
public static Object newProxyInstance(Object target){
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new TimerInvocationHandler(target));
}
}
我们在静态代理的时候,除了以上⼀个接⼝和⼀个实现类之外,是不是要写⼀个代理类
UserServiceProxy呀!在动态代理中UserServiceProxy代理类是可以动态⽣成的。这个类不需要写。我们直接写客户端程序即可:
Client类:
//客户端程序
public class Client {
public static void main(String[] args) {
// 创建目标对象
OrderService target = new OrderServiceImpl();
// 创建代理对象
/*
1. newProxyInstance 翻译为:新建代理对象
也就是说,通过调用这个方法可以创建代理对象。
本质上,这个Proxy.newProxyInstance()方法的执行,做了两件事:
第一件事:在内存中动态的生成了一个代理类的字节码class。
第二件事:new对象了。通过内存中生成的代理类这个代码,实例化了代理对象。
2. 关于newProxyInstance()方法的三个重要的参数,每一个什么含义,有什么用?
第一个参数:ClassLoader loader
类加载器。这个类加载器有什么用呢?
在内存当中生成的字节码也是class文件,要执行也得先加载到内存当中。加载类就需要类加载器。所以这里需要指定类加载器。
并且JDK要求,目标类的类加载器必须和代理类的类加载器使用同一个。
第二个参数:Class<?>[] interfaces
代理类和目标类要实现同一个接口或同一些接口。
在内存中生成代理类的时候,这个代理类是需要你告诉它实现哪些接口的。
第三个参数:InvocationHandler h
InvocationHandler 被翻译为:调用处理器。是一个接口。
在调用处理器接口中编写的就是:增强代码。
因为具体要增强什么代码,JDK动态代理技术它是猜不到的。没有那么神。
既然是接口,就要写接口的实现类。
可能会有疑问?
自己还要动手写调用处理器接口的实现类,这不会类爆炸吗?不会。
因为这种调用处理器写一次就好。
注意:代理对象和目标对象实现的接口一样,所以可以向下转型。
*/
/*OrderService proxyObj = (OrderService)Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new TimerInvocationHandler(target));*/
// 上面代码通过一个工具类的封装,就简洁了。
OrderService proxyObj =(OrderService) ProxyUtil.newProxyInstance(target);
// 调用代理对象的代理方法
// 注意:调用代理对象的代理方法的时候,如果你要做增强的话,目标对象的目标方法得保证执行。
proxyObj.generate();
proxyObj.modify();
proxyObj.detail();
String name = proxyObj.getName();
System.out.println(name);
}
}
⼤家可能会⽐较好奇:那个InvocationHandler接⼝中的invoke()⽅法没看⻅在哪⾥调⽤呀?
注意:当你调⽤代理对象的代理⽅法的时候,注册在InvocationHandler接⼝中的invoke()⽅法会被调⽤。也就是上⾯代码第24 25 26
⾏,这三⾏代码中任意⼀⾏代码执⾏,注册在InvocationHandler接⼝中的invoke()⽅法都会被调⽤。
我们可以看到,不管你有多少个Service接⼝,多少个业务类,这个TimerInvocationHandler接⼝是不是只需要写⼀次就⾏了,代码是不是得到复⽤了!!!!
⽽且最重要的是,以后程序员只需要关注核⼼业务的编写了,像这种统计时间的代码根本不需要关注。 因为这种统计时间的代码只需要在调⽤处理器中编写⼀次即可。
14.3.2 CGLIB动态代理
CGLIB既可以代理接⼝,⼜可以代理类。底层采⽤继承的⽅式实现。所以被代理的⽬标类不能使⽤final 修饰
使⽤CGLIB,需要引⼊它的依赖:
<dependencies>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
</dependencies>
我们准备⼀个没有实现接⼝的类,如下:
UserService类:
public class UserService {
// 目标方法
public boolean login(String username, String password){
System.out.println("系统正在验证身份...");
if ("admin".equals(username) && "123".equals(password)) {
return true;
}
return false;
}
// 目标方法
public void logout(){
System.out.println("系统正在退出...");
}
}
TimerMethodIntercepter类:
MethodInterceptor接⼝中有⼀个⽅法intercept(),该⽅法有
4
个参数:
第⼀个参数:⽬标对象
第⼆个参数:⽬标⽅法
第三个参数:⽬标⽅法调⽤时的实参
第四个参数:代理⽅法
public class TimerMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
// 前面增强
long begin = System.currentTimeMillis();
// 怎么调用目标对象的目标方法呢?
Object retValue = methodProxy.invokeSuper(target, objects);
// 后面增强
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
return retValue;
}
}
Client类:
public class Client {
public static void main(String[] args) {
// 创建字节码增强器对象
// 这个对象是CGLIB库当中的核心对象,就是依靠它来生成代理类。
Enhancer enhancer = new Enhancer();
// 告诉CGLIB父类是谁。告诉CGLIB目标类是谁。
enhancer.setSuperclass(UserService.class);
// 设置回调(等同于JDK动态代理当中的调用处理器。InvocationHandler)
// 在CGLIB当中不是InvocationHandler接口,是方法拦截器接口:MethodInterceptor
enhancer.setCallback(new TimerMethodInterceptor());
// 创建代理对象
// 这一步会做两件事:
// 第一件事:在内存中生成UserService类的子类,其实就是代理类的字节码。
// 第二件事:创建代理对象。
// 父类是UserService,子类这个代理类一定是UserService
UserService userServiceProxy = (UserService) enhancer.create();
// 建议大家能够把CGLIB动态代理生成的代理对象的名字格式有点印象。
// 根据这个名字可以推测框架底层是否使用了CGLIB动态代理
System.out.println(userServiceProxy);
// 调用代理对象的代理方法。
boolean success = userServiceProxy.login("admin", "123");
System.out.println(success ? "登录成功" : "登录失败");
userServiceProxy.logout();
}
}
// 底层本质
//class UserService$$EnhancerByCGLIB$$82cb55e3 extends UserService{}
和JDK动态代理原理差不多,在CGLIB中需要提供的不是InvocationHandler,⽽是:
net.sf.cglib.proxy.MethodInterceptor
对于⾼版本的JDK,如果使⽤CGLIB,需要在启动项中添加两个启动参数:
●
--add-opens java.base/java.lang=ALL-UNNAMED
●
--add-opens java.base/sun.net.util=ALL-UNNAMED
⼗五、⾯向切⾯编程AOP
IoC使软件组件松耦合。AOP让你能够捕捉系统中经常使⽤的功能,把它转化成组件。
AOP(Aspect Oriented Programming):⾯向切⾯编程,⾯向⽅⾯编程。(AOP是⼀种编程技术)
AOP是对OOP的补充延伸。
AOP底层使⽤的就是动态代理来实现的。
Spring的AOP使⽤的动态代理是:JDK动态代理 + CGLIB动态代理技术。Spring在这两种动态代理中灵活切换,如果是代理接⼝,会默认使⽤JDK动态代理,如果要代理某个类,这个类没有实现接⼝,就会切换使⽤CGLIB。当然,你也可以强制通过⼀些配置让Spring只使⽤CGLIB。
15.1 AOP介绍
⼀般⼀个系统当中都会有⼀些系统服务,例如:⽇志、事务管理、安全等。这些系统服务被称为:
交叉业务。
这些
交叉业务
⼏乎是通⽤的,不管你是做银⾏账户转账,还是删除⽤户数据。⽇志、事务管理、安全, 这些都是需要做的。
如果在每⼀个业务处理过程当中,都掺杂这些交叉业务代码进去的话,存在两⽅⾯问题:
●
第⼀:交叉业务代码在多个业务流程中反复出现,显然这个交叉业务代码没有得到复⽤。并且修改这些交叉业务代码的话,需要修改多处。
●
第⼆:程序员⽆法专注核⼼业务代码的编写,在编写核⼼业务代码的同时还需要处理这些交叉业务。
使⽤AOP可以很轻松的解决以上问题。
请看下图,可以帮助你快速理解AOP的思想:

⽤⼀句话总结AOP:将与核⼼业务⽆关的代码独⽴的抽取出来,形成⼀个独⽴的组件,然后以横向交叉的⽅式应⽤到业务流程当中的过程被称为AOP。
AOP的优点:
●
第⼀:代码复⽤性增强。
●
第⼆:代码易维护。
●
第三:使开发者更关注业务逻辑。
15.2 AOP的七⼤术语
public class UserService{
public void do1(){
System.out.println("do 1");
}
public void do2(){
System.out.println("do 2");
}
public void do3(){
System.out.println("do 3");
}
public void do4(){
System.out.println("do 4");
}
public void do5(){
System.out.println("do 5");
}
// 核⼼业务⽅法
public void service(){
do1();
do2();
do3();
do5();
}
}
●连接点 Joinpoint
○在程序的整个执⾏流程中,
可以织⼊
切⾯的位置。⽅法的执⾏前后,异常抛出之后等位置。
●切点 Pointcut
○在程序执⾏流程中,
真正织⼊
切⾯的⽅法。(⼀个切点对应多个连接点)
●通知 Advice
○通知⼜叫增强,就是具体你要织⼊的代码。
○通知包括:
■前置通知
■后置通知
■环绕通知
■异常通知
■最终通知
●切⾯ Aspect
○
切点 + 通知就是切⾯。
●
织⼊ Weaving
○
把通知应⽤到⽬标对象上的过程。
●
代理对象 Proxy
○
⼀个⽬标对象被织⼊通知后产⽣的新对象。
●
⽬标对象 Target
○
被织⼊通知的对象。
通过下图,⼤家可以很好的理解AOP的相关术语:

15.3 切点表达式
切点表达式⽤来定义通知(Advice)往哪些⽅法上切⼊。
切⼊点表达式语法格式:
execution([ 访问控制权限修饰符 ] 返回值类型 [ 全限定类名 ] ⽅法名 ( 形式参数列表 ) [ 异常 ])
访问控制权限修饰符:
●可选项。
●没写,就是4个权限都包括。
●写public就表示只包括公开的⽅法。
返回值类型:
●必填项。
●* 表示返回值类型任意。
全限定类名:
●可选项。
●两个点“..”代表当前包以及⼦包下的所有类。
●省略时表示所有的类。
⽅法名:
●必填项。
●*表示所有⽅法。
●set*表示所有的set⽅法。
形式参数列表:
●必填项
●() 表示没有参数的⽅法
●(..) 参数类型和个数随意的⽅法
●(*) 只有⼀个参数的⽅法
●(*, String) 第⼀个参数类型随意,第⼆个参数是String的。
异常:
●可选项。
●省略时表示任意异常类型。
理解以下的切点表达式:
service包下所有的类中以delete开始的所有⽅法execution ( public * com . powernode . mall . service . * . delete * (..))
mall包下所有的类的所有的⽅法
execution(* com.powernode.mall..*(..))
所有类的所有⽅法
execution ( * * ( .. ))
15.4 使⽤Spring的AOP
Spring对AOP的实现包括以下
3
种⽅式:
●第⼀种⽅式:Spring框架结合AspectJ框架实现的AOP,基于注解⽅式。
●第⼆种⽅式:Spring框架结合AspectJ框架实现的AOP,基于XML⽅式。
●第三种⽅式:Spring框架⾃⼰实现的AOP,基于XML配置⽅式。
实际开发中,都是Spring+AspectJ来实现AOP。所以我们重点学习第⼀种和第⼆种⽅式。
什么是AspectJ?(Eclipse组织的⼀个⽀持AOP的框架。AspectJ框架是独⽴于Spring框架之外的⼀个框架,Spring框架⽤了AspectJ)
AspectJ项⽬起源于帕洛阿尔托(Palo Alto)研究中⼼(缩写为PARC)。该中⼼由Xerox集团资助,Gregor Kiczales领导,从1997
年开始致⼒于AspectJ的开发,
1998
年第⼀次发布给外部⽤户,
2001
年发布1
.
0
release。为了推动AspectJ技术和社团的发展,PARC在
2003
年
3
⽉正式将AspectJ项⽬移交给了Eclipse组织,因为AspectJ的发展和受关注程度⼤⼤超出了PARC的预期,他们已经⽆⼒继续维持它的发展。
15.4.1 准备⼯作
使⽤Spring+AspectJ的AOP需要引⼊的依赖如下:
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.11</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
Spring配置⽂件中添加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/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">
</beans>
15.4.2 基于AspectJ的AOP注解式开发
实现步骤
第⼀步:定义⽬标类以及⽬标⽅法
@Service
public class UserService {//目标类
public void login(){//目标方法
System.out.println("系统正在进行身份认证!");
}
}
第⼆步:定义切⾯类,在切⾯类中添加通知,在通知上添加切点表达式
@Component
@Aspect //切面类是需要使用@Aspect注解进行标注的
public class LogAspect {//切面
//切面=通知+切点
//通知就是增强,就是具体的要编写的增强的代码
//这里通知Advice以方法的形式出现(因为方法中可以写代码)
//@Before(切点表达式)注解标注的方法就是一个前置通知
@Before("execution(* com.powernode.spring6.service.UserService.*(..))")
public void 增强(){
System.out.println("我是一个通知,我是一段增强代码");
}
}
第三步:在spring配置⽂件中添加组建扫描,在spring配置⽂件中启⽤⾃动代理
<!--组件扫描-->
<context:component-scan base-package="com.powernode.spring6.service"/>
<!--开启aspectj的自动代理-->
<!--spring容器在扫描类的时候,查看该类上是否有@Aspect注解,如果有,则给这个类生成代理对象-->
<!--
proxy-target-class="true" 表示强制使用CGLIB动态代理
proxy-target-class="false" 这是默认值,表示接口使用JDK动态代理,反之使用CGLIB动态代理。
-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
SpringAOPTest类:
@Test
public void testBefore(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.login();
// 我是一个通知,我是一段增强代码
// 系统正在进行身份认证!
}
通知类型
通知类型包括:
●前置通知:@Before ⽬标⽅法执⾏之前的通知
●后置通知:@AfterReturning ⽬标⽅法执⾏之后的通知
●环绕通知:@Around ⽬标⽅法之前添加通知,同时⽬标⽅法执⾏之后添加通知。
●异常通知:@AfterThrowing 发⽣异常之后执⾏的通知
●最终通知:@After 放在finally语句块中的通知
接下来,编写程序来测试这⼏个通知的执⾏顺序:
//前置通知
@Before("execution(* com.powernode.spring6.service..*(..))")
public void beforeAdvice(){
System.out.println("前置通知!");
}
//后置通知
@AfterReturning("execution(* com.powernode.spring6.service..*(..))")
public void afterReturningAdvice(){
System.out.println("后置通知!");
}
//环绕通知(环绕通知时最大的通知,在前置通知之前,在后置通知之后)
@Around("execution(* com.powernode.spring6.service..*(..))")
public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
//前面代码
System.out.println("前环绕!");
//执行目标
joinPoint.proceed();
//后面代码
System.out.println("后环绕!");
}
//异常通知
@AfterThrowing("execution(* com.powernode.spring6.service..*(..))")
public void afterThrowingAdvice(){
System.out.println("异常通知!");
}
//最终通知(finally语句块中的通知)
@After("execution(* com.powernode.spring6.service..*(..))")
public void afterAdvice(){
System.out.println("最终通知!");
}
没有发生异常时:
前环绕! 前置通知! 生成订单 最终通知! 后环绕!
发生异常时:
前环绕! 前置通知! 生成订单 异常通知! 最终通知!
通过测试得知,当发⽣异常之后,最终通知也会执⾏,因为最终通知@After会出现在finally语句块中。出现异常之后,
后置通知
和
环绕通知的结束部分
不会执⾏。
切⾯的先后顺序:
我们知道,业务流程当中不⼀定只有⼀个切⾯,可能有的切⾯控制事务,有的记录⽇志,有的进⾏安全控制,如果多个切⾯的话,顺序如何控制:
可以使⽤@Order注解来标识切⾯类,为@Order注解的value 指定⼀个整数型的数字,数字越⼩,优先级越⾼。
SecurityAspect类:
@Component
@Aspect
@Order(1)
public class SecurityAspect {
//通知+切点
@Before("execution(* com.powernode.spring6.service..*(..))")
public void beforeAdvice(){
System.out.println("前置通知:安全。。。。");
}
}
LogAspect类:
@Component
@Aspect //切面类是需要使用@Aspect注解进行标注的
@Order(2)
public class LogAspect {//切面
//切面=通知+切点
//通知就是增强,就是具体的要编写的增强的代码
//这里通知Advice以方法的形式出现(因为方法中可以写代码)
//@Before(切点表达式)注解标注的方法就是一个前置通知
// @Before("execution(* com.powernode.spring6.service..*(..))")
// public void 增强(){
// System.out.println("我是一个通知,我是一段增强代码");
// }
//前置通知
@Before("execution(* com.powernode.spring6.service..*(..))")
public void beforeAdvice(){
System.out.println("前置通知!");
}
//后置通知
@AfterReturning("execution(* com.powernode.spring6.service..*(..))")
public void afterReturningAdvice(){
System.out.println("后置通知!");
}
//环绕通知(环绕通知时最大的通知,在前置通知之前,在后置通知之后)
@Around("execution(* com.powernode.spring6.service..*(..))")
public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
//前面代码
System.out.println("前环绕!");
//执行目标
joinPoint.proceed();
//后面代码
System.out.println("后环绕!");
}
//异常通知
@AfterThrowing("execution(* com.powernode.spring6.service..*(..))")
public void afterThrowingAdvice(){
System.out.println("异常通知!");
}
//最终通知(finally语句块中的通知)
@After("execution(* com.powernode.spring6.service..*(..))")
public void afterAdvice(){
System.out.println("最终通知!");
}
}
执行结果:
前置通知:安全。。。。
前环绕!
前置通知!
生成订单
后置通知!
最终通知!
后环绕!
缺点是:
第⼀:切点表达式重复写了多次,没有得到复⽤。
第⼆:如果要修改切点表达式,需要修改多处,难维护。
可以这样做:将切点表达式单独的定义出来,在需要的位置引⼊即可。如下:
@Component
@Aspect //切面类是需要使用@Aspect注解进行标注的
@Order(2)
public class LogAspect {//切面
//切面=通知+切点
//通知就是增强,就是具体的要编写的增强的代码
//这里通知Advice以方法的形式出现(因为方法中可以写代码)
//@Before(切点表达式)注解标注的方法就是一个前置通知
// @Before("execution(* com.powernode.spring6.service..*(..))")
// public void 增强(){
// System.out.println("我是一个通知,我是一段增强代码");
// }
//通用切点
@Pointcut("execution(* com.powernode.spring6.service..*(..))")
public void 通用切点(){
//这个方法只是一个标记,方法名随意,方法体也不需要写任何东西
}
//前置通知
@Before("通用切点()")
public void beforeAdvice(JoinPoint joinPoint){
System.out.println("前置通知!");
// 这个JoinPoint joinPoint,在Spring容器调用这个方法的时候自动传过来.
// 我们可以直接用。用这个JoinPoint joinPoint干啥?
// Signature signature = joinPoint.getSignature(); 获取目标方法的签名。
// 通过方法的签名可以获取到一个方法的具体信息。
// 获取目标方法的方法名。
System.out.println("目标方法的方法名:" + joinPoint.getSignature().getName());
}
//后置通知
@AfterReturning("通用切点()")
public void afterReturningAdvice(){
System.out.println("后置通知!");
}
//环绕通知(环绕通知时最大的通知,在前置通知之前,在后置通知之后)
@Around("通用切点()")
public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
//前面代码
System.out.println("前环绕!");
//执行目标
joinPoint.proceed();
//后面代码
System.out.println("后环绕!");
}
//异常通知
@AfterThrowing("通用切点()")
public void afterThrowingAdvice(){
System.out.println("异常通知!");
}
//最终通知(finally语句块中的通知)
@After("通用切点()")
public void afterAdvice(){
System.out.println("最终通知!");
}
}
使⽤@Pointcut注解来定义独⽴的切点表达式。
注意这个@Pointcut注解标注的⽅法随意,只是起到⼀个能够让@Pointcut注解编写的位置。
全注解式开发AOP
就是编写⼀个类,在这个类上⾯使⽤⼤量注解来代替spring的配置⽂件,spring配置⽂件消失了,如下:
Spring6Config类:
@Configuration //代替spring.xml文件
@ComponentScan({"com.powernode.spring6.service"})//组件扫描
@EnableAspectJAutoProxy(proxyTargetClass = true)//启用cglib
public class Spring6Config {
}
@Test
public void testNoXML(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Config.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
orderService.generate();
}
15.4.3 基于XML配置⽅式的AOP(了解)
第⼀步:编写⽬标类UserService:
public class UserService {//目标对象
//目标方法
public void logout(){
System.out.println("系统正在退出....");
}
}
第⼆步:编写切⾯类,并且编写通知TimerAspect:
public class TimerAspect {
//通知
public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
//前环绕
long begin =System.currentTimeMillis();
//目标
joinPoint.proceed();
//后环绕
long end=System.currentTimeMillis();
System.out.println("耗时多少毫秒:"+(end-begin));
}
}
第三步:编写spring配置⽂件
<?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">
<!--纳入spring ioc-->
<bean id="userService" class="com.powernode.spring6.service.UserService"></bean>
<bean id="timerAspect" class="com.powernode.spring6.service.TimerAspect"></bean>
<!--aop的配置-->
<aop:config>
<!--切点表达式-->
<aop:pointcut id="mypointcut" expression="execution(* com.powernode.spring6.service..*(..))"/>
<!--切面:通知+切点-->
<aop:aspect ref="timerAspect">
<aop:around method="aroundAdvice" pointcut-ref="mypointcut"/>
</aop:aspect>
</aop:config>
</beans>
测试程序:
public class SpringAOPTest {
@Test
public void testXml(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.logout();
}
}
15.5 AOP的实际案例:事务处理
项⽬中的事务控制是在所难免的。在⼀个业务流程当中,可能需要多条DML语句共同完成,为了保证数据的安全,这多条DML语句要么同时成功,要么同时失败。这就需要添加事务控制的代码。例如以下伪代码:
class 业务类1{
public void 业务⽅法1(){
try{
// 开启事务
startTransaction();
// 执⾏核⼼业务逻辑
step1();
step2();
step3();
....
// 提交事务
commitTransaction();
}catch(Exception e){
// 回滚事务
rollbackTransaction();
}
}
public void 业务⽅法2(){
try{
// 开启事务
startTransaction();
// 执⾏核⼼业务逻辑
step1();
step2();
step3();
....
// 提交事务
commitTransaction();
}catch(Exception e){
// 回滚事务
rollbackTransaction();
}
}
.........
这个控制事务的代码就是和业务逻辑没有关系的“
交叉业务
”。以上伪代码当中可以看到这些交叉业务的代码没有得到复⽤,并且如果这些交叉业务代码需要修改,那必然需要修改多处,难维护,怎么解决?
可以采⽤AOP思想解决。可以把以上控制事务的代码作为环绕通知,切⼊到⽬标类的⽅法当中。接下来我们做⼀下这件事,有两个业务类,如下:
AccountService类:
@Service
public class AccountService {//目标对象
//目标方法
//转账的业务方法
public void transfer(){
System.out.println("银行账户正在完成转账的业务!");
}
//目标方法
//取款的业务方法
public void withdraw(){
System.out.println("正在取款,请稍后!");
}
}
OrderService类:
@Service
public class OrderService {//目标对象
//目标方法
//生成订单的业务方法
public void generate(){
System.out.println("正在生成订单!");
}
//目标方法
//取消订单的业务方法
public void cancel(){
System.out.println("订单已取消!");
String s=null;
s.toString();
}
}
TransactionAspect类:
@Component
@Aspect
public class TransactionAspect {
//编程式解决方案
@Around("execution(* com.powernode.spring6.service..*(..))")
public void aroundAspect(ProceedingJoinPoint joinPoint){
//执行目标
try {
//前环绕
System.out.println("开启事物");
joinPoint.proceed();
//后环绕
System.out.println("提交事物");
} catch (Throwable e) {
System.out.println("回滚事物");
}
}
}
spring.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">
<!--组件扫描-->
<context:component-scan base-package="com.powernode.spring6.service"/>
<!--启动自动代理-->
<aop:aspectj-autoproxy/>
</beans>
public class AOPRealAppTest {
@Test
public void testTransaction(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
accountService.transfer();
accountService.withdraw();
orderService.generate();
orderService.cancel();
// 开启事物
// 银行账户正在完成转账的业务!
// 提交事物
// 开启事物
// 正在取款,请稍后!
// 提交事物
// 开启事物
// 正在生成订单!
// 提交事物
// 开启事物
// 订单已取消!
// 回滚事物
}
}
你看,这个事务控制代码是不是只需要写⼀次就⾏了,并且修改起来也没有成本。通过测试可以看到,所有的业务⽅法都添加了事务控制的代码。
15.6 AOP的实际案例:安全⽇志
需求是这样的:项⽬开发结束了,已经上线了。运⾏正常。客户提出了新的需求:凡事在系统中进⾏修改操作的,删除操作的,新增操作的,都要把这个⼈记录下来。因为这⼏个操作是属于危险⾏为。例如有业务类和业务⽅法:
VipService类:
@Service
public class VipService {
public void saveVip(){
System.out.println("新增会员信息");
}
public void deleteVip(){
System.out.println("删除会员信息");
}
public void modifyVip(){
System.out.println("修改会员信息");
}
public void getVip(){
System.out.println("获取会员信息");
}
}
UserService类:
@Service
public class UserService {
public void saveUser(){
System.out.println("新增用户信息");
}
public void deleteUser(){
System.out.println("删除用户信息");
}
public void modifyUser(){
System.out.println("修改用户信息");
}
public void getUser(){
System.out.println("获取用户信息");
}
}
SecurityLogAspect类:
@Component
@Aspect
public class SecurityLogAspect {
@Pointcut("execution(* com.powernode.spring6.biz..save*(..))")
public void savePointcut(){}
@Pointcut("execution(* com.powernode.spring6.biz..delete*(..))")
public void deletePointcut(){}
@Pointcut("execution(* com.powernode.spring6.biz..modify*(..))")
public void modifyPointcut(){}
// @Pointcut("execution(* com.powernode.spring6.biz..get*(..))")
// public void getPointcut(){}
@Before("savePointcut() || deletePointcut() || modifyPointcut()")
public void beforeAspect(JoinPoint joinPoint){
//系统时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String nowTime = sdf.format(new Date());
//输出日志信息
System.out.println(nowTime+" zhangsan:"+joinPoint.getSignature().getDeclaringTypeName()+"."+joinPoint.getSignature().getName());
}
}
spring.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">
<!--组件扫描-->
<context:component-scan base-package="com.powernode.spring6"/>
<!--启动自动代理-->
<aop:aspectj-autoproxy/>
</beans>
@Test
public void testSecurityLog(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
VipService vipService = applicationContext.getBean("vipService", VipService.class);
userService.saveUser();
userService.deleteUser();
userService.modifyUser();
userService.getUser();
vipService.saveVip();
vipService.deleteVip();
vipService.modifyVip();
vipService.getVip();
// 2023-08-20 20:30:04 574 zhangsan:com.powernode.spring6.biz.UserService.saveUser
// 新增用户信息
// 2023-08-20 20:30:04 576 zhangsan:com.powernode.spring6.biz.UserService.deleteUser
// 删除用户信息
// 2023-08-20 20:30:04 576 zhangsan:com.powernode.spring6.biz.UserService.modifyUser
// 修改用户信息
// 获取用户信息
// 2023-08-20 20:30:04 576 zhangsan:com.powernode.spring6.biz.VipService.saveVip
// 新增会员信息
// 2023-08-20 20:30:04 577 zhangsan:com.powernode.spring6.biz.VipService.deleteVip
// 删除会员信息
// 2023-08-20 20:30:04 577 zhangsan:com.powernode.spring6.biz.VipService.modifyVip
// 修改会员信息
// 获取会员信息
}