Java面试篇-AOP专题(什么是AOP、AOP的几个核心概念、AOP的常用场景、使用AOP记录操作日志、Spring中的事务是如何实现的)

news2024/12/31 6:17:18

文章目录

  • 1. 什么是AOP
  • 2. AOP的几个核心概念
  • 3. AOP的常用场景
  • 4. 使用AOP记录操作日志
    • 4.1 准备工作
      • 4.1.1 引入Maven依赖
      • 4.1.2 UserController.java
      • 4.1.3 User.java
      • 4.1.4 UserService.java
    • 4.2 具体实现(以根据id查询用户信息为例)
      • 4.2.1 定义切面类(切入点和环绕增强)
      • 4.2.2 自定义注解
      • 4.2.3 为方法添加自定义注解
    • 4.3 测试
  • 5. Spring中的事务是如何实现的

1. 什么是AOP

AOP(Aspect Oriented Programming),面向切面编程

AOP 主要的功能是将 与业务无关,但却对多个对象产生影响的公共行为或逻辑 抽取并封装为一个可重用的模块,这个可重用的模块称为切面(Aspect)

AOP 能够减少系统中的重复代码,降低模块间的耦合度,同时提高系统的可维护性


如果想了解 Spring 事务失效的情况,可以参考我的另一篇博文:Spring中事务失效的常见场景及解决方法

2. AOP的几个核心概念

AOP 的核心概念主要包括以下几个:

  1. 切面(Aspect)
    • 切面是AOP中的一个核心概念,它代表了一个横切关注点(cross-cutting concern),即将多个模块中共有的行为抽象出来形成的一个独立模块。在Spring AOP中,切面通常是通过使用@Aspect注解的类来实现的
  2. 连接点(Join Point)
    • 连接点是在程序执行过程中的一个特定点,例如方法的调用、异常的抛出等。在Spring AOP中,只支持方法的连接点
  3. 切入点(Pointcut)
    • 切入点是一组连接点的定义,它定义了哪些连接点会被切面所拦截。通常使用正则表达式或者特定的表达式语言来指定哪些方法会被拦截
  4. 通知(Advice)
    • 通知定义了切面在特定的连接点上要执行的动作。通知有多种类型:
      • 前置通知(Before):在连接点之前执行
      • 后置通知(After):在连接点之后执行,无论方法是否正常结束
      • 返回通知(After Returning):在连接点正常返回后执行
      • 异常通知(After Throwing):在连接点抛出异常后执行
      • 环绕通知(Around):包围一个连接点的通知,可以在方法调用前后执行自定义的行为
  5. 目标对象(Target Object)
    • 目标对象是指被一个或多个切面所通知的对象。在Spring AOP中,目标对象通常是Spring容器中的Bean
  6. 代理(Proxy)
    • AOP通过代理模式来实现对目标对象的增强。代理对象会在运行时创建,并用来代替目标对象。当调用代理对象的方法时,代理会根据切面的配置来执行相应的通知
  7. 织入(Weaving)
    • 织入是将切面的通知应用到目标对象并创建新的代理对象的过程。这个过程可以在编译时、类加载时或运行时进行

3. AOP的常用场景

  1. 记录操作日志:记录日志属于公共的行为,每一个 Service 都需要记录操作日志,直接在每一个 Service 里面编写记录日志的代码不太合适,使用 AOP 就可以很方便地完成记录日志操作
  2. 处理缓存:如果某些业务要添加缓存,直接写在 Service 层会造成代码耦合的情况,我们可以利用 AOP 的切面,拦截需要添加缓存的业务方法,为业务方法添加缓存
  3. Spring 中内置的事务处理

4. 使用AOP记录操作日志

4.1 准备工作

4.1.1 引入Maven依赖

在 SpringBoot 项目的 pom.xml 文件中引入 aspectjweaver 的 Maven 依赖

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

4.1.2 UserController.java

在这里插入图片描述

import cn.edu.scau.aop.pojo.User;
import cn.edu.scau.aop.service.UserService;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/getById/{id}")
    public User getById(@PathVariable("id") Integer id) {
        return userService.getById(id);
    }

    @PostMapping("/save")
    public void save(@RequestBody User user) {
        userService.save(user);
    }

    @PutMapping("/update")
    public void update(User user) {
        userService.update(user);
    }

    @DeleteMapping("/delete/{id}")
    public void delete(@PathVariable("id") Integer id) {
        userService.delete(id);
    }

}

4.1.3 User.java

public class User {

    private Integer id;

    private String name;

    private Integer age;

    private Short sex;

    private Short status;

    private String image;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Short getSex() {
        return sex;
    }

    public void setSex(Short sex) {
        this.sex = sex;
    }

    public Short getStatus() {
        return status;
    }

