三层架构设计模式MVC和AOP面向切面编程—SSM框架的学习与应用(Spring + Spring MVC + MyBatis)-Java EE企业级应用开发学习记录(第九天)

SSM框架的学习与应用(Spring + Spring MVC + MyBatis)-Java EE企业级应用开发学习记录(第九天)三层架构设计模式MVC和AOP面向切面编程

​ 昨天我们详细讲解了Spring IoC容器的原理、掌握了Bean标签以及属性的使用、熟悉Bean的实例化、Bean的几种作用域、掌握Bean的装配方式、自动装配方式XML和注解方式的区别和优势、什么是Bean的生命周期。所以今天我们要掌握的是MVC三层架构设计模式,以及Spring的另外一大核心特性:AoP。


文章目录

一、三层架构设计模型

三层架构是将整个项目业务分为表示层,业务逻辑层,数据访问层,区分层次的目的是为了实现“高内聚,低耦合”的思想。在软件体系架构设计中,分层式结构式最为常见,也是最为重要的一种结构

在这里插入图片描述

也就是我们这几天一直用的模式,三层结构,都是在为学习SpringMVC打好基础。但是我们斗志实现了前俩层,数据访问层和业务逻辑层,表示层View等整合的时候再讲。

我们这里回顾一下,创建三层结构的流程,也方便后面我们写入实例进行SpringAoP思想的验证。

①新建项目为:springaoptest,搭建依赖环境编写好pom.xml引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>springaoptest</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.2</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.11</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    <!--Spring的基础包Spring-core-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>
    <!--Spring的基础包Spring-beans-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>
    <!--Spring的基础包Spring-context-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>
        <!--用于引入 Spring Framework 中的 AOP(面向切面编程)模块-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.2.8.RELEASE</version>
        </dependency>

    </dependencies>
</project>

②新建entity包,包内创建User.java
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;
    }
}

③创建dao层,新建dao包,包内创建UserDao作为数据访问接口
package com.steveDash.dao;

public interface UserDao {
    public void save(User user); //保存数据的方法
}

④运用我们之前学习的快捷键方式创建接口实现类UserDaoImpl
package com.steveDash.dao;
import com.steveDash.entity.User;

public class UserDaoImpl implements UserDao {
    public void save(User user) {
        // 这里并未实现完整的数据库操作,仅为说明问题
        System.out.println("保存用户"+user.getName()+"信息到数据库");	
    }
}

⑤创建Service层:在service包里创建一个接口UserService,里面包含新增加数据的方法
package com.steveDash.service;

import com.steveDash.entity.User;

public interface UserService {
    public void addNewUser(User user); //新的保存user 的方法
}

⑥创建UserService接口的实现类UserServiceImpl
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,实现三层结构的开发模式。


⑦在spring-config.xml中用IoC实例化上面的对象,并调用它的addNewUser()方法向用户表写入数据:
<?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/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);

    }
}

在这里插入图片描述

这就是分层设计模式的思路。

二、SpringAOP

基本概念:

Spring的AOP(面向切面编程)是Spring框架的一个核心功能,它提供了一种方式来模块化横切关注点(cross-cutting concerns)

AOP一般适用于具有横切逻辑的场合,例如访问控制、事务管理、性能监测、安全性、日志记录等,以减少代码的重复性,提高可维护性和可扩展性

面向切面编程(Aspect Oriented Programming,AOP)是软件编程思想发展到一定阶段的产物,是对面向对象编程(Object Oriented Programming,OOP)的有益补充

面向切面编程,简单地说就是在不改变原有程序的基础上为代码段增加新的功能,对其进行增强处理


打个比方,帮助理解AOP概念:

例如,假设由于业务发展的需要,现在项目要增加功能,我们不想去修改旧的代码,在不改变原代码的基础上新增功能,这里就需要使用AOP的思想。

这有点像汉堡包,我们想在中间加上一层菜,我们可以使用面向切面方法实现,但是包子我们就不能再加上新的菜进去而不影响整个包子,因为汉堡包是面向切面设计的,而包子不是。

