Spring AOP

在写这篇文章之前,小编开始背八股文了!!恰逢遇到了Spring AOP的相关内容,仔细回顾下来,发现,对应Spring AOP仅仅局限于: AOP(面向切面编程)就是将那些与业务无关,但是在项目中发挥着不可缺少的作用的代码封装起来,比如:事务,日志,权限等相关的代码,Spring AOP是基于动态代理的方式实现的,如果实现了某个接口,则会基于动态代理的方式实现,如果没有实现接口,则是基于CGLIB代理的方式来实现一个需要代理对象的子类作为代理对象!其实上面这些内容没什么错误!

这是八股文的正常范畴,但是,今日,小编不想再按照八股文来将一大批文字意思了!直接上代码,上案列,来带领大家走进Spring AOP(面向切面编程)!

场景:

假设,我们需要统计某项目中的某些方法的耗时较长的方法,此时就需要我们来统计每个业务方法的执行耗时!直接思路:在每个方法开始执行之前记录当前时间,然后在方法执行结束后在记录当前时间,然后在一相减就是最后的执行耗时!当然,这个想法是没有什么问题的,问题出现在,当这个项目中方法比较少时,所需要修改的代码比较少,但是,当这个项目中的方法比较多,成千上百的时候,所需要更改的代码就显得很繁琐了!这样就得不偿失了!

所以Spring AOP应运而生

Spring AOP就可以将公共的记录方法开始时间,结束时间的相关代码来进行抽取出来,封装成一个注解类,通过添加注解的方式来实现注入,那么,这样不就显得更加简单了吗?仅仅一个注解就解决了一大串代码的更改!!YYDS

那么,我们所谓的面向切面编程最简单的描述就是:面向一个方法或者多个方法进行编程!(不全面哈!)

实现:动态代理是面向切面编程最主流的实现,而Spring AOP是Spring框架的高级技术,旨在管理Bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程/

导入依赖:在pom.xml文件中导入AOP依赖:

     <!--  Spring AOP相关依赖    -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

编写AOP程序:针对特定方法根据业务需要进行编程!

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Slf4j
@Component //交给Spring容器进行管理,成为Bean
@Aspect  //有这个注解,说明当前类不是普通类,而是AOP类
public class TimeAspect {

    @Around("execution(* com.example.controller.*.*(..))")  //拦截所有controller包下的所有方法
    //当我们需要获取其他包/类下的方法时候,只需要在execution()中添加包/类名即可【支持|| 】
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis(); //记录开始时间
        Object obj = joinPoint.proceed();//调用原始方法运行
        long end = System.currentTimeMillis(); //记录结束时间
        log.info(joinPoint.getSignature().getName() + "方法运行时间:" + (end - start) + "ms");
        return obj; //返回原始方法的返回值
    }

}

重新启动程序,运行项目:然后在控制台中就会出现:【当然具体内容还得看您那边调用了哪个类/方法】

然后,大家就会发现,我们并没有修改任何有关项目中的代码,但是却实现了统计各个方法的运行时间!!YYDS啊!!这不就是我们想要的效果吗??

上面统计各个方法的运行时间,仅仅是Spring AOP的小试牛刀,其实Spring AOP不止这一个功能:Spring AOP也能进行记录操作日志,权限控制,事务管理………

Spring AOP核心概念:

  • 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
  • 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
  • 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
  • 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
  • 目标对象:Target,通知所应用的对象

Spring AOP的通知类型:

  • @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
  • @Before:前置通知,此注解标注的通知方法在目标方法前被执行
  • @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
  • @AfterReturning :返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
  • @AfterThrowing :异常后通知,此注解标注的通知方法发生异常后执行

注意事项:

  1. @Around环绕通知需要自己调用 Proceeding]oinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行
  2. @Around环绕通知方法的返回值,必须指定为0bject,来接收原始方法的返回值。

参考写法1:

参考写法2:

根据参考写法1,我们可以看出,上述的切入点表达式重复了,当我们需要对切入点表达式进行改动,是否需要将上述的切入点表达式一个一个的来进行改动呢??这部就又变得繁琐了吗??那么,本着应简尽简的原则,那么,我们也需要将公共/重复的切入点表达式提取出来!

 值得注意的是:上面的切入点表达式是用private来修饰的,关于private的相关属性相比大家也都了解了!只能在当前类当中使用,那么,假如我想在其他类中引用当前切入点表达式就需要将其更改为public即可!

那么,当我们引入其他类的切入点表达式的时候,需要注意:该切入点表达式对应的包名+类名+方法名!!

@pointCut注解:

该注解的作用就是将公共的切点表达式抽取出来,需要用到时引用该切入点表达式即可!

通知顺序:

