一.Spring 整合 Junit 测试框架
基本概念
Spring 整合 Junit 是为了方便在 Spring 环境下进行单元测试和集成测试。通过 Spring 提供的测试支持,可以轻松地加载 Spring 容器、注入依赖以及进行事务管理等操作。
核心注解
@RunWith(SpringRunner.class)
- 替代了传统的 Junit 运行器
- 提供了 Spring 测试功能支持
- 示例:
@RunWith(SpringRunner.class)
public class UserServiceTest {
// 测试代码
}
@ContextConfiguration
- 用于指定 Spring 配置文件或配置类的位置
- 两种使用方式:
- 基于 XML 配置:
- 基于 Java 配置:
@ContextConfiguration(locations = "classpath:applicationContext.xml")
@ContextConfiguration(classes = AppConfig.class)
依赖注入
@Autowired
- 自动注入测试所需的 Spring Bean
- 可以注入服务层、DAO 层等组件
- 示例:
@Autowired
private UserService userService;
事务管理
@Transactional
- 测试方法默认会在事务中执行
- 测试完成后会自动回滚事务
- 避免测试数据污染数据库
- 示例:
@Test
@Transactional
public void testSaveUser() {
// 测试保存用户
}
高级特性
@TestPropertySource
- 用于指定测试专用的属性文件
- 覆盖主配置文件的属性
- 示例:
@TestPropertySource(locations = "classpath:test.properties")
@MockBean
- 用于在测试环境中模拟 Bean
- 可以替换实际 Bean 或添加新的模拟 Bean
- 示例:
@MockBean
private UserRepository userRepository;
测试示例
完整测试类示例:
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = TestConfig.class)
@Transactional
public class UserServiceIntegrationTest {
@Autowired
private UserService userService;
@MockBean
private EmailService emailService;
@Test
public void testRegisterUser() {
User user = new User("test", "test@example.com");
when(emailService.sendWelcomeEmail(anyString())).thenReturn(true);
User registered = userService.register(user);
assertNotNull(registered);
assertEquals("test", registered.getUsername());
verify(emailService).sendWelcomeEmail(user.getEmail());
}
}
最佳实践
二、Spring整合JDBC实现转账业务
数据准备
-
创建银行账户表account,包含字段:
CREATE TABLE account (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
balance DECIMAL(10,2) NOT NULL
); -
插入测试数据:
INSERT INTO account(name, balance) VALUES('张三', 1000),('李四', 500);
配置数据源 在Spring配置文件中配置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/bank"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
创建JdbcTemplate
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
实现转账业务
-
创建AccountDao接口:
public interface AccountDao {
void out(String outUser, double money);
void in(String inUser, double money);
}
-
实现类使用JdbcTemplate:
@Repository
public class AccountDaoImpl implements AccountDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public void out(String outUser, double money) {
jdbcTemplate.update("UPDATE account SET balance=balance-? WHERE name=?", money, outUser);
}
@Override
public void in(String inUser, double money) {
jdbcTemplate.update("UPDATE account SET balance=balance+? WHERE name=?", money, inUser);
}
}
添加事务管理
-
配置事务管理器:
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
-
开启注解事务支持:
<tx:annotation-driven transaction-manager="transactionManager"/>
-
在Service层添加事务注解:
@Service
public class AccountService {
@Autowired
private AccountDao accountDao;
@Transactional
public void transfer(String outUser, String inUser, double money) {
accountDao.out(outUser, money);
// 模拟异常
// int i = 1/0;
accountDao.in(inUser, money);
}
}
测试转账功能
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class AccountTest {
@Autowired
private AccountService accountService;
@Test
public void testTransfer() {
accountService.transfer("张三", "李四", 100);
}
}
注意事项
- 确保数据库连接参数正确
- 事务传播行为默认是REQUIRED
- 异常回滚默认只针对RuntimeException
- 可通过@Transactional的属性自定义事务行为
三、动态代理
动态代理是一种在运行时动态创建代理类和代理对象的技术,它允许我们在不修改原始类代码的情况下,为对象添加额外的功能或控制访问。与静态代理相比,动态代理更加灵活和高效,适用于需要为多个类或接口创建代理的场景。
1. 动态代理的特点
- 运行时生成:代理类是在程序运行时动态生成的,不需要预先编写代理类代码
- 通用性强:一个动态代理类可以代理多个不同的接口或类
- 减少代码量:避免了为每个被代理类编写单独的代理类
- 灵活性高:可以在运行时决定代理哪些方法以及如何代理
2. Java中的实现方式
在Java中,主要有两种实现动态代理的方式:
(1) JDK动态代理
基于接口的动态代理,要求被代理类必须实现至少一个接口。
实现步骤:
示例代码:
public interface UserService {
void save();
}
public class UserServiceImpl implements UserService {
public void save() {
System.out.println("保存用户");
}
}
public class LogHandler implements InvocationHandler {
private Object target;
public LogHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开始记录日志…");
Object result = method.invoke(target, args);
System.out.println("日志记录完成");
return result;
}
}
// 使用方式
UserService userService = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
userService.getClass().getClassLoader(),
userService.getClass().getInterfaces(),
new LogHandler(userService)
);
proxy.save();
(2) CGLIB动态代理
基于继承的动态代理,可以对没有实现接口的类进行代理。
实现步骤:
示例代码:
public class UserDao {
public void save() {
System.out.println("保存用户数据");
}
}
public class LogInterceptor implements MethodInterceptor {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("开始记录日志…");
Object result = proxy.invokeSuper(obj, args);
System.out.println("日志记录完成");
return result;
}
}
// 使用方式
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserDao.class);
enhancer.setCallback(new LogInterceptor());
UserDao proxy = (UserDao) enhancer.create();
proxy.save();
3. 动态代理的应用场景
- AOP编程:实现日志记录、性能监控、事务管理等横切关注点
- RPC框架:远程方法调用的客户端代理
- Mock测试:为测试创建代理对象
- 权限控制:方法调用前的权限校验
- 缓存代理:为方法调用添加缓存功能
4. JDK代理与CGLIB代理比较
| 代理方式 | 基于接口 | 基于继承 |
| 目标类要求 | 必须实现接口 | 可以代理普通类 |
| 性能 | 创建速度较快,调用稍慢 | 创建速度较慢,调用较快 |
| 方法拦截 | 只能拦截接口方法 | 可以拦截所有非final方法 |
| 依赖 | JDK自带 | 需要额外引入CGLIB库 |
5. 注意事项
- 使用JDK动态代理时,被代理类必须实现接口
- CGLIB不能代理final类或final方法
- 动态代理会增加一定的性能开销
- 复杂的代理逻辑可能会影响程序的可读性
- 在Spring框架中,默认使用JDK动态代理,如果没有接口则使用CGLIB
四、Spring AOP机制详解
4.1 AOP基本概念
AOP(Aspect-Oriented Programming)即面向切面编程,是Spring框架的核心模块之一。它通过预编译方式和运行期动态代理实现程序功能的统一维护。AOP主要解决的是横切关注点问题,将与业务无关但需要大量重复的代码(如日志记录、事务管理、安全检查等)从业务逻辑中分离出来。
4.1.1 AOP术语解析
- 前置通知(Before advice)
- 后置通知(After advice)
- 返回通知(After-returning advice)
- 异常通知(After-throwing advice)
- 环绕通知(Around advice)
4.2 Spring AOP实现机制
4.2.1 代理模式
Spring AOP默认使用JDK动态代理和CGLIB代理两种方式:
JDK动态代理:基于接口实现,要求目标类必须实现至少一个接口。运行时动态生成代理类,通过反射机制调用目标方法。
public interface UserService {
void saveUser();
}
public class UserServiceImpl implements UserService {
public void saveUser() {
System.out.println("保存用户");
}
}
CGLIB代理:基于继承实现,不需要接口。通过生成目标类的子类来拦截方法调用。对于final类或方法无法使用CGLIB代理。
4.2.2 织入(Weaving)时机
Spring AOP采用运行时织入方式,即在应用运行期间通过代理机制将切面逻辑织入到目标对象中。这与AspectJ等框架提供的编译时织入和类加载时织入不同。
4.3 Spring AOP配置方式
4.3.1 基于XML的配置
<aop:config>
<aop:aspect id="logAspect" ref="loggingAspect">
<aop:pointcut id="servicePointcut"
expression="execution(* com.example.service.*.*(..))"/>
<aop:before method="beforeAdvice" pointcut-ref="servicePointcut"/>
</aop:aspect>
</aop:config>
4.3.2 基于注解的配置
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice(JoinPoint joinPoint) {
System.out.println("方法执行前:" + joinPoint.getSignature().getName());
}
@AfterReturning(pointcut="execution(* com.example.service.*.*(..))",
returning="result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
System.out.println("方法返回后:" + result);
}
}
4.4 切点表达式语法
Spring AOP使用AspectJ切点表达式语言定义切点:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
常见表达式示例:
4.5 AOP应用场景
4.6 AOP最佳实践
4.7 AOP与IOC的关系
Spring AOP与IOC容器紧密集成:
- AOP代理对象由IOC容器管理
- 切面组件可以像普通Bean一样被依赖注入
- AOP配置可以与其他Bean配置统一管理
这种集成使得AOP在Spring应用中能够无缝工作,开发者无需关心底层代理机制的实现细节。
五、Spring基于注解的AOP配置
常用注解说明
- @Aspect:标注当前类为切面类
- @Before:前置通知,在目标方法执行前执行
- @AfterReturning:后置通知,在目标方法正常执行后执行
- @AfterThrowing:异常通知,在目标方法抛出异常后执行
- @After:最终通知,无论目标方法是否正常执行都会执行
- @Around:环绕通知,可以控制目标方法的执行
- @Pointcut:定义切入点表达式
<aop:aspectj-autoproxy/>
(2) 定义切面类并添加@Component注解:
@Component
@Aspect
public class LogAspect {
// 切入点表达式示例
@Pointcut("execution(* com.example.service.*.*(..))")
private void serviceLayer(){}
@Before("serviceLayer()")
public void beforeAdvice(JoinPoint joinPoint){
System.out.println("前置通知:方法"+joinPoint.getSignature().getName()+"即将执行");
}
}
切入点表达式语法
- execution:匹配方法执行
- within:匹配类型
- args:匹配参数
- target:匹配目标对象
- this:匹配代理对象
- bean:匹配Bean名称
实际应用场景 (1) 日志记录:在方法执行前后记录日志 (2) 性能监控:统计方法执行时间 (3) 事务管理:通过环绕通知实现事务控制 (4) 权限校验:在方法执行前进行权限验证 (5) 缓存处理:方法执行结果缓存
注意事项
- 确保切面类被Spring容器管理(添加@Component等注解)
- 环绕通知需要显式调用proceed()方法
- 多个切面的执行顺序可通过@Order注解控制
- 注意切入点表达式的性能影响,避免过于宽泛的匹配