**接下来,我们引入AOP的概念,如果上面的软件使用了一段时间后,用户想新增功能,比如在保存用户信息到数据库之前,用日志输出“准备添加数据”**
(注意,现代的迭代开发思想,在新增功能的时候,尽量不要去修改旧代码,直接写新功能就行。然后将新的代码“切入”到旧代码中去运行。这时就用到了面向切面编程AOP。)

添加新的功能,在不修改旧代码的同时

我们新增一个类,用于满足需求:添加用户表数据之前,输出“准备添加数据。在添加用户表成功之后,输出“添加数据完成”

①因此在service包下新建一个类为UserServiceLogger.java,里面创建俩个方法before和afterReturning

具体代码如下:

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("添加数据完成");
    }
}

在这里插入图片描述

可以看到引入import org.aspectj.lang.JoinPoint;,这一行报红

这是因为我们没有引入aspectjweaver 依赖,这个依赖是AspectJ 是一个用于实现面向切面编程 (AOP) 的框架,它提供了强大的切面编程功能,可以在程序中实现横切关注点(如日志记录、性能监视、事务管理等)

②因此我们需要更新一下pom.xml,往其中添加这一段依赖
<!--引入AspectJ 是一个用于实现面向切面编程 (AOP) 的框架-->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.7</version>
</dependency>
③编写spring-config.xml
<!-- 声明通知方法所在的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))" />
    <!-- 引用包含通知方法的Bean -->
    <aop:aspect ref="theLogger">
        <!-- 将before()方法定义为前置通知并引用pointcut切入点 -->
        <aop:before method="before" pointcut-ref="pointcut"></aop:before>
        <!-- 将afterReturning()方法定义为后置通知并引用pointcut切入点 -->
        <!-- 通过returning属性指定为名为result的参数注入返回值 -->
        <aop:after-returning method="afterReturning"
                             pointcut-ref="pointcut" returning="result" />
    </aop:aspect>
</aop:config>
bean中配置的aop代理代码解释:

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的AOP实现基于代理模式在Spring中,切面是通过代理对象来实现的,代理对象包装了目标对象,并在目标对象的方法执行前后执行一些特定操作,这些操作就是通知(Advice)


通知Advice的五种类型:
通知处理类型特点
Before前置通知处理,在目标方法前置通知执行处理
AfterReturning后置通知处理,在目标方法正常执行(不出现异常)后置 通知执行处理
AfterThrowing异常通知处理,在目标方法抛出异常后置通知执行处理
After最终通知处理,不论方法是否抛出异常,都会在目标方法最后通知执行处理
Around环绕通知处理,在目标方法的前后都会通知执行处理

PS:也有称之为前置增强的说法,但是“前置通知”更容易理解


以下是Spring AOP的关键概念和用法:

  1. 切点(Pointcut): 切点是一个表达式,用于定义在哪些连接点上应用通知。连接点是应用程序执行的点,通常是方法调用。切点表达式定义了哪些方法调用会被拦截。Spring 只支持方法连接点

  2. 通知(Advice): 通知是在连接点上执行的代码,它定义了在连接点的何时和如何执行。通知的类型包括前置通知、后置通知、环绕通知、异常通知和最终通知。

  3. 切面(Aspect): 切面是通知和切点的组合,它定义了在哪些连接点上应用哪些通知。切面是AOP的核心概念,它将关注点(例如事务管理、日志记录)模块化,以便将它们应用到不同的目标对象中。

  4. 目标对象(Target Object): 目标对象是真正执行业务逻辑的对象,它通常是一个普通的Java对象。通知在目标对象上运行。

  5. 代理对象(Proxy Object): 代理对象是包装目标对象的对象,通知实际上是在代理对象上执行的。Spring使用JDK动态代理和CGLIB代理来创建代理对象