    public void setStatus(Short status) {
        this.status = status;
    }

    public String getImage() {
        return image;
    }

    public void setImage(String image) {
        this.image = image;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", sex=" + sex +
                ", status=" + status +
                ", image='" + image + '\'' +
                '}';
    }
    
}

4.1.4 UserService.java

import cn.edu.scau.aop.pojo.User;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    public User getById(Integer id) {
        return new User();
    }

    public void save(User user) {
        System.out.println("保存用户信息");
    }

    public void update(User user) {
        System.out.println("更新用户信息");
    }

    public void delete(Integer id) {
        System.out.println("删除用户信息");
    }

}

4.2 具体实现(以根据id查询用户信息为例)

在我们的开发过程中,大多都有记录操作日志的需求

在这里插入图片描述

当用户访问某一个接口时,我们需要记录发起请求的用户是谁,请求的方式是什么,访问地址是什么,访问了哪一个模块,登录的 IP 地址,操作时间等


接下来我们分析一下利用 AOP 记录操作日志的具体实现方式

假如后台有四个请求的接口:登录、新增用户、更新用户、删除用户

在这里插入图片描述

我们以查询用户为例,利用 AOP 提供的环绕通知做一个切面

在这里插入图片描述

4.2.1 定义切面类(切入点和环绕增强)

用 @Aspect 注解表名当前类是一个切面类,而且切面类需要交由 Spring 管理

import cn.edu.scau.aop.annotation.Log;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.reflect.Method;
import java.util.Date;

/**
 * 切面类
 */
@Component
@Aspect
public class SystemAspect {

    @Pointcut("@annotation(cn.edu.scau.aop.annotation.Log)")
    private void pointcut() {

    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 通过解析 session 或 token 获取用户名

        // 获取被增强类和方法的信息
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;

        // 获取被增强的方法对象
        Method method = methodSignature.getMethod();

        // 从方法中解析注解
        if (method != null) {
            Log logAnnotation = method.getAnnotation(Log.class);
            System.out.println(logAnnotation.name());
        }

        // 方法名字
        String name = null;
        if (method != null) {
            name = method.getName();
        }
        System.out.println("方法名:" + name);

        // 通过工具类获取Request对象
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
        HttpServletRequest request = null;
        if (servletRequestAttributes != null) {
            request = servletRequestAttributes.getRequest();
        }

        // 访问的URL
        String url = null;
        if (request != null) {
            url = request.getRequestURI();
        }
        System.out.println("访问的URL:" + url);

        // 请求方式
        String methodName = null;
        if (request != null) {
            methodName = request.getMethod();
        }
        System.out.println("请求方式:" + methodName);

        // 登录IP
        String ipAddress = null;
        if (request != null) {
            ipAddress = getIpAddress(request);
        }
        System.out.println("登录IP:" + ipAddress);

        // 操作时间
        System.out.println("操作时间:" + new Date());

        // 将操作日志保存到数据库

        return joinPoint.proceed();
    }

    /**
     * 获取 IP 地址
     *
     * @param request HttpServletRequest
     * @return String
     */
    public String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");

        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }

        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
    }

}

切面类中有一个切点表达式

在这里插入图片描述

这个切点表达式找的是一个注解,也就是说,如果某个方法上添加了 Log 注解,进化就会进入到环绕通知中进行增强


环绕通知增强如下

在这里插入图片描述

@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    // 通过解析 session 或 token 获取用户名

    // 获取被增强类和方法的信息
    Signature signature = joinPoint.getSignature();
    MethodSignature methodSignature = (MethodSignature) signature;

    // 获取被增强的方法对象
    Method method = methodSignature.getMethod();

    // 从方法中解析注解
    if (method != null) {
        Log logAnnotation = method.getAnnotation(Log.class);
        System.out.println("模块名称:" + logAnnotation.name());
    }

    // 方法名字
    String name = null;
    if (method != null) {
        name = method.getName();
    }
    System.out.println("方法名:" + name);

    // 通过工具类获取Request对象
    RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
    HttpServletRequest request = null;
    if (servletRequestAttributes != null) {
        request = servletRequestAttributes.getRequest();
    }

    // 访问的URL
    String url = null;
    if (request != null) {
        url = request.getRequestURI();
    }
    System.out.println("访问的URL:" + url);

    // 请求方式
    String methodName = null;
    if (request != null) {
        methodName = request.getMethod();
    }
    System.out.println("请求方式:" + methodName);

    // 登录IP
    String ipAddress = null;
    if (request != null) {
        ipAddress = getIpAddress(request);
    }
    System.out.println("登录IP:" + ipAddress);

    // 操作时间
    System.out.println("操作时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

    // 将操作日志保存到数据库

    return joinPoint.proceed();
}

