昨天我们详细讲解了Spring IoC容器的原理、掌握了Bean标签以及属性的使用、熟悉Bean的实例化、Bean的几种作用域、掌握Bean的装配方式、自动装配方式XML和注解方式的区别和优势、什么是Bean的生命周期。所以今天我们要掌握的是MVC三层架构设计模式,以及Spring的另外一大核心特性:AoP。
三层架构是将整个项目业务分为表示层,业务逻辑层,数据访问层,区分层次的目的是为了实现“高内聚,低耦合”的思想。在软件体系架构设计中,分层式结构式最为常见,也是最为重要的一种结构。
也就是我们这几天一直用的模式,三层结构,都是在为学习SpringMVC打好基础。但是我们斗志实现了前俩层,数据访问层和业务逻辑层,表示层View等整合的时候再讲。
我们这里回顾一下,创建三层结构的流程,也方便后面我们写入实例进行SpringAoP思想的验证。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>org.examplegroupId>
<artifactId>springaoptestartifactId>
<version>1.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatisartifactId>
<version>3.5.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.11version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>compilescope>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-coreartifactId>
<version>5.2.8.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-beansartifactId>
<version>5.2.8.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.2.8.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aopartifactId>
<version>5.2.8.RELEASEversion>
dependency>
dependencies>
project>
package com.steveDash.entity;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
public class User {
private int id;// 用户id
private String name;//用户名
private String password;//用户密码
// getter/setter方法和toString()方法
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 String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String toString(){
return "用户id:"+id+",用户名:"+name+",密码:"+password;
}
}
package com.steveDash.dao;
public interface UserDao {
public void save(User user); //保存数据的方法
}
package com.steveDash.dao;
import com.steveDash.entity.User;
public class UserDaoImpl implements UserDao {
public void save(User user) {
// 这里并未实现完整的数据库操作,仅为说明问题
System.out.println("保存用户"+user.getName()+"信息到数据库");
}
}
package com.steveDash.service;
import com.steveDash.entity.User;
public interface UserService {
public void addNewUser(User user); //新的保存user 的方法
}
package com.steveDash.service.impl;
import com.steveDash.dao.UserDao;
import com.steveDash.entity.User;
import com.steveDash.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
public class UserServiceImpl implements UserService {
// 声明接口类型的引用,和具体实现类解耦合
private UserDao dao;
public UserDao getDao() {
return dao;
}
public void setDao(UserDao dao) {
this.dao = dao;
}
public void addNewUser(User user) {
// 调用用户DAO的方法保存用户信息
dao.save(user);
}
}
(1)这里的UserServiceImpl把UserDao当成它的一个成员变量,我们称为UserServiceImpl依赖UserDao.使用UserServiceImpl前需要将UserDao实例化,并注入给UserServiceImpl.
(2)这里体现了分层的思想,将以前简单的添加数据的操作,我们分成了两层:dao层(数据访问层)和service层(业务层),将来我们还要加再上表现层View,实现三层结构的开发模式。
<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/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.2.xsd">
<bean id="dao" class="com.steveDash.dao.UserDaoImpl">bean>
<bean id="service" class="com.steveDash.service.impl.UserServiceImpl">
<property name="dao" ref="dao">property>
bean>
beans>
圈出来的是添加对AOP的支持,最后真正实现添加数据的操作由service对象完成。只需要调用它的addNewUser()方法即可。注意体会这种封装方式。
package Test;
import com.steveDash.entity.User;
import com.steveDash.service.UserService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class UserTest {
@Test
public void saveUserTest(){
ApplicationContext context=new ClassPathXmlApplicationContext("spring-config.xml");
UserService userService=(UserService)context.getBean("service");
User user = new User();
user.setId(1);
user.setName("test");
user.setPassword("123456");
userService.addNewUser(user);
}
}
这就是分层设计模式的思路。
Spring的AOP(面向切面编程)是Spring框架的一个核心功能,它提供了一种方式来模块化横切关注点(cross-cutting concerns)。
AOP一般适用于具有横切逻辑的场合,例如访问控制、事务管理、性能监测、安全性、日志记录等,以减少代码的重复性,提高可维护性和可扩展性。
面向切面编程(Aspect Oriented Programming,AOP)是软件编程思想发展到一定阶段的产物,是对面向对象编程(Object Oriented Programming,OOP)的有益补充。
面向切面编程,简单地说就是在不改变原有程序的基础上为代码段增加新的功能,对其进行增强处理。
例如,假设由于业务发展的需要,现在项目要增加功能,我们不想去修改旧的代码,在不改变原代码的基础上新增功能,这里就需要使用AOP的思想。
这有点像汉堡包,我们想在中间加上一层菜,我们可以使用面向切面方法实现,但是包子我们就不能再加上新的菜进去而不影响整个包子,因为汉堡包是面向切面设计的,而包子不是。
**接下来,我们引入AOP的概念,如果上面的软件使用了一段时间后,用户想新增功能,比如在保存用户信息到数据库之前,用日志输出“准备添加数据”**
我们新增一个类,用于满足需求:添加用户表数据之前,输出“准备添加数据。在添加用户表成功之后,输出“添加数据完成”
具体代码如下:
package com.steveDash.service;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import java.util.Arrays;
public class UserServiceLogger {
private static final Logger log = Logger.getLogger(UserServiceLogger.class);
public void before(JoinPoint jp) {
log.info("准备添加数据, 调用 " + jp.getTarget() + " 的 " + jp.getSignature().getName()
+ " 方法。方法入参:" + Arrays.toString(jp.getArgs()));
}
public void afterReturning(JoinPoint jp) {
log.info("添加数据完成");
}
}
这是因为我们没有引入aspectjweaver
依赖,这个依赖是AspectJ 是一个用于实现面向切面编程 (AOP) 的框架,它提供了强大的切面编程功能,可以在程序中实现横切关注点(如日志记录、性能监视、事务管理等)。
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
<version>1.9.7version>
dependency>
<bean id="theLogger" class="com.steveDash.service.UserServiceLogger">bean>
<aop:config>
<aop:pointcut id="pointcut"
expression="execution(public void com.steveDash.service.impl.UserServiceImpl.addNewUser(com.steveDash.entity.User))" />
<aop:aspect ref="theLogger">
<aop:before method="before" pointcut-ref="pointcut">aop:before>
<aop:after-returning method="afterReturning"
pointcut-ref="pointcut" returning="result" />
aop:aspect>
aop:config>
expression=“execution(public void com.steveDash.service.impl.UserServiceImpl.addNewUser(com.steveDash.entity.User))”
execution是切入点指示符,括号中是一个切入点表达式,用于配置需要切入增强处理的方法的特征。
在aop:config中使用aop:aspect引用包含增强方法的Bean,然后分别通过aop:before和aop:after-returning将方法声明为前置增强和后置增强,在aop:after-returning中通过returning属性指定需要注入返回值的属性名。方法的Join Point类型参数无须特殊处理,Spring会自动为其注入连接点实例。
很明显,User Service的add New User()方法可以和切入点pointcut相匹配,Spring会生成代理对象
,在它执行前后分别调用before()和afterReturning()方法,这样就完成了日志输出
这里先简单理解:在addNewUser(com.steveDash.entity.User)之前执行UserServiceLogger类的before()方法,之后执行UserServiceLogger类的afterReturning()方法,后面再详细讲解。
④重新运行测试类,查看结果:
查看终端输出框:
我们可以发现,在我们没有修改旧代码的情况下,在调用addNewUser()方法时,自动运行了我们新增的before()和afterReturning()的代码。
代码解释:
从以上示例可以看出,业务代码和日志代码是完全分离的,经过AOP的配置以后,不做任何代码上的修改就在addNewUser()方法前后实现了日志输出。其实,只需稍稍修改切入点的指示符,不仅可以为User Service的add New User()方法增强日志功能,也可以为所有业务方法进行增强;并且可以增强日志功能,如实现访问控制、事务管理、性能监测等实用功能
Spring的AOP实现基于代理模式。在Spring中,切面是通过代理对象来实现的,代理对象包装了目标对象,并在目标对象的方法执行前后执行一些特定操作,这些操作就是通知(Advice)。
通知处理类型 | 特点 |
---|---|
Before | 前置通知处理,在目标方法前置通知执行处理 |
AfterReturning | 后置通知处理,在目标方法正常执行(不出现异常)后置 通知执行处理 |
AfterThrowing | 异常通知处理,在目标方法抛出异常后置通知执行处理 |
After | 最终通知处理,不论方法是否抛出异常,都会在目标方法最后通知执行处理 |
Around | 环绕通知处理,在目标方法的前后都会通知执行处理 |
PS:也有称之为前置增强
的说法,但是“前置通知”
更容易理解。
切点(Pointcut): 切点是一个表达式,用于定义在哪些连接点上应用通知。连接点是应用程序执行的点,通常是方法调用。切点表达式定义了哪些方法调用会被拦截。Spring 只支持方法连接点
通知(Advice): 通知是在连接点上执行的代码,它定义了在连接点的何时和如何执行。通知的类型包括前置通知、后置通知、环绕通知、异常通知和最终通知。
切面(Aspect): 切面是通知和切点的组合,它定义了在哪些连接点上应用哪些通知。切面是AOP的核心概念,它将关注点(例如事务管理、日志记录)模块化,以便将它们应用到不同的目标对象中。
目标对象(Target Object): 目标对象是真正执行业务逻辑的对象,它通常是一个普通的Java对象。通知在目标对象上运行。
代理对象(Proxy Object): 代理对象是包装目标对象的对象,通知实际上是在代理对象上执行的。Spring使用JDK动态代理和CGLIB代理来创建代理对象。
Spring AOP的配置可以使用XML配置文件或者注解来完成。
通常,开发人员更喜欢使用注解来声明切面和通知,因为它更加简洁和易于理解。
Spring AOP,它默认使用基于 JDK 动态代理(JDK Dynamic Proxy)或者 CGLIB(Code Generation Library)代理。
Spring的AOP功能使得切面编程变得相对容易,可以帮助我们开发人员更好地管理和维护应用程序中的横切关注点。
刚才我们已经学习了前置通知**Before
和后置通知AfterReturing
**,
下面我们再介绍一下**异常通知AfterThrowing
和最终通知After
**
package com.steveDash.service.impl;
import com.steveDash.dao.UserDao;
import com.steveDash.entity.User;
import com.steveDash.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
public class UserServiceImpl implements UserService {
// 声明接口类型的引用,和具体实现类解耦合
private UserDao dao;
public UserDao getDao() {
return dao;
}
public void setDao(UserDao dao) {
this.dao = dao;
}
public void addNewUser(User user) {
// 调用用户DAO的方法保存用户信息
int i=1/0; //故意出错
dao.save(user);
}
}
package com.steveDash.service;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import java.util.Arrays;
public class UserServiceLogger {
private static final Logger log = Logger.getLogger(UserServiceLogger.class);
public void before(JoinPoint jp) {
log.info("准备添加数据, 调用 " + jp.getTarget() + " 的 " + jp.getSignature().getName()
+ " 方法。方法入参:" + Arrays.toString(jp.getArgs()));
}
//添加数据错误时,日志输出
public void errorThrow(JoinPoint jp){
log.error("添加数据出错,发生在"+jp.getSignature().getName());
}
public void afterReturning(JoinPoint jp) {
log.info("添加数据完成"+jp.getSignature().getName());
}
//最终通知
public void afterLogger()
{
log.info("谢谢使用");
}
}
<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/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.2.xsd">
<bean id="dao" class="com.steveDash.dao.UserDaoImpl">bean>
<bean id="service" class="com.steveDash.service.impl.UserServiceImpl">
<property name="dao" ref="dao">property>
bean>
<bean id="theLogger" class="com.steveDash.service.UserServiceLogger">bean>
<aop:config>
<aop:pointcut id="pointcut"
expression="execution(public void com.steveDash.service.impl.UserServiceImpl.addNewUser(com.steveDash.entity.User))" />
<aop:aspect ref="theLogger">
<aop:before method="before" pointcut-ref="pointcut">aop:before>
<aop:after-returning method="afterReturning" pointcut-ref="pointcut"/>
<aop:after-throwing method="errorThrow" pointcut-ref="pointcut"/>
<aop:after method="afterLogger" pointcut-ref="pointcut"/>
aop:aspect>
aop:config>
beans>
异常通知正常运行,最终通知也正常运行
无错误运行:
AOP的注解类型如下
@Aspect
public class UserServiceLogger {
private static final Logger log = Logger.getLogger(UserServiceLogger.class);
//设置断点:
@Pointcut("execution(* service.UserServiceImpl.addNewUser (..))")
public void pointcut(){}
@Before("pointcut()")
public void before(JoinPoint jp) {
log.info("准备添加数据, 调用 " + jp.getTarget() + " 的 " + jp.getSignature().getName()
+ " 方法。方法入参:" + Arrays.toString(jp.getArgs()));
}
@AfterReturning("pointcut()")
public void afterReturning(JoinPoint jp) {
log.info("添加数据完成 ");
}
@After("pointcut()")
public void afterLogger()
{
log.info("谢谢使用");
}
@AfterThrowing("pointcut()")
public void errorThrow(JoinPoint jp){
log.error("添加数据出错,发生在"+jp.getSignature().getName());
}
}
把前面这段代码修改成
某些版本需要在后面加上:
即可使用注解的方式实现AOP
今天我们首先介绍了三层架构设计模式,方便后面引入SpringMVC;然后主要讲解了Spring中的AOP,讲解了Spring AOP的概念、实现机制,五种通知类型,包括JDK动态代理和CGLib动态代理;接着讲解了基于XML的AOP实现,最后讲解了基于注解的AOP实现。通过今天的学习,各位读者可以对Spring的AOP实现有个大致的了解,为框架开发打下坚实基础。
想要跟着学习的可以去我的资源里面找对应的文件下载,我的md文件也会发上去,项目文件会上传可以自己跟着学习一下。
作者:Stevedash
发表于:2023年9月5日 8点17分