Spring AOP支持两种类型的代理:

  • 基于接口的代理: 如果目标对象实现了一个或多个接口,Spring将使用JDK动态代理来创建代理对象。这是默认的代理方式
  • 基于类的代理: 如果目标对象没有实现接口,Spring将使用CGLIB代理来创建代理对象。这种方式要求目标对象不能被标记为final。

Spring AOP的配置可以使用XML配置文件或者注解来完成

通常,开发人员更喜欢使用注解来声明切面和通知,因为它更加简洁和易于理解。


如何判断SpringAOP使用了哪种代理?

​ Spring AOP,它默认使用基于 JDK 动态代理(JDK Dynamic Proxy)或者 CGLIB(Code Generation Library)代理

  1. 如果目标对象实现了至少一个接口,则 Spring AOP 使用 JDK 动态代理。JDK 动态代理要求目标对象实现接口,它通过创建目标对象的代理对象,实现接口中定义的方法,并在代理对象中织入增强逻辑。
  2. 如果目标对象没有实现任何接口,则 Spring AOP 使用 CGLIB 代理。CGLIB 会创建目标对象的子类,并在子类中织入增强逻辑。

​ Spring的AOP功能使得切面编程变得相对容易,可以帮助我们开发人员更好地管理和维护应用程序中的横切关注点。


刚才我们已经学习了前置通知**Before和后置通知AfterReturing**,

下面我们再介绍一下**异常通知AfterThrowing最终通知After**

满足需求

  • 如果切入点处的原代码运行发生出错的话,就跳去执行某段代码。这个是异常通知
  • 设置多一个无论报不报错,都执行的某段代码,叫最终通知。
①那我们就添加一个故意出错的代码进去,在UserServiceImpl。
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);
    }
}
②打开UserServiceLogger类,创建一个方法errorThrow() (名称自定义的),把添加出错之后要做的事情写进去,比如我们向日志输出“添加数据出错”。以及一个
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("谢谢使用");
	}
}
③编写spring-config.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/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 -->
    <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))" />
        <!-- 引用包含通知方法的Bean -->
        <aop:aspect ref="theLogger">
            <!-- 将before()方法定义为前置通知并引用pointcut切入点 -->
            <aop:before method="before" pointcut-ref="pointcut"></aop:before>
            <!-- 将afterReturning()方法定义为后置通知并引用pointcut切入点 -->
            
            <aop:after-returning method="afterReturning" pointcut-ref="pointcut"/>
             <!-- 将errorThrow()方法定义为异常通知并引用pointcut切入点 -->
            <aop:after-throwing method="errorThrow" pointcut-ref="pointcut"/>
             <!-- 将afterLogger()方法定义为最终通知并引用pointcut切入点 -->
            <aop:after method="afterLogger" pointcut-ref="pointcut"/>

        </aop:aspect>
    </aop:config>


</beans>
④运行俩次测试类,一次是有错误运行,第二次是无错误运行

有错误运行
在这里插入图片描述

异常通知正常运行,最终通知也正常运行

无错误运行:

在这里插入图片描述


注解方式实现AOP

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:aspectj-autoproxy/>

某些版本需要在后面加上:

<aop:aspectj-autoproxy proxy-target-class="true"/>

即可使用注解的方式实现AOP


总结

​ 今天我们首先介绍了三层架构设计模式,方便后面引入SpringMVC;然后主要讲解了Spring中的AOP,讲解了Spring AOP的概念、实现机制,五种通知类型,包括JDK动态代理和CGLib动态代理;接着讲解了基于XML的AOP实现,最后讲解了基于注解的AOP实现。通过今天的学习,各位读者可以对Spring的AOP实现有个大致的了解,为框架开发打下坚实基础。

​ 想要跟着学习的可以去我的资源里面找对应的文件下载,我的md文件也会发上去,项目文件会上传可以自己跟着学习一下。

作者:Stevedash

发表于:2023年9月5日 8点17分

注:本文内容基于个人学习理解,如有错误或疏漏,欢迎指正。感谢阅读!如果觉得有帮助,请点赞和分享。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Stevedash

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值