/**
 * 获取 IP 地址
 *
 * @param request HttpServletRequest
 * @return String
 */
public String getIpAddress(HttpServletRequest request) {
    String ip = request.getHeader("x-forwarded-for");

    if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getHeader("Proxy-Client-IP");
    }
    if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getHeader("WL-Proxy-Client-IP");
    }
    if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getRemoteAddr();
    }

    return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}

4.2.2 自定义注解

那 Log 注解是从哪里来的呢,其实是我们自定义的,注解中的 name 属性是模块的名称

在这里插入图片描述

import java.lang.annotation.*;

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {

    /**
     * 模块名称
     */
    String name() default "";

}

4.2.3 为方法添加自定义注解

我们在需要记录操作日志的方法上添加自定义注解

在这里插入图片描述

4.3 测试

启动项目后,我们在浏览器输入以下网址访问接口

http://localhost:8080/user/getById/1

查看控制台,发现操作日志已成功打印操作日志

在这里插入图片描述

5. Spring中的事务是如何实现的

Spring支持编程式事务管理和声明式事务管理两种方式

  • 编程式事务控制:需使用 TransactionTemplate 来进行实现,对业务代码有侵入性,项目中很少使用
  • 声明式事务管理:声明式事务管理建立在 AOP 之上,本质是通过 AOP 对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务

声明式事务的示意图

在这里插入图片描述

joinPoint.proceed 是真正要执行的目标对象的方法,在方法执行前开启事务,方法成功执行之后提交事务

如果方法在执行的过程中出错了,需要回滚事务,在 catch 代码块中会有一个回滚事务的操作

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2156036.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

整合多方大佬博客以及视频 一文读懂 servlet

参考文章以及视频 文章&#xff1a; 都2023年了&#xff0c;Servlet还有必要学习吗&#xff1f;一文带你快速了解Servlet_servlet用得多吗-CSDN博客 【计算机网络】HTTP 协议详解_3.简述浏览器请求一个网址的过程中用到的网络协议,以及协议的用途(写关键点即可)-CSDN博客 【…

[数据结构]无头单向非循环链表的实现与应用

文章目录 一、引言二、线性表的基本概念1、线性表是什么2、链表与顺序表的区别3、无头单向非循环链表 三、无头单向非循环链表的实现1、结构体定义2、初始化3、销毁4、显示5、增删查改 四、分析无头单向非循环链表1、存储方式2、优点3、缺点 五、总结1、练习题2、源代码 一、引…

Mysql----索引与事务

1.索引 1.1什么是MYSQL的索引 MySQL官方对于索引的定义&#xff1a;索引是帮助Mysql高效获取数据的数据结构 Mysql在存储数据之外&#xff0c;数据库系统中还维护着满足特定查找算法的数据结构&#xff0c;这些数据结构以某种引用&#xff08;指向&#xff09;表中的数据&…

萤石云平台接入SVMSPro平台

萤石云平台接入SVMSPro平台 步骤一&#xff1a;进入萤石云官网&#xff1a;https://open.ys7.com/ &#xff0c;点右上角的登陆&#xff0c;填写自己的用户名密码&#xff1b; 步骤二&#xff1a;登陆进去后&#xff0c;开发者服务—>我的账号—>应用信息&#xff0c;在…

电气自动化入门07:开关电源、三相异步电动机多地与顺序控制电路

视频链接&#xff1a;3.5 电工知识&#xff1a;三相交流异步电动机多地与顺序控制及开关电源选型_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1PJ41117PW?p9&vd_sourceb5775c3a4ea16a5306db9c7c1c1486b5 1.开关电源功能与选型说明&#xff1a; 2.三相异步电动机…

数据结构与算法之间有何关系?

相信很多人都应该上个《数据结构与算法》这门课吧&#xff0c;而这两个概念也如孪生兄弟一样经常被拿出来一起讨论。那它们究竟是一个什么样子的关系呢&#xff1f; 听到数据结构与算法我第一反应是想到了Pascal 语言之父尼古拉斯沃斯在他的《Algorithms Data Structures Pro…

esp32s3分区表配置及读写

一、分区表介绍 每片 ESP32-S3 的 flash 可以包含多个应用程序&#xff0c;以及多种不同类型的数据&#xff08;例如校准数据、文件系统数据、参数存储数据等&#xff09;。因此&#xff0c;在 flash 的 默认偏移地址 0x8000 处烧写一张分区表。 分区表中的每个条目都包括以下…

【d47】【Java】【力扣】997.找到小镇的法官

思路 记录入度和出度 一个人可以连接多个&#xff0c;一个人也可以被多个人连接&#xff0c;就是图的性质 一个人可以信任多人&#xff0c;一个人也可以被多个人信任 统计入度出度&#xff0c; 法官&#xff1a;入度n-1,出度0 其他人&#xff1a;因为被所有其他人信任的 只能…