然而,当我们定义了多个切面类,而且每个切面类对应的目标方法都是同一个的时候,该如何处理呢??

显而易见的是:当有多个前面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行!

比如:

上述定义了三个切面类,而且每个切面类的切入点表达式通知方法都是一样的,只不过输出的日志标识不一样罢了!

最终程序的执行顺序为:

这样来看是:

  • 类名越靠前before越先执行,类名越靠后before越后执行!
  • 类名越靠前after越后执行,类名越靠后after越先执行!

前提:在没定义其他要求的情况下:跟类名有关!!

这种方式非常繁琐,而且不便管理!!

所以,Spring给我们提供了第二种方式,我们可以直接在切面类上添加@Order(数字)注解即可!!

用@Order(数字)注解加在切面类上来控制顺序:数字就是来控制执行顺序的

  • 目标方法前的通知方法:数字小的先执行!
  • 目标方法后的通知方法:数字小的后执行!

切入点表达式:

切入点表达式--execution(常见)

 

常见的切入点表达式的写法:

专门执行两个方法:可以使用“ || ”来连接(或者)

书写建议:

  • 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是 update开头
  • 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
  • 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:忽名匹配尽量不使用…,使用*匹配单个包。
切入点表达式--@annotation

@annotation切入点表达式,用于匹配标识有特定注解的方法!

这就涉及到自定义注解的相关知识了!

这个注解起啥名,无所谓,自己知道就行!

自定义注解:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)         // 注解作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时保留
public @interface MyLog {
}

该注解当中并没有指定属性,仅仅起到标识作用!!

然后在方法上加上刚刚自定义的注解即可!

注意!!

如果你还想在匹配一个方法呢??那么,此时只需要在想要匹配的方法上,加入该注解即可!!

一切改完后,重启程序!!即可!

连接点:

在Spring中用]oinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等,

  • 对于 @Around 通知,获取连接点信息只能使用 ProceedingJoinPoint
  • 对于其他四种通知,获取连接点信息只能使用 JoinPoint,它是 ProceedingJoinPoint 的父类型

Spring AOP案列:记录操作日志:

后端相关代码:

引入依赖:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

切面注解

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoLog {
    String value() default "";
}

AOP实现操作日志的记录:
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.example.entity.Admin;
import com.example.entity.Log;
import com.example.service.LogService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

/**
 * 处理切面的“监控”
 */
@Component
@Aspect
public class LogAspect {

    @Resource
    private LogService logService;

    @Around("@annotation(autoLog)")
    public Object doAround(ProceedingJoinPoint joinPoint, AutoLog autoLog) throws Throwable {

        // 操作内容,我们在注解里已经定义了value(),然后再需要切入的接口上面去写上对应的操作内容即可
        String name = autoLog.value();
        // 操作时间(当前时间)
        String time = DateUtil.now();
        // 操作人
        String username = "";
        Admin user = JwtTokenUtils.getCurrentUser();
        if (ObjectUtil.isNotNull(user)) {
            username = user.getName();
        }
        // 操作人IP
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String ip = request.getRemoteAddr();

        // 执行具体的接口
        Result result = (Result) joinPoint.proceed();

        Object data = result.getData();
        if (data instanceof Admin) {
            Admin admin = (Admin) data;
            username = admin.getName();
        }


        // 再去往日志表里写一条日志记录
        Log log = new Log(null, name, time, username, ip);
        logService.add(log);

        // 你可以走了,去返回前台报到吧~
        return result;
    }
}

关于操作日志整个相关代码:仅供参考:

数据库表:
CREATE TABLE `log` (
  `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作内容',
  `time` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作时间',
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作人',
  `ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作人IP',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=42 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志表';
Entity
import javax.persistence.*;

@Table(name = "log")
public class Log {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(name = "name")
    private String name;
    @Column(name = "time")
    private String time;
    @Column(name = "username")
    private String username;
    @Column(name = "ip")
    private String ip;
}
Controller
import com.example.common.Result;
import com.example.entity.Log;
import com.example.entity.Params;
import com.example.service.LogService;
import com.github.pagehelper.PageInfo;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

@CrossOrigin
@RestController
@RequestMapping("/log")
public class LogController {

    @Resource
    private LogService logService;


    @PostMapping
    public Result save(@RequestBody Log log) {
       logService.add(log);
        return Result.success();
    }

    @GetMapping("/search")
    public Result findBySearch(Params params) {
        PageInfo<Log> info = logService.findBySearch(params);
        return Result.success(info);
    }

    @DeleteMapping("/{id}")
    public Result delete(@PathVariable Integer id) {
        logService.delete(id);
        return Result.success();
    }

}
Service
import com.example.dao.LogDao;
import com.example.entity.Log;
import com.example.entity.Params;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;


@Service
public class LogService {

    @Resource
    private LogDao logDao;


    public void add(Log type) {
        logDao.insertSelective(type);
    }


    public PageInfo<Log> findBySearch(Params params) {
        // 开启分页查询
        PageHelper.startPage(params.getPageNum(), params.getPageSize());
        // 接下来的查询会自动按照当前开启的分页设置来查询
        List<Log> list = logDao.findBySearch(params);
        return PageInfo.of(list);
    }

    public void delete(Integer id) {
        logDao.deleteByPrimaryKey(id);
    }

}
Dao和Mapper
import com.example.entity.Log;
import com.example.entity.Params;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import tk.mybatis.mapper.common.Mapper;

import java.util.List;


@Repository
public interface LogDao extends Mapper<Log> {

    List<Log> findBySearch(@Param("params") Params params);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.dao.LogDao">

    <select id="findBySearch" resultType="com.example.entity.Log">
        select * from log
        <where>
            <if test="params != null and params.name != null and params.name != ''">
                and name like concat('%', #{ params.name }, '%')
            </if>
            <if test="params != null and params.username != null and params.username != ''">
                and username like concat('%', #{ params.username }, '%')
            </if>
        </where>
    </select>

</mapper>
LogView.vue
<template>
  <div>
    <div style="margin-bottom: 15px">
      <el-input v-model="params.name" style="width: 200px" placeholder="请输入操作内容"></el-input>
      <el-input v-model="params.username" style="width: 200px; margin-left: 5px" placeholder="请输入操作人"></el-input>
      <el-button type="warning" style="margin-left: 10px" @click="findBySearch()">查询</el-button>
      <el-button type="warning" style="margin-left: 10px" @click="reset()">清空</el-button>
    </div>
    <div>
      <el-table :data="tableData" style="width: 100%">
        <el-table-column prop="name" label="操作内容"></el-table-column>
        <el-table-column prop="time" label="操作时间"></el-table-column>
        <el-table-column prop="username" label="操作人"></el-table-column>
        <el-table-column prop="ip" label="ip"></el-table-column>
        <el-table-column label="操作">
          <template slot-scope="scope">
            <el-popconfirm title="确定删除吗?" @confirm="del(scope.row.id)">
              <el-button slot="reference" type="danger" style="margin-left: 5px">删除</el-button>
            </el-popconfirm>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <div style="margin-top: 10px">
      <el-pagination
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
          :current-page="params.pageNum"
          :page-sizes="[5, 10, 15, 20]"
          :page-size="params.pageSize"
          layout="total, sizes, prev, pager, next"
          :total="total">
      </el-pagination>
    </div>
  </div>
</template>

<script>
import request from "@/utils/request";

export default {
  data() {
    return {
      params: {
        name: '',
        username: '',
        pageNum: 1,
        pageSize: 5
      },
      tableData: [],
      total: 0,
      dialogFormVisible: false,
      form: {}
    }
  },
  // 页面加载的时候,做一些事情,在created里面
  created() {
    this.findBySearch();
  },
  // 定义一些页面上控件出发的事件调用的方法
  methods: {
    findBySearch() {
      request.get("/log/search", {
        params: this.params
      }).then(res => {
        if (res.code === '0') {
          this.tableData = res.data.list;
          this.total = res.data.total;
        } else {
          this.$message({
            message: res.msg,
            type: 'success'
          });
        }
      })
    },
    reset() {
      this.params = {
        pageNum: 1,
        pageSize: 5,
        name: '',
        username: '',
      }
      this.findBySearch();
    },
    handleSizeChange(pageSize) {
      this.params.pageSize = pageSize;
      this.findBySearch();
    },
    handleCurrentChange(pageNum) {
      this.params.pageNum = pageNum;
      this.findBySearch();
    },
    submit() {
      request.post("/log", this.form).then(res => {
        if (res.code === '0') {
          this.$message({
            message: '操作成功',
            type: 'success'
          });
          this.dialogFormVisible = false;
          this.findBySearch();
        } else {
          this.$message({
            message: res.msg,
            type: 'error'
          });
        }
      })
    },
    del(id) {
      request.delete("/log/" + id).then(res => {
        if (res.code === '0') {
          this.$message({
            message: '删除成功',
            type: 'success'
          });
          this.findBySearch();
        } else {
          this.$message({
            message: res.msg,
            type: 'success'
          });
        }
      })
    }
  }
}
</script>

警告!!!

上述记录操作日志的Controller,Service,Dao,Mapper,Vue等相关代码仅供参考!!不全!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

念君思宁

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

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

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

打赏作者

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

抵扣说明:

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

余额充值