循序渐进学 Spring (下):从注解、AOP到底层原理与整合实战
文章目录
- 7. 自动装配 (Autowiring)
- 7.1 XML 自动装配
- 7.2 使用注解实现自动装配
- `@Autowired` vs `@Resource`
- 8. 使用注解开发(完全体)
- 8.1 定义 Bean (`@Component` 及其衍生注解)
- 8.2 注入属性 (`@Value`)
- 8.3 注入对象
- 8.4 定义作用域 (`@Scope`)
- 8.5 小结:XML vs. 注解
- 9. 使用 Java 配置 (JavaConfig)
- 10、代理模式
- 10.1 静态代理
- 角色分析
- 代码实现
- 优缺点
- 10.2 静态代理再理解
- 代码实现
- 10.3 动态代理
- 代码实现
- 10.4 动态代理再理解
- 通用处理器
- 测试
- 动态代理的好处
- 11、AOP(TODO:未手敲)
- 11.1 什么是AOP
- 11.2 AOP在Spring中的作用
- 11.3 使用Spring实现AOP
- 1. 导入AOP依赖
- 方式一:使用Spring原生API接口
- 方式二:自定义类来实现AOP
- 方式三:使用注解实现
- **知识点:`<aop:aspectj-autoproxy/>`**
- 11.4 三种实现 AOP 方式的对比
- 相同点 (Core Principles)
- 不同点 (Evolution & Style)
- 12、整合MyBatis
- 整合步骤概览
- 1. 导入Maven依赖
- 2. 配置Maven静态资源过滤
- 12.1 回忆MyBatis
- 1. 实体类 (User.java)
- 2. MyBatis核心配置文件 (mybatis-config.xml)
- 3. Mapper接口 (UserMapper.java)
- 4. Mapper XML文件 (UserMapper.xml)
- 5. 测试类
- 12.2 MyBatis-Spring核心概念
- 12.3 整合实现方式一:SqlSessionTemplate
- 1. 创建Spring数据层配置文件 (spring-dao.xml)
- 2. 创建Mapper实现类 (UserMapperImpl.java)
- 3. 创建Spring主配置文件 (applicationContext.xml)
- 4. 测试
- 12.4 整合实现方式二:SqlSessionDaoSupport
- 1. 修改Mapper实现类 (UserMapperImpl2.java)
- 2. 修改Spring配置 (applicationContext.xml)
- 3. 测试 (类似方式一)
- 13、声明式事务
- 13.1 回顾事务
- 13.2 事务失效场景测试
- 13.3 Spring中的事务管理
- 配置声明式事务(XML方式)
- 再次测试
- 写在最后
- 参考
你好,我是 ZzzFatFish,欢迎回到我的 Spring 学习笔记。在 循序渐进学 Spring (上):从 IoC/DI 核心原理到 XML 配置实战 中,我们深入探讨了 Spring 的核心 IoC/DI 以及 XML 配置的各种方式。这次,我们将更进一步,探索如何让 Spring 变得更“聪明”,从自动装配开始,逐步走向现代化的注解和 Java 配置,并最终揭开 AOP 的神秘面纱,完成与 MyBatis 的整合实战。
7. 自动装配 (Autowiring)
自动装配是 Spring IoC 容器的一项强大功能,它可以自动满足 Bean 之间的依赖关系,减少 XML 中的显式配置。
7.1 XML 自动装配
通过在 <bean>
标签上设置 autowire
属性实现。
环境准备
public class Cat { public void shout() { System.out.println("喵~"); } }
public class Dog { public void shout() { System.out.println("汪~"); } }
public class People {private Cat cat;private Dog dog;private String name;// ... getters and setters and toString
}
beans.xml
<bean id="cat" class="com.github.subei.pojo.Cat"/>
<bean id="dog" class="com.github.subei.pojo.Dog"/><!-- 手动装配 (传统方式) -->
<bean id="peopleManual" class="com.github.subei.pojo.People"><property name="cat" ref="cat"/><property name="dog" ref="dog"/>
</bean><!-- 手动装配 (传统方式2) -->
<bean id="people" class="com.github.pojo.People" p:cat-ref="cat" p:dog-ref="dog"/><!-- 自动装配 byName -->
<bean id="peopleByName" class="com.github.subei.pojo.People" autowire="byName"/><!-- 自动装配 byType -->
<bean id="peopleByType" class="com.github.subei.pojo.People" autowire="byType"/>
autowire="byName"
: Spring 会在容器中查找 id 与People
类中属性名(cat
,dog
)相同的 Bean,并进行注入。autowire="byType"
: Spring 会在容器中查找与People
类中属性类型(Cat
,Dog
)相匹配的 Bean,并进行注入。- 注意:使用
byType
时,容器中同类型的 Bean 必须是唯一的,否则会抛出异常。
- 注意:使用
public class MyTest {@Testpublic void test(){ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");People people = context.getBean("people", People.class);people.getCat().shout();people.getDog().shout();}
}
7.2 使用注解实现自动装配
注解是目前更主流的自动装配方式,它将配置信息直接写在 Java 类中,更为便捷。
步骤 1:开启注解支持
在 XML 配置文件中,需要添加 context
命名空间,并开启注解扫描。
<?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"xsi:schemaLocation="..."><!-- 开启基于注解的配置,它会激活 @Autowired, @Resource 等注解 --><context:annotation-config/><!-- 仍然需要将 Bean 定义在 XML 中 --><bean id="cat" class="com.github.subei.pojo.Cat"/><bean id="dog" class="com.github.subei.pojo.Dog"/><bean id="people" class="com.github.subei.pojo.People"/></beans>
步骤 2:在类中使用注解
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import javax.annotation.Resource;public class People {@Autowired // 1. 使用 @Autowired@Qualifier("cat") // 2. 当有多个同类型Bean时,用 @Qualifier 指定名称private Cat cat;// @Autowired// private Dog dog;@Resource(name = "dog") // 3. 使用 @Resource (更推荐)private Dog dog;// ...
}
@Autowired
vs @Resource
这是非常重要的一个区别点:
特性 | @Autowired (Spring 提供) | @Resource (JSR-250, Java 标准) |
---|---|---|
装配顺序 | 1. 按类型 (byType ) 在容器中查找。 | 1. 按名称 (byName ) 查找。 |
2. 如果找到多个,再按名称 (byName ) 查找。 | 2. 如果按名称找不到,再按类型 (byType ) 查找。 | |
3. 如果仍未确定,可配合 @Qualifier("beanId") 使用。 | 3. 如果按类型也找不到或找到多个,则报错。 | |
依赖 | 强依赖 Spring 框架。 | 是 Java 标准,减少了对 Spring 的耦合。 |
常用场景 | 简单场景,或需要 @Qualifier 精准控制时。 | 推荐使用,其装配顺序更符合直觉,更明确。 |
@Autowired(required = false)
:可以标注某个属性不是必须的,如果容器中找不到对应的 Bean,该属性为null
而不会报错。
观点:有人认为“注解一时爽,维护火葬场”,是因为当项目庞大、依赖关系复杂时,注解分散在各个类中,不如 XML 集中管理来得清晰。但在现代开发中,注解的便利性已成为主流,配合良好的设计可以很好地管理。
8. 使用注解开发(完全体)
之前的注解只是用于自动装配,Bean本身还是在XML中定义的。现在我们学习使用注解来定义Bean,从而可以完全替代XML。
步骤 1:开启组件扫描
使用 <context:component-scan>
来代替 <context:annotation-config/>
。它不仅会开启注解支持,还会扫描指定包下的类,并将带有特定注解的类自动注册为 Bean。
<?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"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsd"><!--指定要扫描的包,Spring 会自动查找这个包及其子包下所有带注解的类。这个标签包含了 <context:annotation-config/> 的功能。
--><context:component-scan base-package="com.github.pojo"/></beans>
步骤 2:使用注解定义 Bean 及其属性
8.1 定义 Bean (@Component
及其衍生注解)
@Component
: 通用的组件注解,表示这个类是一个由 Spring 管理的 Bean。默认的 beanId 是类名首字母小写(如User
->user
)。@Component("customId")
可以自定义 beanId。
为了更好地标识分层架构,@Component
有三个衍生注解,它们在功能上完全相同,但语义更清晰:
@Repository
: 用于标注数据访问层 (DAO) 的组件。@Service
: 用于标注业务逻辑层 (Service) 的组件。@Controller
: 用于标注表现层 (Controller) 的组件。
import org.springframework.stereotype.Component;@Component // 等价于 <bean id="user" class="com.github.subei.pojo.User"/>
public class User {// 相当于 <property name="name" value="subeiLY"/>@Value("subeiLY")public String name;// ...
}
8.2 注入属性 (@Value
)
@Value
注解用于注入基本类型和 String
类型的值。
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;@Component
public class User {// 相当于 <property name="name" value="subeiLY"/>@Value("subeiLY")public String name;
}
8.3 注入对象
使用 @Autowired
或 @Resource
,与第 7 节相同。
8.4 定义作用域 (@Scope
)
使用 @Scope
注解来定义 Bean 的作用域。
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;@Component
@Scope("prototype") // 等价于 <bean ... scope="prototype"/>
public class User { ... }
8.5 小结:XML vs. 注解
- XML:配置集中,一目了然,适用于所有场景。但当 Bean 数量多时,会很繁琐。
- 注解:配置分散在代码中,开发便捷。但对于第三方库的类(我们无法修改源码),则无法使用注解。
最佳实践(混合使用):
- XML:负责整体配置,如数据源、事务管理器、组件扫描
<context:component-scan>
等。 - 注解:负责业务类(Controller, Service, DAO)的 Bean 定义和依赖注入。
9. 使用 Java 配置 (JavaConfig)
从 Spring 3.0 开始,官方提供了完全基于 Java 类来进行配置的方式,可以彻底摆脱 XML 文件。
步骤 1:创建配置类
配置类使用 @Configuration
标注,它本身也是一个 @Component
。
package com.github.subei.config;import com.github.subei.pojo.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;@Configuration // 声明这是一个配置类,替代 beans.xml
@ComponentScan("com.github.subei.pojo") // 扫描组件,等同于 <context:component-scan>
@Import(AnotherConfig.class) // 导入其他配置类,等同于 <import>
public class MyConfig {// @Bean 注解表示这个方法将返回一个对象,该对象将被注册为 Spring 容器中的 Bean。// 方法名 `getUser` 默认成为 bean 的 id。// 返回值类型 `User` 相当于 <bean> 标签的 class 属性。@Beanpublic User getUser() {return new User(); // 返回要注入到容器中的对象}
}
步骤 2:创建实体类(可以与JavaConfig配合使用)
package com.github.subei.pojo;import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;// @Component注解可以让 @ComponentScan 扫描到
@Component
public class User {private String name;public String getName() { return name; }@Value("KANGSHIFU") // 注入属性值public void setName(String name) { this.name = name; }// ... toString
}
步骤 3:测试
当完全使用 Java 配置时,需要用 AnnotationConfigApplicationContext
来加载容器。
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class MyTest {public static void main(String[] args) {// 通过 AnnotationConfigApplicationContext 来获取容器,参数是配置类的 Class 对象ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);// beanId 默认为方法名 "getUser"User user = (User) context.getBean("getUser");System.out.println(user.getName());}
}
好的,我已经帮你把笔记重新整理和排版,并针对你遇到的问题进行了一些补充说明。我保留了你所有的原始内容,包括你的个人备注,只是让整体结构更清晰、更易于阅读。
10、代理模式
为什么要学习代理模式?
因为这就是 Spring AOP 的底层实现原理!
代理模式的分类:
- 静态代理
- 动态代理
10.1 静态代理
角色分析
- 抽象角色 (Subject):一般会使用接口或者抽象类来定义。
- 真实角色 (Real Subject):被代理的角色,真正执行业务逻辑的类。
- 代理角色 (Proxy):代理真实角色,在真实角色执行前后,可以附加一些操作。
- 客户 (Client):访问代理角色的人。
代码实现
1. 接口 (Rent.java)
package com.github.subei.demo;// 租房
public interface Rent {public void rent();
}
2. 真实角色 (Host.java)
package com.github.subei.demo;// 房东
public class Host implements Rent{public void rent(){System.out.println("房东要出租房子!");}
}
3. 代理角色 (Proxy.java)
package com.github.subei.demo;public class Proxy implements Rent { // 注意:代理类也应该实现同一个接口private Host host;public Proxy() {}public Proxy(Host host) {this.host = host;}public void rent(){seeHouse();host.rent(); // 调用真实角色的方法contract();fare();}// 看房public void seeHouse(){System.out.println("中介带你看房!");}// 收中介费public void fare(){System.out.println("收中介费!");}// 签合同public void contract(){System.out.println("和你签合同!");}
}
4. 客户端 (Client.java)
package com.github.subei.demo;public class Client {public static void main(String[] args) {// 房东要租房子Host host = new Host();// 代理,中介帮房东租房子,但是代理角色一般会有一些附属操作!Proxy proxy = new Proxy(host);// 我们不直接找房东,而是直接找中介租房proxy.rent();}
}
优缺点
- 优点:
- 可以使得我们的真实角色更加纯粹,不再去关注一些公共的业务。
- 公共的业务由代理来完成,实现了业务的分工。
- 公共业务发生扩展时,方便集中管理。
- 缺点:
- 一个真实角色就会产生一个代理角色,代码量会翻倍,开发效率会变低。
10.2 静态代理再理解
以用户管理业务为例,日志功能就是可以被代理的公共业务。
代码实现
1. 抽象角色 (UserService.java)
package com.github.subei.demo2;// 实现增删改查业务
public interface UserService {void add();void delete();void update();void query();
}
2. 真实角色 (UserServiceImpl.java)
package com.github.subei.demo2;public class UserServiceImpl implements UserService {public void add() {System.out.println("添加用户");}public void delete() {System.out.println("删除用户");}public void update() {System.out.println("更新用户");}public void query() {System.out.println("查询用户");}
}
3. 代理角色 (UserServiceProxy.java)
package com.github.subei.demo2;public class UserServiceProxy implements UserService {private UserServiceImpl userService;public void setUserService(UserServiceImpl userService) {this.userService = userService;}public void add() {log("add");userService.add();}public void delete() {log("delete");userService.delete();}public void update() {log("update");userService.update();}public void query() {log("query");userService.query();}// 日志方法public void log(String msg){System.out.println("执行了 " + msg + " 方法");}
}
4. 客户端 (Client.java)
package com.github.subei.demo2;public class Client {public static void main(String[] args) {// 真实业务对象UserServiceImpl userService = new UserServiceImpl();// 代理类UserServiceProxy proxy = new UserServiceProxy();// 设置要代理的真实对象proxy.setUserService(userService);proxy.add();proxy.query();}
}
思考: 这种开发模式像是纵向的业务开发中,切入了横向的功能(如日志)。
我们想要静态代理的好处,又不想要它的缺点,于是就有了 动态代理!
10.3 动态代理
- 动态代理和静态代理的角色一样。
- 动态代理的代理类是动态生成的,不是我们直接写好的。
- 动态代理分类:
- 基于接口的动态代理 — JDK 动态代理【本例使用】
- 基于类的动态代理 — CGLIB
- Java 字节码实现 — Javassist
需要了解两个核心类:
java.lang.reflect.Proxy
和java.lang.reflect.InvocationHandler
。
代码实现
1. 抽象角色 (Rent.java)
package com.github.subei.demo3;public interface Rent {void rent();
}
2. 真实角色 (Host.java)
package com.github.subei.demo3;public class Host implements Rent {public void rent(){System.out.println("房东要出租房子!");}
}
3. 代理处理程序 (ProxyInvocationHandler.java)
package com.github.subei.demo3;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;// 会用这个类,自动生成代理类
public class ProxyInvocationHandler implements InvocationHandler {// 被代理的接口private Rent rent;public void setRent(Rent rent){this.rent = rent;}// 生成得到代理类public Object getProxy(){return Proxy.newProxyInstance(this.getClass().getClassLoader(),rent.getClass().getInterfaces(),this);}// 处理代理实例,并返回代理结果// 这个方法是代理对象调用任何接口方法时都会执行的public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 动态代理的本质就是利用反射机制seeHouse();// 调用真实对象的方法Object result = method.invoke(rent, args);fare();return result;}// 附加操作public void seeHouse(){System.out.println("中介带你看房!");}public void fare(){System.out.println("收中介费!");}
}
4. 客户端 (Client.java)
package com.github.subei.demo3;public class Client {public static void main(String[] args) {// 真实角色Host host = new Host();// 代理角色:现在没有具体的代理类,只有一个处理器ProxyInvocationHandler handler = new ProxyInvocationHandler();// 通过调用程序来处理我们要调用的接口对象!handler.setRent(host);// 动态生成对应的代理类!Rent proxy = (Rent) handler.getProxy(); proxy.rent();}
}
核心: 一个动态代理处理器,一般代理某一类业务,可以代理实现了同一接口的多个类。
10.4 动态代理再理解
我们可以编写一个通用的动态代理处理器,让它能代理任何对象。
通用处理器
package com.github.subei.Demo4;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;// 通用的动态代理处理器
public class ProxyInvocationHandler implements InvocationHandler {// 被代理的对象,设置为Object类型private Object target;public void setTarget(Object target){this.target = target;}// 生成得到代理类public Object getProxy(){return Proxy.newProxyInstance(this.getClass().getClassLoader(),target.getClass().getInterfaces(),this);}// 处理代理实例,并返回代理结果public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {log(method.getName());Object result = method.invoke(target, args);return result;}public void log(String methodName){System.out.println("执行了 " + methodName + " 方法");}
}
测试
package com.github.subei.Demo4;import com.github.subei.demo2.UserService;
import com.github.subei.demo2.UserServiceImpl;public class Client {public static void main(String[] args) {// 真实角色UserServiceImpl userService = new UserServiceImpl();// 代理角色处理器ProxyInvocationHandler pih = new ProxyInvocationHandler();// 设置要代理的对象pih.setTarget(userService);// 动态生成代理类!UserService proxy = (UserService)pih.getProxy();proxy.add();}
}
动态代理的好处
- 使得我们的真实角色更加纯粹,不再关注公共业务。
- 公共业务由代理完成,实现了业务分工。
- 公共业务发生扩展时,方便集中管理。
- 一个动态代理处理器可以代理多个类,只要它们实现了接口。
11、AOP(TODO:未手敲)
11.1 什么是AOP
AOP(Aspect Oriented Programming),意为:面向切面编程。通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
AOP是OOP(面向对象编程)的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
11.2 AOP在Spring中的作用
提供声明式事务;允许用户自定义切面。
AOP 核心概念:
- 横切关注点 (Cross-cutting concerns):
跨越应用程序多个模块的功能。即与我们业务逻辑无关,但我们又需要关注的部分,就是横切关注点。如:日志、安全、缓存、事务等等。 - 切面 (Aspect):
横切关注点被模块化的特殊对象。在代码中通常是一个类。 - 通知 (Advice):
切面必须要完成的工作,即切面类中的方法。 - 目标 (Target):
被通知的对象,即被代理的真实对象。 - 代理 (Proxy):
向目标对象应用通知之后创建的对象。 - 切入点 (Pointcut):
切面通知**执行的“地点”**的定义。 - 连接点 (JoinPoint):
与切入点匹配的执行点,在Spring中,连接点就是方法的执行。
Spring AOP 中,通过 Advice 定义横切逻辑,Spring 支持 5 种类型的 Advice。
核心思想: AOP 就是在不改变原有代码的情况下,去增加新的功能。
11.3 使用Spring实现AOP
1. 导入AOP依赖
使用AOP织入,需要导入aspectjweaver
依赖包。
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.4</version> <!-- 版本号可根据项目情况调整 -->
</dependency>
方式一:使用Spring原生API接口
1. 业务接口和实现类
// UserService.java
package com.github.subei.service;
public interface UserService {void add();void delete();void select();void update();
}// UserServiceImpl.java
package com.github.subei.service;
public class UserServiceImpl implements UserService{public void add() { System.out.println("增加了一个用户"); }public void delete() { System.out.println("删除了一个用户"); }public void select() { System.out.println("查询了一个用户"); }public void update() { System.out.println("更新了一个用户"); }
}
2. 前置/后置增强类
// Log.java (前置通知)
package com.github.subei.log;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;public class Log implements MethodBeforeAdvice {// method: 要执行的目标对象的方法// args: 参数// target: 目标对象public void before(Method method, Object[] args, Object target) throws Throwable {System.out.println(target.getClass().getName() + "的" + method.getName() + "方法被执行了!");}
}// AfterLog.java (后置通知)
package com.github.subei.log;
import org.springframework.aop.AfterReturningAdvice;
import java.lang.reflect.Method;public class AfterLog implements AfterReturningAdvice {// returnValue: 返回值public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {System.out.println("执行了" + target.getClass().getName() + "的" + method.getName() + "方法,返回结果为:" + returnValue);}
}
3. Spring配置 (applicationContext.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:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop.xsd"><!-- 注册bean --><bean id="userService" class="com.github.subei.service.UserServiceImpl"/><bean id="log" class="com.github.subei.log.Log"/><bean id="afterLog" class="com.github.subei.log.AfterLog"/><!-- 方式一: 使用原生Spring API接口 --><aop:config><!-- 切入点: expression:表达式, execution(要执行的位置!* * * *) --><aop:pointcut id="pointcut" expression="execution(* com.github.subei.service.UserServiceImpl.*(..))"/><!-- 配置通知器 aop:advisor --><aop:advisor advice-ref="log" pointcut-ref="pointcut"/><aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/></aop:config></beans>
4. 测试
import com.github.subei.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;public class MyTest {public static void main(String[] args) {ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");// 动态代理代理的是接口,所以要用接口类型接收UserService userService = context.getBean("userService", UserService.class);userService.select();}
}
方式二:自定义类来实现AOP
1. 编写自定义切面类 (POJO)
package com.github.subei.diy;public class DiyPointCut {public void before(){System.out.println("---------方法执行前---------");}public void after(){System.out.println("---------方法执行后---------");}
}
或:
public class DiyPointCut {// "前置通知"方法现在可以接收一个JoinPoint对象public void before(JoinPoint jp) {System.out.println("---------方法执行前---------");// 1. 获取目标对象System.out.println("目标对象: " + jp.getTarget());// 2. 获取方法签名,从而得到方法名和类名System.out.println("拦截的方法: " + jp.getSignature().getName());System.out.println("方法所属类: " + jp.getSignature().getDeclaringTypeName());// 3. 获取方法的参数System.out.println("方法参数: " + Arrays.toString(jp.getArgs()));}// "后置通知"方法同样可以获取这些信息public void after(JoinPoint jp) {System.out.println("---------方法执行后---------");System.out.println("方法 " + jp.getSignature().getName() + " 执行完毕。");}
}
2. Spring配置
<!-- 注册自定义切面 bean -->
<bean id="diy" class="com.github.subei.diy.DiyPointCut"/><aop:config><!-- 使用 aop:aspect 定义切面 --><aop:aspect ref="diy"><!-- 定义切入点 --><aop:pointcut id="point" expression="execution(* com.github.subei.service.UserServiceImpl.*(..))"/><!-- 定义通知 --><aop:before method="before" pointcut-ref="point" /><aop:after method="after" pointcut-ref="point"/></aop:aspect>
</aop:config>
3. 测试 (同上)
方式三:使用注解实现
1. 编写注解实现的切面类
package com.github.subei.diy;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;// 使用注解方式实现AOP
@Aspect // 标注这个类是一个切面
public class AnnotationPointCut {@Before("execution(* com.github.subei.service.UserServiceImpl.*(..))")public void before(){System.out.println("---------方法执行前(注解)---------");}@After("execution(* com.github.subei.service.UserServiceImpl.*(..))")public void after(){System.out.println("---------方法执行后(注解)---------");}// 在环绕增强中,我们可以给定一个参数,代表我们要获取处理切入的点@Around("execution(* com.github.subei.service.UserServiceImpl.*(..))")public void around(ProceedingJoinPoint jp) throws Throwable {System.out.println("环绕前");Signature signature = jp.getSignature(); // 获得签名System.out.println("签名: " + signature);// 执行目标方法: proceedObject proceed = jp.proceed();System.out.println("环绕后");System.out.println("执行结果: " + proceed);}
}
2. Spring配置文件
<!-- 第三种方法: 使用注解方式实现 --><!-- 1. 注册带有@Aspect注解的切面类 -->
<bean id="annotationPointCut" class="com.github.subei.diy.AnnotationPointCut"/><!-- 2. 开启注解支持!(这会让Spring自动寻找@Aspect注解的bean并创建代理) -->
<aop:aspectj-autoproxy/>
知识点:<aop:aspectj-autoproxy/>
- 通过AOP命名空间的
<aop:aspectj-autoproxy />
声明,可以自动为Spring容器中那些配置了@AspectJ
切面的bean创建代理,织入切面。 - 它有一个
proxy-target-class
属性,默认为false
:proxy-target-class="false"
(默认): 使用 JDK动态代理 织入增强。目标类必须实现接口。proxy-target-class="true"
: 使用 CGLIB动态代理 技术织入增强。即使目标类没有实现接口,也可以创建代理。- 注意: 即使
proxy-target-class
设置为false
,如果目标类没有声明任何接口,Spring 将自动切换到使用 CGLIB。
11.4 三种实现 AOP 方式的对比
对比维度 (Feature) | 方式一:Spring API接口 | 方式二:自定义类 + XML配置 | 方式三:注解方式 (@AspectJ) |
---|---|---|---|
实现方式 | 切面类必须实现Spring特定的接口,如 MethodBeforeAdvice 。 | 切面类是一个普通的Java类 (POJO),不需要实现任何接口。 | 切面类是一个普通的Java类 (POJO),但使用 @Aspect 注解标识。 |
与Spring框架的耦合度 | 高。代码强依赖于org.springframework.aop.* 包,可移植性差。 | 低。切面类本身完全不依赖Spring,可以独立存在。 | 中等。依赖于org.aspectj.* 注解包,但与Spring核心API解耦。 |
配置方式 | 完全通过XML配置。使用<aop:advisor> 标签将通知和切入点绑定。 | 完全通过XML配置。使用<aop:aspect> 标签引用切面Bean,并指定方法。 | XML中只需开启注解支持<aop:aspectj-autoproxy/> ,具体逻辑在Java类中通过注解完成。 |
易用性与可读性 | 差。代码最繁琐,需要为不同类型的通知创建不同的类,结构分散。 | 中等。逻辑与配置分离,需要来回查看XML和Java文件才能理解整体。 | 好。逻辑、切入点、通知类型都集中在一个类中,代码即配置,内聚性高,可读性最好。 |
功能强大性 | 较弱。仅提供基本的前置、后置等通知,功能有限。 | 较强。支持前置、后置、环绕、异常等所有通知类型,但配置在XML中。 | 最强。完全支持AspectJ的所有功能,特别是@Around 环绕通知,可以精确控制目标方法的执行。 |
当前主流用法 | 已过时,基本不再使用。 | 在一些需要将AOP配置与业务代码完全解耦的遗留项目中可能见到。 | 绝对主流,是当前Spring/Spring Boot项目开发的首选和推荐方式。 |
相同点 (Core Principles)
- 核心思想一致:三种方式都是为了实现AOP(面向切面编程),将横切关注点(如日志、事务)与业务逻辑代码分离。
- 底层实现一致:它们的底层都依赖于Spring在运行时创建动态代理对象(JDK动态代理或CGLIB代理)来织入切面逻辑。
- 依赖一致:都需要在项目中引入
aspectjweaver
这个依赖包。 - 切入点表达式一致:定义切入点时,使用的
execution()
表达式语法是完全相同的。
不同点 (Evolution & Style)
-
最大区别在于“耦合度”和“配置风格”:
- 方式一是强耦合、纯XML配置。代码和Spring API绑死。
- 方式二是低耦合、纯XML配置。代码是干净的POJO,但AOP的“身份”和行为完全由外部XML赋予。
- 方式三是中等耦合、注解驱动。代码通过注解自我声明其AOP“身份”和行为,XML只负责开启总开关。
-
代码的侵入性不同:
- 方式一侵入性最强,因为它强制你的类去实现它的接口。
- 方式二和方式三对业务代码都是非侵入式的,这也是AOP提倡的。
-
发展趋势和推荐度不同:
这三种方式清晰地展示了Spring AOP的演进路线:从早期与框架紧密绑定的API,到配置与代码分离,再到最终使用注解将配置与逻辑内聚。方式三(注解)无疑是目前最佳的实践,因为它兼顾了低耦合、高可读性和强大的功能。
简单来说,你可以这样理解它们的演进:
- 方式一:“你(代码)必须听我的(框架),按我的规矩来写。”
- 方式二:“你(代码)做你自己的事,我(XML)来告诉你什么时候该做什么。”
- 方式三:“你(代码)自己决定自己该做什么,并告诉大家(通过注解),我(框架)负责让你生效就行。”
12、整合MyBatis
整合步骤概览
- 导入相关Jar包
- 编写配置文件
- 编写代码与测试
1. 导入Maven依赖
<!-- JUnit 测试框架 -->
<dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version>
</dependency><!-- MyBatis 核心包 -->
<dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.3</version>
</dependency><!-- MySQL 数据库驱动 -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.47</version>
</dependency><!-- Spring 相关包 -->
<dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>5.2.12.RELEASE</version>
</dependency>
<dependency><groupId>org.springframework</groupId><artifactId>spring-jdbc</artifactId><version>5.1.10.RELEASE</version>
</dependency><!-- AOP 织入器 -->
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.6</version>
</dependency><!-- 【关键】MyBatis-Spring 整合包 -->
<dependency><groupId>org.mybatis</groupId><artifactId>mybatis-spring</artifactId><version>2.0.6</version>
</dependency>
2. 配置Maven静态资源过滤
为了确保 src/main/java
目录下的 .xml
和 .properties
文件能被正确打包,需要在 pom.xml
中添加以下配置:
<build><resources><resource><directory>src/main/java</directory><includes><include>**/*.properties</include><include>**/*.xml</include></includes><filtering>true</filtering></resource></resources>
</build>
12.1 回忆MyBatis
在整合前,我们先回顾一下原生MyBatis的开发流程。
1. 实体类 (User.java)
package com.github.subei.pojo;
import lombok.Data;@Data
public class User {private int id;private String name;private String pwd;// ... Constructors, Getters, Setters, toString() ...
}
2. MyBatis核心配置文件 (mybatis-config.xml)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration><typeAliases><package name="com.github.subei.pojo"/></typeAliases><environments default="development"><environment id="development"><transactionManager type="JDBC"/><dataSource type="POOLED"><property name="driver" value="com.mysql.jdbc.Driver"/><property name="url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=UTF-8"/><property name="username" value="root"/><property name="password" value="root"/></dataSource></environment></environments><mappers><package name="com.github.subei.mapper"/></mappers>
</configuration>
3. Mapper接口 (UserMapper.java)
package com.github.subei.mapper;
import com.github.subei.pojo.User;
import java.util.List;public interface UserMapper {List<User> selectUser();
}
4. Mapper XML文件 (UserMapper.xml)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.github.subei.mapper.UserMapper"><select id="selectUser" resultType="User">select * from user;</select>
</mapper>
5. 测试类
public class MyTest {@Testpublic void selectUser() throws IOException {String resource = "mybatis-config.xml";InputStream inputStream = Resources.getResourceAsStream(resource);SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);SqlSession sqlSession = sqlSessionFactory.openSession();try {UserMapper mapper = sqlSession.getMapper(UserMapper.class);List<User> userList = mapper.selectUser();for(User user : userList){System.out.println(user);}} finally {sqlSession.close();}}
}
12.2 MyBatis-Spring核心概念
官方文档地址: http://www.mybatis.org/spring/zh/index.html
MyBatis-Spring 的核心目标是帮助我们将 MyBatis 无缝地整合到 Spring 容器中,让 Spring 来管理 MyBatis 的组件。
-
SqlSessionFactoryBean
:- 在 Spring 中,我们不再使用
SqlSessionFactoryBuilder
,而是使用SqlSessionFactoryBean
来创建SqlSessionFactory
。 - 它负责读取配置,并创建一个由 Spring 管理的
SqlSessionFactory
实例。 - 其最重要的属性是
dataSource
,用于接收 Spring 管理的数据源。
- 在 Spring 中,我们不再使用
-
SqlSessionTemplate
:SqlSessionTemplate
是 MyBatis-Spring 的核心,它是SqlSession
的一个线程安全实现。- 它能自动参与到 Spring 的事务管理中,负责 session 的生命周期(获取、提交/回滚、关闭)。
- 在整合后,我们应该始终使用
SqlSessionTemplate
来代替原生的DefaultSqlSession
。
12.3 整合实现方式一:SqlSessionTemplate
1. 创建Spring数据层配置文件 (spring-dao.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"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd"><!-- 1. 配置数据源 (DataSource) --><bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"><property name="driverClassName" value="com.mysql.jdbc.Driver" /><property name="url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=UTF-8"/><property name="username" value="root"/><property name="password" value="root"/></bean><!-- 2. 配置 SqlSessionFactory --><bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"><property name="dataSource" ref="dataSource" /><!-- 关联MyBatis核心配置文件 (可选,用于settings, typeAliases等) --><property name="configLocation" value="classpath:mybatis-config.xml"/><!-- 扫描Mapper XML文件 --><property name="mapperLocations" value="classpath:com/github/subei/mapper/*.xml"/></bean><!-- 3. 注册 SqlSessionTemplate --><bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate"><!-- 只能使用构造器注入sqlSessionFactory,因为它没有set方法 --><constructor-arg index="0" ref="sqlSessionFactory"/></bean>
</beans>
2. 创建Mapper实现类 (UserMapperImpl.java)
package com.github.subei.mapper;
import com.github.subei.pojo.User;
import org.mybatis.spring.SqlSessionTemplate;
import java.util.List;public class UserMapperImpl implements UserMapper {// 我们的所有操作,都使用SqlSessionTemplate来执行private SqlSessionTemplate sqlSession;public void setSqlSession(SqlSessionTemplate sqlSession) {this.sqlSession = sqlSession;}public List<User> selectUser() {UserMapper mapper = sqlSession.getMapper(UserMapper.class);return mapper.selectUser();}
}
3. 创建Spring主配置文件 (applicationContext.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:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop.xsd"><!-- 导入数据层配置 --><import resource="spring-dao.xml"/><!-- 注册Mapper实现类的Bean --><bean id="userMapper" class="com.github.subei.mapper.UserMapperImpl"><property name="sqlSession" ref="sqlSessionTemplate"/></bean>
</beans>
4. 测试
@Test
public void testSelectUser() {ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");UserMapper userMapper = context.getBean("userMapper", UserMapper.class);for (User user : userMapper.selectUser()){System.out.println(user);}
}
结果成功输出! 此时,原生的 mybatis-config.xml
文件中的数据源和事务管理器配置已被 Spring 完全接管,可以被简化。
12.4 整合实现方式二:SqlSessionDaoSupport
这是一种更便捷的方式,通过继承 SqlSessionDaoSupport
类来简化 Mapper 实现类的编写。
1. 修改Mapper实现类 (UserMapperImpl2.java)
package com.github.subei.mapper;
import com.github.subei.pojo.User;
import org.mybatis.spring.support.SqlSessionDaoSupport;
import java.util.List;public class UserMapperImpl2 extends SqlSessionDaoSupport implements UserMapper {public List<User> selectUser() {// 直接通过 getSqlSession() 获取 SqlSessionTemplateUserMapper mapper = getSqlSession().getMapper(UserMapper.class);return mapper.selectUser();}
}
2. 修改Spring配置 (applicationContext.xml)
SqlSessionDaoSupport
需要注入 SqlSessionFactory
而不是 SqlSessionTemplate
。
<bean id="userMapper2" class="com.github.subei.mapper.UserMapperImpl2"><property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
3. 测试 (类似方式一)
@Test
public void testSelectUser2() {ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");UserMapper userMapper = context.getBean("userMapper2", UserMapper.class);// ...
}
总结:整合到Spring后,可以完全移除MyBatis配置文件中的数据源和事务配置。除了XML配置,还可以使用注解方式实现整合,这是现代开发中更主流的方式。
13、声明式事务
13.1 回顾事务
事务 是一组不可分割的业务操作单元,要么都成功,要么都失败。它用于保证数据的完整性和一致性。
事务的ACID原则:
- 原子性 (Atomicity):事务中的所有操作是一个整体,不可分割。
- 一致性 (Consistency):事务完成后,数据必须保持业务规则上的一致状态。
- 隔离性 (Isolation):多个事务并发执行时,应相互隔离,防止数据损坏。
- 持久性 (Durability):事务一旦提交,其结果就是永久性的。
13.2 事务失效场景测试
假设我们在一个方法内,先执行一个成功的插入操作,再执行一个失败的删除操作(SQL语法错误)。
1. 扩展Mapper接口和XML
// UserMapper.java
public interface UserMapper {List<User> selectUser();int addUser(User user);int deleteUser(int id);
}
<!-- UserMapper.xml -->
<insert id="addUser" ...>insert into user (id,name,pwd) values (#{id},#{name},#{pwd});
</insert>
<!-- 故意写错SQL,将 delete 写成 deletes -->
<delete id="deleteUser" ...>deletes from user where id = #{id};
</delete>
2. 在一个方法中调用
// UserMapperImpl.java
public List<User> selectUser() {UserMapper mapper = getSqlSession().getMapper(UserMapper.class);// 先添加mapper.addUser(new User(6,"维维","123456"));// 再删除(这个会失败)mapper.deleteUser(6); return mapper.selectUser();
}
测试结果:程序会因SQL异常而中断,但数据库中用户添加成功了!这破坏了数据的一致性,因为我们期望添加和删除是一个整体。
13.3 Spring中的事务管理
Spring 提供了两种事务管理方式:
- 编程式事务:在业务代码中手动控制事务的开启、提交、回滚。侵入性强,不推荐。
- 声明式事务:通过配置(XML或注解)来管理事务,业务代码无需关心事务逻辑。这是我们使用的重点。
配置声明式事务(XML方式)
1. 配置事务管理器
首先,需要一个事务管理器 DataSourceTransactionManager
,并关联我们的数据源。
<!-- applicationContext.xml --><!-- 1. 配置声明式事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource" />
</bean>
2. 配置事务通知 (tx:advice)
在这里定义事务的规则,比如哪些方法需要事务,以及事务的传播特性。
需要先引入 tx
命名空间及其约束。
<!-- 2. 配置事务通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager"><tx:attributes><!-- 配置哪些方法使用什么样的事务, propagation是传播特性REQUIRED: 如果当前没有事务,就新建一个;如果已存在,就加入。这是最常用的。--><tx:method name="add*" propagation="REQUIRED"/><tx:method name="delete*" propagation="REQUIRED"/><tx:method name="update*" propagation="REQUIRED"/><tx:method name="select*" read-only="true"/><tx:method name="*" propagation="REQUIRED"/></tx:attributes>
</tx:advice>
3. 配置AOP,将事务织入
使用AOP将事务通知应用到指定的方法上。
<!-- 3. 配置AOP -->
<aop:config><!-- 定义切入点,这里是对mapper包下的所有方法 --><aop:pointcut id="txPointcut" expression="execution(* com.github.subei.mapper.*.*(..))"/><!-- 将事务通知和切入点绑定 --><aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
再次测试
在配置好声明式事务后,再次运行之前的测试代码。
结果:程序依然会报错,但是数据库中新用户没有被添加!事务成功回滚,保证了数据的一致性。
为什么需要事务?
- 保证数据一致性:防止因部分操作失败而导致数据状态混乱。
- 简化开发:使用Spring的声明式事务,开发者可以专注于业务逻辑,而无需手动管理复杂的事务代码。
写在最后
从 IoC/DI 的核心原理,到 XML、注解、JavaConfig 三种配置方式的演进;从代理模式的底层铺垫,到 AOP 的切面思想;再到最后整合 MyBatis 并加上事务的保障。至此,我们已经走完了 Spring Framework 最核心、最常用的一段旅程。
即使在 Spring Boot 已成为主流的今天,这些底层的概念和配置思想依然是理解 Spring 生态、排查复杂问题、成为一名优秀 Java 开发者的基石。
希望这份笔记能帮助正在学习 Spring 的你,理清思路,夯实基础。学习是一个持续迭代的过程,与君共勉。
🎉🎉🎉 Spring 核心部分完结撒花! 🎉🎉🎉
📦 对应代码仓库:https://gitee.com/zzzfatfish/spring-test
✍ 作者:fatfish
🕒 状态:学习中,内容持续补充完善…
参考
【狂神说Java】Spring5最新完整教程IDEA版通俗易懂
Spring学习目录(6天) - subeiLY - 博客园