JS执行机制(同步和异步)

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。 异步:在做这件事的同时&#xff0c;你还可以去处理其他事 他们的本质区别&#xff1a;这条流水线上各个流程的执行顺序不同。 同步任务 同步任务都在主线程上执行&#xff0c;形成一个执行栈。 异步…

7、论等保的必要性

数据来源&#xff1a;7.论等保的必要性_哔哩哔哩_bilibili 等级保护必要性 降低信息安全风险 等级保护旨在降低信息安全风险&#xff0c;提高信息系统的安全防护能力。 风险发现与整改 开展等级保护的最重要原因是通过测评工作&#xff0c;发现单位系统内外部的安全风险和脆弱…

【计网】从零开始掌握序列化 --- JSON实现协议 + 设计 传输\会话\应用 三层结构

唯有梦想才配让你不安&#xff0c; 唯有行动才能解除你的不安。 --- 卢思浩 --- 从零开始掌握序列化 1 知识回顾2 序列化与编写协议2.1 使用Json进行序列化2.2 编写协议 3 封装IOService4 应用层 --- 网络计算器5 总结 1 知识回顾 上一篇文章我们讲解了协议的本质是双方能够…

【JavaEE】多线程编程引入——认识Thread类

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯&#xff0c;你们的点赞收藏是我前进最大的动力&#xff01;&#xff01;希望本文内容能帮到你&#xff01; 目录 引入&#xff1a; 一&#xff1a;Thread类 1&#xff1a;Thread类可以直接调用 2&#xff1a;run方法 &a…

SpringBoot+thymeleaf竞赛报名系统

一、介绍 > 这是一个基于Spring Boot的后台管理系统。 > 使用了Mybatis Plus作为持久层框架&#xff0c;EasyExcel用于Excel操作&#xff0c;Thymeleaf作为前端模板引擎。 > 界面简洁&#xff0c;功能丰富&#xff0c;完成度比较高&#xff0c;适用于JAVA初学者作…

安国U盘量产工具系列下载地址

来源地址&#xff08;访问需要科学工具&#xff09;&#xff1a;AlcorMP (Последняя версия ALCOR U2 MP v23.08.07.00.H) – [USBDev.ru] 版本列表&#xff1a; AlcorMP&#xff08;最新版本的 ALCOR U2 MP v23.08.07.00.H&#xff09; AlcorMP是在Alcor Mic…

SpringBoot项目License证书生成与验证(TrueLicense) 【记录】

SpringBoot项目License证书生成与验证(TrueLicense) 【记录】 在非开源产品、商业软件、收费软件等系统的使用上&#xff0c;需要考虑系统的使用版权问题&#xff0c;不能随便一个人拿去在任何环境都能用。应用部署一般分为两种情况&#xff1a; 应用部署在开发者自己的云服务…

数据集-目标检测系列-火车检测数据集 train >> DataBall

数据集-目标检测系列-火车检测数据集 train >> DataBall 数据集-目标检测系列-火车检测数据集 数据量&#xff1a;1W 想要进一步了解&#xff0c;请联系 DataBall。 DataBall 助力快速掌握数据集的信息和使用方式&#xff0c;会员享有 百种数据集&#xff0c;不断增加…

跟李沐学AI:注意力机制、注意力分数

目录 不随意线索 随意线索 注意力机制 非参注意力池化层 参数化的注意力机制 注意力机制总结 注意力分数 拓展到高维度 加性模型&#xff08;Additive Attention&#xff09; 点积注意力机制&#xff08;Dot Product Attention&#xff09; 注意力分数总结 不随意线…

vscode 顶部 Command Center,minimap

目录 vscode 顶部 Command Center 设置显示步骤: minimap设置 方法一:使用设置界面 方法二:使用命令面板 方法三:编辑 settings.json 文件 左侧目录树和编辑器字体不一致: vscode 顶部 Command Center Visual Studio Code (VSCode) 中的 Command Center 是一个集中…

240912-设置WSL中的Ollama可在局域网访问

A. 最终效果 B. 设置Ollama&#xff08;前提&#xff09; sudo vim /etc/systemd/system/ollama.service[Unit] DescriptionOllama Service Afternetwork-online.target[Service] ExecStart/usr/bin/ollama serve Userollama Groupollama Restartalways RestartSec3 Environme…

Python redis 安装和使用介绍

python redis安装和使用 一、Redis 安装1.1、Windows安装 二、安装 redis 模块二、使用redis 实例1.1、简单使用1.2、连接池1.3、redis 基本命令 String1.3.1、ex - 过期时间&#xff08;秒&#xff09;1.3.2、nx - 如果设置为True&#xff0c;则只有name不存在时&#xff0c;当…