Java学习指南
  • Java 编程的逻辑
  • Java进阶
  • Java FrameWorks
  • 了解 USB Type-A,B,C 三大标准接口
  • 深入浅出DDD
  • 重构:改善既有代码的设计
  • 面试大纲
  • 云原生
    • 什么是无服务器(what is serverless)?
  • 博客
    • 深入分析Log4j 漏洞
  • 博客
    • Serverless之快速搭建Spring Boot应用
  • 博客
    • 使用 Prometheus + Grafana + Spring Boot Actuator 监控应用
  • 博客
    • 使用 Prometheus + Grafana 监控 MySQL
  • 博客
    • 使用Github Actions + Docker 部署Spring Boot应用
  • 博客
    • Redis分布式锁之Redisson的原理和实践
  • 博客
    • 数据库中的树结构应该怎样去设计
  • 学习&成长
    • 如何成为技术大牛
  • 开发工具
    • Git Commit Message Guidelines
  • 开发工具
    • git命名大全
  • 开发工具
    • Gradle vs Maven Comparison
  • 开发工具
    • Swagger2常用注解及其说明
  • 开发工具
    • 简明 VIM 练级攻略
  • 微服务
    • 十大微服务设计模式和原则
  • 微服务
    • 微服务下的身份认证和令牌管理
  • 微服务
    • 微服务坏味道之循环依赖
  • 设计模式
    • 设计模式 - JDK中的设计模式
  • 设计模式
    • 设计模式 - Java三种代理模式
  • 设计模式
    • 设计模式 - 六大设计原则
  • 设计模式
    • 设计模式 - 单例模式
  • 设计模式
    • 设计模式 - 命名模式
  • 设计模式
    • 设计模式 - 备忘录模式
  • 设计模式
    • 设计模式 - 概览
  • 设计模式
    • 设计模式 - 没用的设计模式
  • 质量&效率
    • Homebrew 替换国内镜像源
  • 质量&效率
    • 工作中如何做好技术积累
  • Java FrameWorks
    • Logback
      • 自定义 logback 日志过滤器
  • Java FrameWorks
    • Mybatis
      • MyBatis(十三) - 整合Spring
  • Java FrameWorks
    • Mybatis
      • MyBatis(十二) - 一些API
  • Java FrameWorks
    • Mybatis
      • Mybatis(一) - 概述
  • Java FrameWorks
    • Mybatis
      • Mybatis(七) - 结果集的封装与映射
  • Java FrameWorks
    • Mybatis
      • Mybatis(三) - mapper.xml及其加载机制
  • Java FrameWorks
    • Mybatis
      • Mybatis(九) - 事务
  • Java FrameWorks
    • Mybatis
      • Mybatis(二) - 全局配置文件及其加载机制
  • Java FrameWorks
    • Mybatis
      • Mybatis(五) - SqlSession执行流程
  • Java FrameWorks
    • Mybatis
      • Mybatis(八) - 缓存
  • Java FrameWorks
    • Mybatis
      • Mybatis(六) - 动态SQL的参数绑定与执行
  • Java FrameWorks
    • Mybatis
      • Mybatis(十) - 插件
  • Java FrameWorks
    • Mybatis
      • Mybatis(十一) - 日志
  • Java FrameWorks
    • Mybatis
      • Mybatis(四) - Mapper接口解析
  • Java FrameWorks
    • Netty
      • Netty 可靠性分析
  • Java FrameWorks
    • Netty
      • Netty - Netty 线程模型
  • Java FrameWorks
    • Netty
      • Netty堆外内存泄露排查盛宴
  • Java FrameWorks
    • Netty
      • Netty高级 - 高性能之道
  • Java FrameWorks
    • Shiro
      • Shiro + JWT + Spring Boot Restful 简易教程
  • Java FrameWorks
    • Shiro
      • 非常详尽的 Shiro 架构解析!
  • Java FrameWorks
    • Spring
      • Spring AOP 使用介绍,从前世到今生
  • Java FrameWorks
    • Spring
      • Spring AOP 源码解析
  • Java FrameWorks
    • Spring
      • Spring Event 实现原理
  • Java FrameWorks
    • Spring
      • Spring Events
  • Java FrameWorks
    • Spring
      • Spring IOC容器源码分析
  • Java FrameWorks
    • Spring
      • Spring Integration简介
  • Java FrameWorks
    • Spring
      • Spring MVC 框架中拦截器 Interceptor 的使用方法
  • Java FrameWorks
    • Spring
      • Spring bean 解析、注册、实例化流程源码剖析
  • Java FrameWorks
    • Spring
      • Spring validation中@NotNull、@NotEmpty、@NotBlank的区别
  • Java FrameWorks
    • Spring
      • Spring 如何解决循环依赖?
  • Java FrameWorks
    • Spring
      • Spring 异步实现原理与实战分享
  • Java FrameWorks
    • Spring
      • Spring中的“for update”问题
  • Java FrameWorks
    • Spring
      • Spring中的设计模式
  • Java FrameWorks
    • Spring
      • Spring事务失效的 8 大原因
  • Java FrameWorks
    • Spring
      • Spring事务管理详解
  • Java FrameWorks
    • Spring
      • Spring计时器StopWatch使用
  • Java FrameWorks
    • Spring
      • 详述 Spring MVC 框架中拦截器 Interceptor 的使用方法
  • Java FrameWorks
    • Spring
      • 透彻的掌握 Spring 中@transactional 的使用
  • Java
    • Java IO&NIO&AIO
      • Java IO - BIO 详解
  • Java
    • Java IO&NIO&AIO
      • Java NIO - IO多路复用详解
  • Java
    • Java IO&NIO&AIO
      • Java N(A)IO - Netty
  • Java
    • Java IO&NIO&AIO
      • Java IO - Unix IO模型
  • Java
    • Java IO&NIO&AIO
      • Java IO - 分类
  • Java
    • Java IO&NIO&AIO
      • Java NIO - 基础详解
  • Java
    • Java IO&NIO&AIO
      • Java IO - 常见类使用
  • Java
    • Java IO&NIO&AIO
      • Java AIO - 异步IO详解
  • Java
    • Java IO&NIO&AIO
      • Java IO概述
  • Java
    • Java IO&NIO&AIO
      • Java IO - 设计模式
  • Java
    • Java IO&NIO&AIO
      • Java NIO - 零拷贝实现
  • Java
    • Java JVM
      • JVM 优化经验总结
  • Java
    • Java JVM
      • JVM 内存结构
  • Java
    • Java JVM
      • JVM参数设置
  • Java
    • Java JVM
      • Java 内存模型
  • Java
    • Java JVM
      • 从实际案例聊聊Java应用的GC优化
  • Java
    • Java JVM
      • Java 垃圾回收器G1详解
  • Java
    • Java JVM
      • 垃圾回收器Shenandoah GC详解
  • Java
    • Java JVM
      • 垃圾回收器ZGC详解
  • Java
    • Java JVM
      • 垃圾回收基础
  • Java
    • Java JVM
      • 如何优化Java GC
  • Java
    • Java JVM
      • 类加载机制
  • Java
    • Java JVM
      • 类字节码详解
  • Java
    • Java 基础
      • Java hashCode() 和 equals()
  • Java
    • Java 基础
      • Java 基础 - Java native方法以及JNI实践
  • Java
    • Java 基础
      • Java serialVersionUID 有什么作用?
  • Java
    • Java 基础
      • Java 泛型的类型擦除
  • Java
    • Java 基础
      • Java 基础 - Unsafe类解析
  • Java
    • Java 基础
      • Difference Between Statement and PreparedStatement
  • Java
    • Java 基础
      • Java 基础 - SPI机制详解
  • Java
    • Java 基础
      • Java 基础 - final
  • Java
    • Java 基础
      • Java中static关键字详解
  • Java
    • Java 基础
      • 为什么说Java中只有值传递?
  • Java
    • Java 基础
      • Java 基础 - 即时编译器原理解析及实践
  • Java
    • Java 基础
      • Java 基础 - 反射
  • Java
    • Java 基础
      • Java多态的面试题
  • Java
    • Java 基础
      • Java 基础 - 异常机制详解
  • Java
    • Java 基础
      • 为什么要有抽象类?
  • Java
    • Java 基础
      • 接口的本质
  • Java
    • Java 基础
      • Java 基础 - 枚举
  • Java
    • Java 基础
      • Java 基础 - 泛型机制详解
  • Java
    • Java 基础
      • Java 基础 - 注解机制详解
  • Java
    • Java 基础
      • 为什么 String hashCode 方法选择数字31作为乘子
  • Java
    • Java 并发
      • Java 并发 - 14个Java并发容器
  • Java
    • Java 并发
      • Java 并发 - AQS
  • Java
    • Java 并发
      • Java 并发 - BlockingQueue
  • Java
    • Java 并发
      • Java 并发 - CAS
  • Java
    • Java 并发
      • Java 并发 - Condition接口
  • Java
    • Java 并发
      • Java 并发 - CopyOnWriteArrayList
  • Java
    • Java 并发
      • Java 并发 - CountDownLatch、CyclicBarrier和Phaser对比
  • Java
    • Java 并发
      • Java 并发 - Fork&Join框架
  • Java
    • Java 并发
      • Java 并发 - Java CompletableFuture 详解
  • Java
    • Java 并发
      • Java 并发 - Java 线程池
  • Java
    • Java 并发
      • Java 并发 - Lock接口
  • Java
    • Java 并发
      • Java 并发 - ReentrantLock
  • Java
    • Java 并发
      • Java 并发 - ReentrantReadWriteLock
  • Java
    • Java 并发
      • Java 并发 - Synchronized
  • Java
    • Java 并发
      • Java 并发 - ThreadLocal 内存泄漏问题
  • Java
    • Java 并发
      • Java 并发 - ThreadLocal
  • Java
    • Java 并发
      • Java 并发 - Volatile
  • Java
    • Java 并发
      • Java 并发 - 从ReentrantLock的实现看AQS的原理及应用
  • Java
    • Java 并发
      • Java 并发 - 公平锁和非公平锁
  • Java
    • Java 并发
      • Java 并发 - 内存模型
  • Java
    • Java 并发
      • Java 并发 - 原子类
  • Java
    • Java 并发
      • Java 并发 - 如何确保三个线程顺序执行?
  • Java
    • Java 并发
      • Java 并发 - 锁
  • Java
    • Java 的新特性
      • Java 10 新特性概述
  • Java
    • Java 的新特性
      • Java 11 新特性概述
  • Java
    • Java 的新特性
      • Java 12 新特性概述
  • Java
    • Java 的新特性
      • Java 13 新特性概述
  • Java
    • Java 的新特性
      • Java 14 新特性概述
  • Java
    • Java 的新特性
      • Java 15 新特性概述
  • Java
    • Java 的新特性
      • Java 8的新特性
  • Java
    • Java 的新特性
      • Java 9 新特性概述
  • Java
    • Java 调试排错
      • 调试排错 - Java Debug Interface(JDI)详解
  • Java
    • Java 调试排错
      • 调试排错 - CPU 100% 排查优化实践
  • Java
    • Java 调试排错
      • 调试排错 - Java Heap Dump分析
  • Java
    • Java 调试排错
      • 调试排错 - Java Thread Dump分析
  • Java
    • Java 调试排错
      • 调试排错 - Java动态调试技术原理
  • Java
    • Java 调试排错
      • 调试排错 - Java应用在线调试Arthas
  • Java
    • Java 调试排错
      • 调试排错 - Java问题排查:工具单
  • Java
    • Java 调试排错
      • 调试排错 - 内存溢出与内存泄漏
  • Java
    • Java 调试排错
      • 调试排错 - 在线分析GC日志的网站GCeasy
  • Java
    • Java 调试排错
      • 调试排错 - 常见的GC问题分析与解决
  • Java
    • Java 集合
      • Java 集合 - ArrayList
  • Java
    • Java 集合
      • Java 集合 - HashMap 和 ConcurrentHashMap
  • Java
    • Java 集合
      • Java 集合 - HashMap的死循环问题
  • Java
    • Java 集合
      • Java 集合 - LinkedHashSet&Map
  • Java
    • Java 集合
      • Java 集合 - LinkedList
  • Java
    • Java 集合
      • Java 集合 - PriorityQueue
  • Java
    • Java 集合
      • Java 集合 - Stack & Queue
  • Java
    • Java 集合
      • Java 集合 - TreeSet & TreeMap
  • Java
    • Java 集合
      • Java 集合 - WeakHashMap
  • Java
    • Java 集合
      • Java 集合 - 为什么HashMap的容量是2的幂次方
  • Java
    • Java 集合
      • Java 集合 - 概览
  • Java
    • Java 集合
      • Java 集合 - 高性能队列Disruptor详解
  • 分布式
    • RPC
      • ⭐️RPC - Dubbo&hsf&Spring cloud的区别
  • 分布式
    • RPC
      • ⭐️RPC - Dubbo的架构原理
  • 分布式
    • RPC
      • ⭐️RPC - HSF的原理分析
  • 分布式
    • RPC
      • ⭐️RPC - 你应该知道的RPC原理
  • 分布式
    • RPC
      • ⭐️RPC - 动态代理
  • 分布式
    • RPC
      • 深入理解 RPC 之协议篇
  • 分布式
    • RPC
      • RPC - 序列化和反序列化
  • 分布式
    • RPC
      • ⭐️RPC - 服务注册与发现
  • 分布式
    • RPC
      • RPC - 核心原理
  • 分布式
    • RPC
      • ⭐️RPC - 框架对比
  • 分布式
    • RPC
      • ⭐️RPC - 网络通信
  • 分布式
    • 分布式事务
      • 分布式事务 Seata TCC 模式深度解析
  • 分布式
    • 分布式事务
      • 分布式事务的实现原理
  • 分布式
    • 分布式事务
      • 常用的分布式事务解决方案
  • 分布式
    • 分布式事务
      • 手写实现基于消息队列的分布式事务框架
  • 分布式
    • 分布式算法
      • CAP 定理的含义
  • 分布式
    • 分布式算法
      • Paxos和Raft比较
  • 分布式
    • 分布式算法
      • 分布式一致性与共识算法
  • 分布式
    • 分布式锁
      • ⭐️分布式锁的原理及实现方式
  • 分布式
    • 搜索引擎
      • ElasticSearch与SpringBoot的集成与JPA方法的使用
  • 分布式
    • 搜索引擎
      • 全文搜索引擎 Elasticsearch 入门教程
  • 分布式
    • 搜索引擎
      • 十分钟学会使用 Elasticsearch 优雅搭建自己的搜索系统
  • 分布式
    • 搜索引擎
      • 腾讯万亿级 Elasticsearch 技术解密
  • 分布式
    • 日志系统
      • Grafana Loki 简明教程
  • 分布式
    • 日志系统
      • 分布式系统中如何优雅地追踪日志
  • 分布式
    • 日志系统
      • 如何优雅地记录操作日志?
  • 分布式
    • 日志系统
      • 日志收集组件—Flume、Logstash、Filebeat对比
  • 分布式
    • 日志系统
      • 集中式日志系统 ELK 协议栈详解
  • 分布式
    • 消息队列
      • 消息队列 - Kafka
  • 分布式
    • 消息队列
      • 消息队列 - Kafka、RabbitMQ、RocketMQ等消息中间件的对比
  • 分布式
    • 消息队列
      • 消息队列之 RabbitMQ
  • 分布式
    • 消息队列
      • 消息队列 - 使用docker-compose构建kafka集群
  • 分布式
    • 消息队列
      • 消息队列 - 分布式系统与消息的投递
  • 分布式
    • 消息队列
      • 消息队列 - 如何保证消息的可靠性传输
  • 分布式
    • 消息队列
      • 消息队列 - 如何保证消息的顺序性
  • 分布式
    • 消息队列
      • 消息队列 - 如何保证消息队列的高可用
  • 分布式
    • 消息队列
      • 消息队列 - 消息队列设计精要
  • 分布式
    • 监控系统
      • 深度剖析开源分布式监控CAT
  • 大数据
    • Flink
      • Flink架构与核心组件
  • 微服务
    • Dubbo
      • 基于dubbo的分布式应用中的统一异常处理
  • 微服务
    • Dubbo
      • Vim快捷键
  • 微服务
    • Service Mesh
      • Istio 是什么?
  • 微服务
    • Service Mesh
      • OCTO 2.0:美团基于Service Mesh的服务治理系统详解
  • 微服务
    • Service Mesh
      • Service Mesh是什么?
  • 微服务
    • Service Mesh
      • Spring Cloud向Service Mesh迁移
  • 微服务
    • Service Mesh
      • 数据挖掘算法
  • 微服务
    • Service Mesh
      • Seata Saga 模式
  • 微服务
    • Spring Cloud
      • Seata TCC 模式
  • 微服务
    • Spring Cloud
      • Spring Cloud Config
  • 微服务
    • Spring Cloud
      • Seata AT 模式
  • 微服务
    • Spring Cloud
      • Spring Cloud Gateway
  • 微服务
    • Spring Cloud
      • Spring Cloud OpenFeign 的核心原理
  • 微服务
    • Spring Cloud
      • Seata XA 模式
  • 数据库
    • Database Version Control
      • Liquibase vs. Flyway
  • 数据库
    • Database Version Control
      • Six reasons to version control your database
  • 数据库
    • MySQL
      • How Sharding Works
  • 数据库
    • MySQL
      • MySQL InnoDB中各种SQL语句加锁分析
  • 数据库
    • MySQL
      • MySQL 事务隔离级别和锁
  • 数据库
    • MySQL
      • MySQL 索引性能分析概要
  • 数据库
    • MySQL
      • MySQL 索引设计概要
  • 数据库
    • MySQL
      • MySQL出现Waiting for table metadata lock的原因以及解决方法
  • 数据库
    • MySQL
      • MySQL的Limit性能问题
  • 数据库
    • MySQL
      • MySQL索引优化explain
  • 数据库
    • MySQL
      • MySQL索引背后的数据结构及算法原理
  • 数据库
    • MySQL
      • MySQL行转列、列转行问题
  • 数据库
    • MySQL
      • 一条SQL更新语句是如何执行的?
  • 数据库
    • MySQL
      • 一条SQL查询语句是如何执行的?
  • 数据库
    • MySQL
      • 为什么 MySQL 使用 B+ 树
  • 数据库
    • MySQL
      • 为什么 MySQL 的自增主键不单调也不连续
  • 数据库
    • MySQL
      • 为什么我的MySQL会“抖”一下?
  • 数据库
    • MySQL
      • 为什么数据库不应该使用外键
  • 数据库
    • MySQL
      • 为什么数据库会丢失数据
  • 数据库
    • MySQL
      • 事务的可重复读的能力是怎么实现的?
  • 数据库
    • MySQL
      • 大众点评订单系统分库分表实践
  • 数据库
    • MySQL
      • 如何保证缓存与数据库双写时的数据一致性?
  • 数据库
    • MySQL
      • 浅谈数据库并发控制 - 锁和 MVCC
  • 数据库
    • MySQL
      • 深入浅出MySQL 中事务的实现
  • 数据库
    • MySQL
      • 浅入浅出MySQL 和 InnoDB
  • 数据库
    • PostgreSQL
      • PostgreSQL upsert功能(insert on conflict do)的用法
  • 数据库
    • Redis
      • Redis GEO & 实现原理深度分析
  • 数据库
    • Redis
      • Redis 和 I/O 多路复用
  • 数据库
    • Redis
      • Redis分布式锁
  • 数据库
    • Redis
      • Redis实现分布式锁中的“坑”
  • 数据库
    • Redis
      • Redis总结
  • 数据库
    • Redis
      • 史上最全Redis高可用技术解决方案大全
  • 数据库
    • Redis
      • Redlock:Redis分布式锁最牛逼的实现
  • 数据库
    • Redis
      • 为什么 Redis 选择单线程模型
  • 数据库
    • TiDB
      • 新一代数据库TiDB在美团的实践
  • 数据库
    • 数据仓库
      • 实时数仓在有赞的实践
  • 数据库
    • 数据库原理
      • OLTP与OLAP的关系是什么?
  • 数据库
    • 数据库原理
      • 为什么 OLAP 需要列式存储
  • 系统设计
    • DDD
      • Domain Primitive
  • 系统设计
    • DDD
      • Repository模式
  • 系统设计
    • DDD
      • 应用架构
  • 系统设计
    • DDD
      • 聊聊如何避免写流水账代码
  • 系统设计
    • DDD
      • 领域层设计规范
  • 系统设计
    • DDD
      • 从三明治到六边形
  • 系统设计
    • DDD
      • 阿里盒马领域驱动设计实践
  • 系统设计
    • DDD
      • 领域驱动设计(DDD)编码实践
  • 系统设计
    • DDD
      • 领域驱动设计在互联网业务开发中的实践
  • 系统设计
    • 基础架构
      • 容错,高可用和灾备
  • 系统设计
    • 数据聚合
      • GraphQL及元数据驱动架构在后端BFF中的实践
  • 系统设计
    • 数据聚合
      • 高效研发-闲鱼在数据聚合上的探索与实践
  • 系统设计
    • 服务安全
      • JSON Web Token 入门教程
  • 系统设计
    • 服务安全
      • 你还在用JWT做身份认证嘛?
  • 系统设计
    • 服务安全
      • 凭证(Credentials)
  • 系统设计
    • 服务安全
      • 授权(Authorization)
  • 系统设计
    • 服务安全
      • 理解OAuth2.0
  • 系统设计
    • 服务安全
      • 认证(Authentication)
  • 系统设计
    • 架构案例
      • 微信 Android 客户端架构演进之路
  • 系统设计
    • 高可用架构
      • 业务高可用的保障:异地多活架构
  • 计算机基础
    • 字符编码
      • Base64原理解析
  • 计算机基础
    • 字符编码
      • 字符编码笔记:ASCII,Unicode 和 UTF-8
  • 计算机基础
    • 操作系统
      • 为什么 CPU 访问硬盘很慢
  • 计算机基础
    • 操作系统
      • 为什么 HTTPS 需要 7 次握手以及 9 倍时延
  • 计算机基础
    • 操作系统
      • 为什么 Linux 默认页大小是 4KB
  • 计算机基础
    • 操作系统
      • 磁盘IO那些事
  • 计算机基础
    • 操作系统
      • 虚拟机的3种网络模式
  • 计算机基础
    • 服务器
      • mac终端bash、zsh、oh-my-zsh最实用教程
  • 计算机基础
    • 服务器
      • Nginx强制跳转Https
  • 计算机基础
    • 服务器
      • curl 的用法指南
  • 计算机基础
    • 网络安全
      • 如何设计一个安全的对外接口?
  • 计算机基础
    • 网络安全
      • 浅谈常见的七种加密算法及实现
  • 计算机基础
    • 网络编程
      • MQTT - The Standard for IoT Messaging
  • 计算机基础
    • 网络编程
      • 两万字长文 50+ 张趣图带你领悟网络编程的内功心法
  • 计算机基础
    • 网络编程
      • 为什么 TCP 协议有 TIME_WAIT 状态
  • 计算机基础
    • 网络编程
      • 为什么 TCP 协议有性能问题
  • 计算机基础
    • 网络编程
      • 为什么 TCP 协议有粘包问题
  • 计算机基础
    • 网络编程
      • 为什么 TCP 建立连接需要三次握手
  • 计算机基础
    • 网络编程
      • 为什么 TCP/IP 协议会拆分数据
  • 计算机基础
    • 网络编程
      • 使用 OAuth 2 和 JWT 为微服务提供安全保障
  • 计算机基础
    • 网络编程
      • 四种常见的 POST 提交数据方式
  • 计算机基础
    • 网络编程
      • 有赞TCP网络编程最佳实践
  • 计算机基础
    • 网络编程
      • 看完这篇HTTP,跟面试官扯皮就没问题了
  • 计算机基础
    • 网络编程
      • 详细解析 HTTP 与 HTTPS 的区别
  • 质量&效率
    • 快捷键
      • Idea快捷键(Mac版)
  • 质量&效率
    • 快捷键
      • Shell快捷键
  • 质量&效率
    • 快捷键
      • conduit
  • 质量&效率
    • 敏捷开发
      • Scrum的3种角色
  • 质量&效率
    • 敏捷开发
      • Scrum的4种会议
  • 质量&效率
    • 敏捷开发
      • ThoughtWorks的敏捷开发
  • 质量&效率
    • 敏捷开发
      • 敏捷开发入门教程
  • 运维&测试
    • Docker
      • Docker (容器) 的原理
  • 运维&测试
    • Docker
      • Docker Compose:链接外部容器的几种方式
  • 运维&测试
    • Docker
      • Docker 入门教程
  • 运维&测试
    • Docker
      • Docker 核心技术与实现原理
  • 运维&测试
    • Docker
      • Dockerfile 最佳实践
  • 运维&测试
    • Docker
      • Docker开启Remote API 访问 2375端口
  • 运维&测试
    • Docker
      • Watchtower - 自动更新 Docker 镜像与容器
  • 运维&测试
    • Kubernetes
      • Kubernetes 介绍
  • 运维&测试
    • Kubernetes
      • Kubernetes 在有赞的实践
  • 运维&测试
    • Kubernetes
      • Kubernetes 学习路径
  • 运维&测试
    • Kubernetes
      • Kubernetes如何改变美团的云基础设施?
  • 运维&测试
    • Kubernetes
      • Kubernetes的三种外部访问方式:NodePort、LoadBalancer 和 Ingress
  • 运维&测试
    • Kubernetes
      • 谈 Kubernetes 的架构设计与实现原理
  • 运维&测试
    • 压测
      • 全链路压测平台(Quake)在美团中的实践
  • 运维&测试
    • 测试
      • Cpress - JavaScript End to End Testing Framework
  • 运维&测试
    • 测试
      • 代码覆盖率-JaCoCo
  • 运维&测试
    • 测试
      • 浅谈代码覆盖率
  • 运维&测试
    • 测试
      • 测试中 Fakes、Mocks 以及 Stubs 概念明晰
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP中的Bean是如何被AOP代理的
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP原生动态代理和Cglib动态代理
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP实现方式(xml&注解)
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP是如何收集切面类并封装的
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP概述
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP的底层核心后置处理器
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP的延伸知识
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot - IOC(一)
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot - IOC(三)
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot - IOC(二)
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot - IOC(五)
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot - IOC(四) - 循环依赖与解决方案
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot - 启动引导
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot JarLauncher
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot Web Mvc 自动装配
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot 使用ApplicationListener监听器
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot 声明式事务
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot 嵌入式容器
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot引起的“堆外内存泄漏”排查及经验总结
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot的启动流程
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot自动化配置源码分析
  • Java FrameWorks
    • Spring
      • Spring Boot
        • 如何自定义Spring Boot Starter?
  • Java FrameWorks
    • Spring
      • Spring IOC
        • IOC - 模块装配和条件装配
  • Java FrameWorks
    • Spring
      • Spring IOC
        • IOC - 配置源(xml,注解)
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Environment
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring ApplicationContext
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring BeanDefinition
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring BeanFactory
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring BeanFactoryPostProcessor
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring BeanPostProcessor
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Bean的生命周期(一) - 概述
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Bean的生命周期(三) - 实例化阶段
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Bean的生命周期(二) - BeanDefinition
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Bean的生命周期(五) - 销毁阶段
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Bean的生命周期(四) - 初始化阶段
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring ComponentScan
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Events
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring IOC 基础篇
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring IOC 总结
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring IOC 进阶篇
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring IOC容器的生命周期
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Resource
  • Java FrameWorks
    • Spring
      • Spring MVC
        • DispatcherServlet的初始化原理
  • Java FrameWorks
    • Spring
      • Spring MVC
        • DispatcherServlet的核心工作原理
  • Java FrameWorks
    • Spring
      • Spring MVC
        • WebMvc的架构设计与组件功能解析
  • Java FrameWorks
    • Spring
      • Spring Security
        • Spring Boot 2 + Spring Security 5 + JWT 的单页应用 Restful 解决方案
  • Java FrameWorks
    • Spring
      • Spring Security
        • Spring Security Oauth
  • Java FrameWorks
    • Spring
      • Spring Security
        • Spring Security
  • Java FrameWorks
    • Spring
      • Spring WebFlux
        • DispatcherHandler的工作原理(传统方式)
  • Java FrameWorks
    • Spring
      • Spring WebFlux
        • DispatcherHandler的工作原理(函数式端点)
  • Java FrameWorks
    • Spring
      • Spring WebFlux
        • WebFlux的自动装配
  • Java FrameWorks
    • Spring
      • Spring WebFlux
        • 快速了解响应式编程与Reactive
  • Java FrameWorks
    • Spring
      • Spring WebFlux
        • 快速使用WebFlux
  • 分布式
    • 协调服务
      • Zookeeper
        • Zookeeper - 客户端之 Curator
  • 分布式
    • 协调服务
      • Zookeeper
        • 详解分布式协调服务 ZooKeeper
  • 分布式
    • 协调服务
      • etcd
        • 高可用分布式存储 etcd 的实现原理
  • 数据库
    • Database Version Control
      • Flyway
        • Database Migrations with Flyway
  • 数据库
    • Database Version Control
      • Flyway
        • How Flyway works
  • 数据库
    • Database Version Control
      • Flyway
        • Rolling Back Migrations with Flyway
  • 数据库
    • Database Version Control
      • Flyway
        • The meaning of the concept of checksums
  • 数据库
    • Database Version Control
      • Liquibase
        • Introduction to Liquibase Rollback
  • 数据库
    • Database Version Control
      • Liquibase
        • LiquiBase中文学习指南
  • 数据库
    • Database Version Control
      • Liquibase
        • Use Liquibase to Safely Evolve Your Database Schema
  • 系统设计
    • 流量控制
      • RateLimiter
        • Guava Rate Limiter实现分析
  • 系统设计
    • 流量控制
      • Sentinel
        • Sentinel 与 Hystrix 的对比
  • 系统设计
    • 流量控制
      • Sentinel
        • Sentinel工作主流程
  • 系统设计
    • 流量控制
      • 算法
        • 分布式服务限流实战
  • 系统设计
    • 解决方案
      • 秒杀系统
        • 如何设计一个秒杀系统
  • 系统设计
    • 解决方案
      • 红包系统
        • 微信高并发资金交易系统设计方案--百亿红包背后的技术支撑
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • 什么是预排序遍历树算法(MPTT,Modified Preorder Tree Traversal)
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • 加密算法
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • 推荐系统算法
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • linkerd
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • 查找算法
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • 缓存淘汰算法中的LRU和LFU
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • 负载均衡算法
  • 计算机基础
    • 数据结构与算法
      • 分布式算法
        • 分布式算法 - Paxos算法
  • 计算机基础
    • 数据结构与算法
      • 分布式算法
        • 分布式算法 - Raft算法
  • 计算机基础
    • 数据结构与算法
      • 分布式算法
        • 分布式算法 - Snowflake算法
  • 计算机基础
    • 数据结构与算法
      • 分布式算法
        • 分布式算法 - ZAB算法
  • 计算机基础
    • 数据结构与算法
      • 分布式算法
        • 分布式算法 - 一致性Hash算法
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - Bitmap & Bloom Filter
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - Map & Reduce
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - Trie树/数据库/倒排索引
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - 分治/hash/排序
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - 双层桶划分
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - 外(磁盘文件)排序
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - 布隆过滤器
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理算法
  • 计算机基础
    • 数据结构与算法
      • 字符串匹配算法
        • 字符串匹配 - 文本预处理:后缀树(Suffix Tree)
  • 计算机基础
    • 数据结构与算法
      • 字符串匹配算法
        • 字符串匹配 - 模式预处理:BM 算法 (Boyer-Moore)
  • 计算机基础
    • 数据结构与算法
      • 字符串匹配算法
        • 字符串匹配 - 模式预处理:KMP 算法(Knuth-Morris-Pratt)
  • 计算机基础
    • 数据结构与算法
      • 字符串匹配算法
        • 字符串匹配 - 模式预处理:朴素算法(Naive)(暴力破解)
  • 计算机基础
    • 数据结构与算法
      • 字符串匹配算法
        • 字符串匹配
  • 计算机基础
    • 数据结构与算法
      • 常用算法
        • 分支限界算法
  • 计算机基础
    • 数据结构与算法
      • 常用算法
        • 分治算法
  • 计算机基础
    • 数据结构与算法
      • 常用算法
        • 动态规划算法
  • 计算机基础
    • 数据结构与算法
      • 常用算法
        • 回溯算法
  • 计算机基础
    • 数据结构与算法
      • 常用算法
        • 贪心算法
  • 计算机基础
    • 数据结构与算法
      • 排序算法
        • 十大排序算法
  • 计算机基础
    • 数据结构与算法
      • 排序算法
        • 图解排序算法(一)之3种简单排序(选择,冒泡,直接插入)
  • 计算机基础
    • 数据结构与算法
      • 排序算法
        • 图解排序算法(三)之堆排序
  • 计算机基础
    • 数据结构与算法
      • 排序算法
        • 图解排序算法(二)之希尔排序
  • 计算机基础
    • 数据结构与算法
      • 排序算法
        • 图解排序算法(四)之归并排序
  • 计算机基础
    • 数据结构与算法
      • 数据结构
        • 树的高度和深度
  • 计算机基础
    • 数据结构与算法
      • 数据结构
        • 红黑树深入剖析及Java实现
  • 计算机基础
    • 数据结构与算法
      • 数据结构
        • 线性结构 - Hash
  • 计算机基础
    • 数据结构与算法
      • 数据结构
        • 线性结构 - 数组、链表、栈、队列
  • 计算机基础
    • 数据结构与算法
      • 数据结构
        • 逻辑结构 - 树
  • 运维&测试
    • 测试
      • Spock
        • Groovy 简明教程
  • 运维&测试
    • 测试
      • Spock
        • Spock 官方文档
  • 运维&测试
    • 测试
      • Spock
        • Spock单元测试框架介绍以及在美团优选的实践
  • 运维&测试
    • 测试
      • TDD
        • TDD 实践 - FizzFuzzWhizz(一)
  • 运维&测试
    • 测试
      • TDD
        • TDD 实践 - FizzFuzzWhizz(三)
  • 运维&测试
    • 测试
      • TDD
        • TDD 实践 - FizzFuzzWhizz(二)
  • 运维&测试
    • 测试
      • TDD
        • 测试驱动开发(TDD)- 原理篇
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Nacos
          • Nacos 服务注册的原理
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Nacos
          • Nacos 配置中心原理分析
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Seata
          • 服务调用过程
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Seata
          • Spring Cloud Bus
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Seata
          • Spring Cloud Consul
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Seata
          • Spring Cloud Stream
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Sentinel
          • Sentinel 与 Hystrix 的对比
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Sentinel
          • Sentinel
  • 微服务
    • Spring Cloud
      • Spring Cloud Netflix
        • Hystrix
          • How Hystrix Works
  • 微服务
    • Spring Cloud
      • Spring Cloud Netflix
        • Hystrix
          • Hystrix
  • 微服务
    • Spring Cloud
      • Spring Cloud Netflix
        • Hystrix
          • Hystrix原理与实战
  • 微服务
    • Spring Cloud
      • Spring Cloud Netflix
        • Hystrix
          • Spring Cloud Hystrix基本原理
由 GitBook 提供支持
在本页
  • 1. AOP联盟
  • 1.1 AOP联盟制定的通知类型
  • 1.2 SpringFramework中对应的通知接口
  • 2. 切面类的通知方法参数
  • 2.1 JoinPoint的使用
  • 2.2 ProceedingJoinPoint的扩展
  • 2.3 返回通知和异常通知的特殊参数
  • 3. 多个切面的执行顺序
  • 3.1 代码准备
  • 3.2 预设的顺序
  • 3.3 显式声明执行顺序
  • 4. 同切面的多个通知执行顺序
  • 5. 代理对象调用自身的方法
  • 5.1 代码准备
  • 5.2 不优雅的解决方案
  • 5.3 正确的解决方案:AopContext
  • 6. AOP的引介
  • 6.1 引介的作用和目标
  • 6.2 SpringFramework中的引介
  • 6.3 AspectJ的引介
  • 7. LoadTimeWeawer
  • 7.1 AOP增强的时机
  • 7.2 AspectJ对于增强的时机
  • 7.3 AspectJ的LoadTimeWeaving

这有帮助吗?

  1. Java FrameWorks
  2. Spring
  3. Spring AOP

Spring AOP的延伸知识

1. AOP联盟

在 SpringFramework 2.0 之前,它还没有整合 AspectJ ,当时的 SpringFramework 还有一套相对低层级的实现,它也是 SpringFramework 原生的实现,而我们要了解它,首先要先了解一个组织:AOP 联盟。

早在很久之前,AOP 的概念就被提出来了。同之前的 EJB 一样,作为一个概念、思想,它要有一批人来制定规范,于是就有了这样一个 AOP 联盟。这个联盟的人将 AOP 的这些概念都整理好,形成了一个规范 AOP 框架底层实现的 API ,并最终总结出了 5 种 AOP 通知类型。

咱要了解的,就是 AOP 联盟提出的这 5 种通知类型。

1.1 AOP联盟制定的通知类型

5 种通知类型分别为:

  • 前置通知

  • 后置通知(返回通知)

  • 异常通知

  • 环绕通知

  • 引介通知

注意它跟 AspectJ 规定的 5 种通知类型的区别:它多了一个引介通知,少了一个后置通知。而且还有一个要注意的,AOP 联盟定义的后置通知实际上是返回通知( after-returning ),而 AspectJ 的后置通知是真的后置通知,与返回通知是两码事。

1.2 SpringFramework中对应的通知接口

AOP 联盟定义的 5 种通知类型在 SpringFramework 中都有对应的接口定义:

  • 前置通知:org.springframework.aop.MethodBeforeAdvice

  • 返回通知:org.springframework.aop.AfterReturningAdvice

  • 异常通知:org.springframework.aop.ThrowsAdvice

  • 环绕通知:org.aopalliance.intercept.MethodInterceptor

  • 引介通知:org.springframework.aop.IntroductionAdvisor

注意!环绕通知的接口是 AOP 联盟原生定义的接口(不是 cglib 的那个 MethodInterceptor )!小伙伴们可以先思考一下为什么会是这样。

其实答案不难理解,由于 SpringFramework 是基于 AOP 联盟制定的规范来的,所以自然会去兼容原有的方案。又由于咱之前写过原生的动态代理,知道它其实就是环绕通知,所以 SpringFramework 要在环绕通知上拆解结构,自然也会保留原本环绕通知的接口支持。

了解这部分的知识,在后面咱分析 Spring AOP 的原理时,看到一些特殊的 API 接口时,就不会觉得奇怪或者陌生了,现在小伙伴们只是有个基本的印象即可。

2. 切面类的通知方法参数

在上一章的环绕通知编写中,咱提到了一个特殊的接口 ProceedingJoinPoint ,它的具体使用,以及切面类中的通知方法参数等等,咱都有必要来学习一下。

其实在之前的代码中,或许有的小伙伴就已经产生很强的不适感了:这所有的日志打印都是一样的,我也不知道哪个日志打印是哪个方法触发的,这咋区分呢? 所以,我们得想个办法,把被增强的方法,以及对应的目标对象的信息拿到才行。(原生动态代理都行,到 AOP 就不行了?这肯定不合理)

2.1 JoinPoint的使用

其实切面类的通知方法,咱都可以在方法的参数列表上加上切入点的引用,就像这样:(咱以 beforePrint 方法为例)

@Before("execution(public * com..FinanceService.*(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println("Logger beforePrint run ......");
}

这样写之后,重新运行程序不会有任何错误,说明这样写是被允许的,但咱更关心的是,能从这个 JoinPoint 中得到什么呢?

直接在方法中调用 joinPoint 的方法,可以发现它能获取到这么多东西:

这么多内容,咱选几个比较重要的来讲解。

2.1.1 getTarget & getThis

getTarget 方法是最容易被理解的,咱可以简单的测试一下效果:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println(joinPoint.getTarget());
    System.out.println("Logger beforePrint run ......");
}

运行 main 方法,控制台会打印 FinanceService 的信息:

com.linkedbear.spring.aop.c_joinpoint.service.FinanceService@4c309d4d
Logger beforePrint run ......
FinanceService 收钱 === 123.45

包括使用 Debug 打断点,识别到的 FinanceService 也是未经过代理的原始对象:

那相对的,getThis 方法返回的就是代理对象咯?咱也可以来打印一下:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println(joinPoint.getTarget());
    System.out.println(joinPoint.getThis());
    System.out.println("Logger beforePrint run ......");
}

重新运行 main 方法,控制台打印了两个一模一样的 FinanceService :

com.linkedbear.spring.aop.c_joinpoint.service.FinanceService@4c309d4d
com.linkedbear.spring.aop.c_joinpoint.service.FinanceService@4c309d4d
Logger beforePrint run ......
FinanceService 收钱 === 123.45

??????怎么个情况?难道是我们推理错了吗?用 Debug 看一眼:

诶呦吓一跳,getThis 肯定还是获取到代理对象才是啦。那为什么原始的目标对象,与代理对象的控制台打印结果是一样的呢?

其实从上面的截图中也能猜到端倪:它增强了 equals 方法,增强了 hashcode 方法,就是没有增强 toString 方法,那当然就执行目标对象的方法啦,自然也就打印原来的目标对象的全限定名了。

2.1.2 getArgs

这个方法也是超级好理解,它可以获取到被拦截的方法的参数列表。快速的来测试一下吧:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println(Arrays.toString(joinPoint.getArgs()));
    System.out.println("Logger beforePrint run ......");
}

重新运行 main 方法,控制台打印出了 addMoney 方法传入的 123.45 :

[123.45]
Logger beforePrint run ......
FinanceService 收钱 === 123.45

2.1.3 getSignature

这个方法,从名上看是获取签名,关键是这个签名是个啥?不知道,猜不出来,干脆先打印一把吧:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println(joinPoint.getSignature());
    System.out.println("Logger beforePrint run ......");
}

重新运行 main 方法,控制台打印的是被拦截的方法的全限定名等信息:

void com.linkedbear.spring.aop.c_joinpoint.service.FinanceService.addMoney(double)
Logger beforePrint run ......
FinanceService 收钱 === 123.45

哦,突然明白了,合着它打印的是这个被拦截的方法的签名啊!那是不是还可以顺便拿到方法的信息呢?

直接调用 getSignature() 的方法,发现这里面只有类的信息,没有方法的信息:

诶?那可奇了怪了,既然基于 AspectJ 的 AOP 是对方法的拦截,那理所应当的应该能拿到方法的信息才对呀!那当然,肯定能拿到,只是缺少了一点点步骤而已。

既然是基于方法的拦截,那获取到的 Signature 就应该可以强转为一种类似于 Method 的 Signature ,刚好还真就有这么一个 MethodSignature 的接口!

所以,咱就可以这样写了:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    
    System.out.println("Logger beforePrint run ......");
}

那既然是这样,MethodSignature 中一定能拿到方法的信息了!果不其然,这个接口还真就定义了获取 Method 的方法:

public interface MethodSignature extends CodeSignature {
    Class getReturnType();
	Method getMethod();
}

so ,我们就可以打印出这个方法的信息了:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    Method method = signature.getMethod();
    System.out.println(method.getName());
    System.out.println("Logger beforePrint run ......");
}

重新运行 main 方法,控制台可以打印出方法的信息:

addMoney
Logger beforePrint run ......
FinanceService 收钱 === 123.45

其实 Signature 的 getName 方法,就相当于拿到 Method 后再调 getName 方法了,小伙伴们可以自行测试一下。

至此,其实我们就可以完成前面说的需求了。

2.1.4 需求的改造

重新修改 addMoney 方法的逻辑,就可以很简单轻松的完成一开始说的 “不知道哪个日志打印是哪个方法触发” 的需求了:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println("Logger beforePrint run ......");
    System.out.println("被拦截的类:" + joinPoint.getTarget().getClass().getName());
    System.out.println("被拦截的方法:" + ((MethodSignature) joinPoint.getSignature()).getMethod().getName());
    System.out.println("被拦截的方法参数:" + Arrays.toString(joinPoint.getArgs()));
}

重新运行 main 方法,控制台打印出了我们预期的需求效果:

Logger beforePrint run ......
被拦截的类:com.linkedbear.spring.aop.c_joinpoint.service.FinanceService
被拦截的方法:addMoney
被拦截的方法参数:[123.45]
FinanceService 收钱 === 123.45

2.2 ProceedingJoinPoint的扩展

上一章中我们提前使用了 ProceedingJoinPoint 这个家伙,而它是基于 JoinPoint 的扩展,它扩展的方法只有 proceed 方法,也就是那个能让我们在环绕通知中显式执行目标对象的目标方法的那个 API 。

不过有一点要注意:proceed 方法还有一个带参数的重载方法:

public Object proceed(Object[] args) throws Throwable;

由此可以说明一点:在环绕通知中,可以自行替换掉原始目标方法执行时传入的参数列表!

其实这个一点也不奇怪,想想在之前的动态代理案例中,咱不就是可以随便改参数的嘛。

2.3 返回通知和异常通知的特殊参数

之前我们在写返回通知和异常通知时,还有一个小问题没有解决:返回通知中我们要拿到方法的返回值,异常通知中我们要拿到具体的异常抛出。这个呢,其实非常容易解决。

咱先把之前的代码再拿出来:

@AfterReturning("execution(* com..FinanceService.subtractMoney(double))")
public void afterReturningPrint() {
    System.out.println("Logger afterReturningPrint run ......");
}

@AfterThrowing("defaultPointcut()")
public void afterThrowingPrint() {
    System.out.println("Logger afterThrowingPrint run ......");
}

想拿到返回值或者异常非常简单,两个步骤。

首先在方法的参数列表中声明一个 result 或者 e :

@AfterReturning("execution(* com..FinanceService.subtractMoney(double))")
public void afterReturningPrint(Object retval) {
    System.out.println("Logger afterReturningPrint run ......");
    System.out.println("返回的数据:" + retval);
}

@AfterThrowing("defaultPointcut()")
public void afterThrowingPrint(Exception e) {
    System.out.println("Logger afterThrowingPrint run ......");
}

注意只是这样写了之后,此时运行 main 方法是不好使的,是拿不到返回值的!我们还需要告诉 SpringFramework ,我拿了一个名叫 retval 的参数来接这个方法返回的异常,拿一个名叫 e 的参数来接收方法抛出的异常,反映到代码上就应该是这样:

@AfterReturning(value = "execution(* com..FinanceService.subtractMoney(double))", returning = "retval")
public void afterReturningPrint(Object retval) {
    System.out.println("Logger afterReturningPrint run ......");
    System.out.println("返回的数据:" + retval);
}

@AfterThrowing(value = "defaultPointcut()", throwing = "e")
public void afterThrowingPrint(Exception e) {
    System.out.println("Logger afterThrowingPrint run ......");
    System.out.println("抛出的异常:" + e.getMessage());
}

这样再运行 main 方法,控制台才会打印出方法的返回值:

FinanceService 付钱 === 543.21
Logger afterReturningPrint run ......
返回的数据:543.21

异常的信息接收同理,小伙伴们可以自行测试。

3. 多个切面的执行顺序

日常开发中,或许我们会碰到一些特殊的情况:一个方法被多个切面同时增强了,这个时候如何控制好各个切面的执行顺序,以保证最终的运行结果能符合最初设计,这个也是非常重要的,咱有必要来研究一下多个切面的执行顺序问题。

3.1 代码准备

咱先把测试的代码准备一下,很简单,咱只声明两个切面和一个 Service 即可:

@Service
public class UserService {
    
    public void saveUser(String id) {
        System.out.println("UserService 保存用户" + id);
    }
}
@Component
@Aspect
public class LogAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void printLog() {
        System.out.println("LogAspect 打印日志 ......");
    }
}
@Component
@Aspect
public class TransactionAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void beginTransaction() {
        System.out.println("TransactionAspect 开启事务 ......");
    }
}

然后,编写配置类,开启注解 AOP :

@Configuration
@ComponentScan("com.linkedbear.spring.aop.d_order")
@EnableAspectJAutoProxy
public class AspectOrderConfiguration {
    
}

最后,编写启动类,用上面的配置类驱动 IOC 容器:

public class AspectOrderApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AspectOrderConfiguration.class);
        UserService userService = ctx.getBean(UserService.class);
        userService.saveUser("abc");
    }
}

运行 main 方法,控制台可以打印出两个切面的前置通知:

LogAspect 打印日志 ......
TransactionAspect 开启事务 ......
UserService 保存用户abc

3.2 预设的顺序

观察这个打印的结果,它是打印日志在前,开启事务在后,这难不成是因为我先写的 LogAspect ,后写的 TransactionAspect ,它就按照我的顺序来了?那不可能啊,即便我后写 TransactionAspect ,也是日志打印在前啊!所以它一定有一个默认的预设规则。

小伙伴可以先猜测一下预设的规则是什么,小册在这里再写一个切面,想必写完这个切面后,基本上顺序就非常容易推理了:

@Component
@Aspect
public class AbcAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void abc() {
        System.out.println("abc abc abc");
    }
}

重新运行 main 方法,发现 AbcAspect 的前置通知打印在 LogAspect 之前!

由此是不是咱就可以推测出预设的顺序了:默认的切面执行顺序,是按照字母表的顺序来的!

严谨一点,排序规则其实是根据切面类的 unicode 编码,按照十六进制排序得来的,unicode 编码靠前的,那自然就会排在前面。(作者个人习惯称其为字典表顺序)

3.3 显式声明执行顺序

那我们不能因为这个毛病,就搞得调整顺序就必须改类名吧,一定有更好的方案才对。

还记得之前在第 26 章,小册讲解 BeanPostProcessor 的 javadoc 中提到的 Ordered 接口吗?而且在 IOC 原理的 BeanPostProcessor 的初始化部分,也提到过有关排序的接口,也涉及到了这个 Ordered 接口。不过前面我们一直都没有实际演示 Ordered 接口的使用,这里咱就来搞一下。

现在咱希望让事务控制的切面提早执行,让它在所有切面之前,那么我们就可以这样写:给 TransactionAspect 实现 Ordered 接口,并声明 getOrder 的返回值:

@Component
@Aspect
public class TransactionAspect implements Ordered {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void beginTransaction() {
        System.out.println("TransactionAspect 开启事务 ......");
    }
    
    @Override
    public int getOrder() {
        return 0;
    }
}

这个值设置成多少呢?咱先放个 0 试试,运行 main 方法,观察控制台的打印:

TransactionAspect 开启事务 ......
abc abc abc
LogAspect 打印日志 ......
UserService 保存用户abc

咦,发现事务切面的前置通知已经提前执行了,说明 0 这个顺序已经是提早了的,那最晚的时机对应的 order 值是什么呢?

3.3.1 默认的排序值

很简单,Integer 的最大值 2147483647 嘛!其实在 Ordered 接口中,就有这两个常量的定义:

public interface Ordered {
	int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
	int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

那我们把这个值调到最低试一下?

@Override
public int getOrder() {
    return Ordered.LOWEST_PRECEDENCE;
}

重新运行 main 方法,发现事务的切面又回去了:

abc abc abc
LogAspect 打印日志 ......
TransactionAspect 开启事务 ......
UserService 保存用户abc

那到底默认值是啥呢?咱把 order 值往上调一个点试试?

@Override
public int getOrder() {
    return Ordered.LOWEST_PRECEDENCE - 1;
}

再次重新运行 main 方法,事务的切面打印又上去了。。。

所以得出结论:在不显式声明 order 排序值时,默认的排序值是 **Integer.MAX_VALUE** 。

3.3.2 另一种声明办法

除了使用 Ordered 接口,还有通过注解的方式声明:**@Order** 。

这次我们在 LogAspect 上标注 @Order 注解,并声明一下排序值:

@Component
@Aspect
@Order(0)
public class LogAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void printLog() {
        System.out.println("LogAspect 打印日志 ......");
    }
}

重新运行 main 方法,发现日志切面的打印提到最早了:

LogAspect 打印日志 ......
TransactionAspect 开启事务 ......
abc abc abc
UserService 保存用户abc

说明 @Order 注解也可以实现同样的效果。

4. 同切面的多个通知执行顺序

除了多个切面的顺序问题,如果同一个切面定义了多个相同类型的通知,它的执行顺序又是怎么样呢?咱也来研究一下。

这次编码的内容就少多了,直接在 AbsAspect 中添加一个方法 def 即可:

@Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
public void def() {
    System.out.println("def def def");
}

直接重新运行 main 方法,控制台先后打印了 abc 和 def 的内容:

LogAspect 打印日志 ......
TransactionAspect 开启事务 ......
abc abc abc
def def def
UserService 保存用户abc

原因是什么呢?估计小伙伴们也能猜到了,跟上面的逻辑一样,都是根据unicode编码顺序(字典表顺序)来的。

至于怎么搞,小伙伴们立马想到办法了吧!直接在方法上标注 **@Order** 注解就 OK (方法没办法实现接口的嘛)!

@Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
@Order(Ordered.HIGHEST_PRECEDENCE)
public void def() {
    System.out.println("def def def");
}

运行 main 方法,发现 def 的通知内容并没有被提前执行。。。看来这个办法行不通。。。

那怎么办呢?哎,这还真没办法。。。只能靠方法名去区分了。。。(是不是很无奈)

好了,有关切面、通知的执行顺序的研究,咱就到这里了。

5. 代理对象调用自身的方法

有一些特殊的场景下,我们产生的这些代理对象,会出现自身调用自身的另外方法的。下面我们也来演示一下这个现象。

5.1 代码准备

测试代码还是三个类,不再重复了:

@Service
public class UserService {
    
    public void update(String id, String name) {
        this.get(id);
        System.out.println("修改指定id的name。。。");
    }
    
    public void get(String id) {
        System.out.println("获取指定id的user。。。");
    }
}
@Component
@Aspect
public class LogAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.e_aopcontext.service.UserService.*(..))")
    public void beforePrint() {
        System.out.println("LogAspect 前置通知 ......");
    }
}
@Configuration
@ComponentScan("com.linkedbear.spring.aop.e_aopcontext")
@EnableAspectJAutoProxy
public class AopContextConfiguration {
    
}

然后,依然是编写测试启动类:

public class AopContextApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AopContextConfiguration.class);
        UserService userService = ctx.getBean(UserService.class);
        userService.update("abc", "def");
    }
}

这样写完之后,运行 main 方法,发现控制台只打印了一次 LogAspect 的切面打印:

LogAspect 前置通知 ......
获取指定id的user。。。
修改指定id的name。。。

如果需求是每次调用 UserService 的方法都需要打印切面日志,应该怎么处理呢?

5.2 不优雅的解决方案

可能有的小伙伴想到了一个能行但不太优雅的解决方案,就是利用依赖注入的特性,把自己注入进来,之后不用 this.get ,换用 userService.get 方法:

@Service
public class UserService {
    
    @Autowired
    UserService userService;
    
    public void update(String id, String name) {
        // this.get(id);
        userService.get(id);
        System.out.println("修改指定id的name。。。");
    }

重新运行 main 方法,控制台确实打印了两次切面日志:

LogAspect 前置通知 ......
LogAspect 前置通知 ......
获取指定id的user。。。
修改指定id的name。。。

但是吧。。。这样写真的好吗。。。有木有感觉怪怪的。。。难不成SpringFramework 就没有考虑到这个问题吗?

5.3 正确的解决方案:AopContext

当然还是得有的,SpringFramework 从一开始就考虑到这个问题了,于是它提供了一个 AopContext 的类,使用这个类,可以在代理对象中取到自身,它的使用方法很简单:

public void update(String id, String name) {
    ((UserService) AopContext.currentProxy()).get(id);
    System.out.println("修改指定id的name。。。");
}

使用 AopContext.currentProxy() 方法就可以取到代理对象的 this 了。

不过这样直接写完之后,运行是不好使的,它会抛出一个异常:

Exception in thread "main" java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.

这个异常的大致含义是,没有开启一个 exposeProxy 的属性,导致无法暴露出代理对象,从而无法获取。那开启 exposeProxy 这个属性的位置在哪里呢?好巧不巧,它是在我们一开始学习注解 AOP 的那个 @EnableAspectJAutoProxy 上:

@Configuration
@ComponentScan("com.linkedbear.spring.aop.e_aopcontext")
@EnableAspectJAutoProxy(exposeProxy = true) // 暴露代理对象
public class AopContextConfiguration {
    
}

它的默认值是 false ,改为 true 之后,再运行 main 方法,就可以达到同样的效果了,控制台会打印两次切面日志。

6. AOP的引介

前面我们在学习 AOP 的术语时,就说过了,引介只是了解即可,现在用的实在是太少了。不过还是会有对此感兴趣的小伙伴,咱这里也讲解一下。

考虑到整体的难度和使用情况来看,小册只讲解基于 AspectJ 的引介,对于 SpringFramework 原生的引介小册不作讲解,感兴趣的小伙伴可以加群与我交流相关内容哈。

6.1 引介的作用和目标

AOP 的术语中咱提到,引介的作用是给目标对象所在的类,动态的添加属性和方法,这种增强的类型区别于方法级别的通知,它不会影响已有的方法,而是直接给类添加新的方法和属性。

不过话又说回来,如果手头的项目,源码都好好的,谁会闲的没事用这种东西呢?而且即便是因为工程依赖的 jar 包中的代码没办法修改,我们也能把那个类拷出来,自己再任意改造呀,所以这个引介通知的使用就越来越少了。

但是(话锋又一转)!这样直接改造框架的源码,回头每个项目都要这么搞,本身就很麻烦;如果每个项目对于既定源码的扩展内容都不一样,那可就没法搞了。所以,引介通知还是能起到作用的。

注意,引介作为一种特殊的 AOP 通知,它的作用对象是目标对象所属类而不是目标对象本身,这也就意味着引介的织入是对类织入,而不是对方法的逻辑织入。

6.2 SpringFramework中的引介

SpringFramework 中原生的引介通知,是通过 IntroductionInterceptor 来创建的,它本身扩展了 MethodInterceptor ,以及 DynamicIntroductionAdvice 接口,我们开发者可以通过实现 IntroductionInterceptor 的子接口 DelegatintIntroductionInterceptor ,来实现引介通知的编写。

不过由于这种使用方式太过于复杂,小册不作讲解。

6.3 AspectJ的引介

AspectJ 中的引介,有一个专门的注解 **@DeclareParents** 来很方便的实现目标对象所属类的属性和方法增强。它可以指定被增强的类要扩展什么接口,以及扩展的实现类。这种声明引介通知的方式相对比较简单,下面咱来学习这种编写方式。

6.3.1 代码准备

这次的代码准备也不难,三个类就可以,分别是 Service 、切面类、配置类:

@Service
public class FinanceService {
    
    public void transfer(Long source, Long target, int money) {
        System.out.println("转账完成!");
        System.out.println(source + " 为 " + target + " 转钱" + money + "元!");
    }
}
@Component
@Aspect
public class IntroductionAspect {
    
    @Before("execution(* com..f_introduction.service.FinanceService.transfer(Long, Long, int))")
    public void beforePrintLog() {
        System.out.println("转账动作前置打印 。。。");
    }
}
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.linkedbear.spring.aop.f_introduction")
public class IntroductionConfiguration {
    
}

最后,编写测试启动类,驱动 IOC 容器后获取 FinanceService 并调用:

public class IntroductionApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(IntroductionConfiguration.class);
        FinanceService financeService = ctx.getBean(FinanceService.class);
        financeService.transfer(1L, 2L, 100);
    }
}

运行 main 方法,控制台可以正常打印 IntroductionAspect 中的前置通知,这样代码就准备好了。

6.3.2 需求说明

下面咱说一下要完成的需求哈。转账的动作中,金额一定不能是负的,而目前的代码中并没有这方面的校验逻辑。

相对简单的办法是,在前置通知中编写参数校验的逻辑即可,这个很好写,咱就不多说了。这里咱学习的是如何用引介的方式解决这个问题。

6.3.3 编写校验服务

首先,要用引介通知增强,首先需要一个新的接口 + 实现类,这里咱可以声明一个 MoneyValidator :

public interface MoneyValidator {
    boolean validate(int money);
}

紧接着,编写一个 MoneyValidator 的实现类:

@Component
public class MoneyValidatorImpl implements MoneyValidator {
    
    @Override
    public boolean validate(int money) {
        return money > 0;
    }
}

这样就有了一个金额的校验器。

6.3.4 @DeclareParents的使用

接下来就是给 FinanceService 织入引介通知了。首先咱要回到切面类中,在这里面添加一个 MoneyValidator 的成员,并标注 @DeclareParents 注解:

@Component
@Aspect
public class IntroductionAspect {
    
    @DeclareParents(value = "", defaultImpl = )
    private MoneyValidator moneyValidator;

这个 @DeclareParents 注解有两个参数,value 是即将增强到原有目标类的全限定名,defaultImpl 是引介接口的默认实现类。所以我们可以在这里面这样声明:

@Component
@Aspect
public class IntroductionAspect {
    
    @DeclareParents(value = "com.linkedbear.spring.aop.f_introduction.service.FinanceService", 
                    defaultImpl = MoneyValidatorImpl.class)
    private MoneyValidator moneyValidator;

但是话又说回来,现在的 FinanceService 是个类,那可以,如果这是个接口呢(FinanceServiceImpl implements FinanceService)?这次该怎么写呢?

AspectJ 当然也考虑到了这一点,只需要在整个接口的全限定名后面带一个 + 就可以了:

@DeclareParents(value = "com.linkedbear.spring.aop.f_introduction.service.FinanceService+", // ←看这里
                defaultImpl = MoneyValidatorImpl.class)
private MoneyValidator moneyValidator;

这样就代表,对于这个 **FinanceService** 接口下面的所有实现类,全部织入引介通知。

6.3.5 编写校验逻辑

剩下的就是使用引介过去的 MoneyValidatorImpl 的逻辑了,这个逻辑也非常的简单,咱先理一下思路哈:首先把方法的请求参数先拿出来,然后拿到目标对象的代理对象(注意此处必须要拿到代理对象,原始目标对象压根就没实现 MoneyValidator 接口),强转为 MoneyValidator 类型,就可以调用它的 validate 方法了。如果 validate 方法返回 true ,则接下来的方法可以执行;如果返回 false ,则代表 money 参数不合法,抛出参数不合法的异常即可。

用代码编写也很简单:

@Before("execution(* com..f_introduction.service.FinanceService.transfer(Long, Long, int))")
public void beforePrintLog(JoinPoint joinPoint) {
    int money = (int) joinPoint.getArgs()[2];
    MoneyValidator validator = (MoneyValidator) joinPoint.getThis();
    if (validator.validate(money)) {
        System.out.println("转账动作前置打印 。。。");
    } else {
        throw new IllegalArgumentException("转账金额不合法!");
    }
}

6.3.6 测试运行

修改一下 main 方法的测试代码,我们把正常数据和错误数据都执行一下:

public static void main(String[] args) throws Exception {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(IntroductionConfiguration.class);
    FinanceService financeService = ctx.getBean(FinanceService.class);
    financeService.transfer(1L, 2L, 100);
    System.out.println("------------------------------");
    financeService.transfer(1L, 2L, -1);
}

运行 main 方法,控制台可以打印出 100 的成功,和 -1 的异常:

转账动作前置打印 。。。
转账完成!
1 为 2 转钱100元!
------------------------------
Exception in thread "main" java.lang.IllegalArgumentException: 转账金额不合法!

由此可以完成引介通知的增强。

7. LoadTimeWeawer

还记得在第 22 章,配置元信息中提到的 <context:load-time-weaver/> 这个标签吗?这个标签的作用是修改代理对象的构建时机。与之相匹配的注解是 @EnableLoadTimeWeaving 。

7.1 AOP增强的时机

之前咱一开始讲解 AOP 的时候,说到 SpringFramework 的 AOP 底层是使用运行时动态代理的技术实现,其实这话并不绝对(所以咱一开始说的是可以,而不是一定),因为从原生的 AOP 设计角度来看,通知的织入是有三种时机的,它们分别是:

  • 字节码编译织入:在 javac 的动作中,使用特殊的编译器,将通知直接织入到 Java 类的字节码文件中

  • 类加载时期织入:在类加载时期,使用特殊的类加载器,在目标类的字节码加载到 JVM 的时机中,将通知织入进去;

  • 运行时创建对象织入:在目标对象的创建时机,使用动态代理技术将通知织入到目标对象中,形成代理对象。

所以你看,我们前面编写的所有 AOP 的实例,全部都是基于运行时创建代理对象的方式织入通知的。除此之外,还有上面的两种方式可以选择,只是我们几乎都不用了。

7.2 AspectJ对于增强的时机

AspectJ 作为很早就出现的 AOP 框架,它可以说是非常强大了,以上三种方式它都有提供方案 / 支持:

  • 对于字节码的编译期织入,它可以利用它自己定义的 AspectJ 语言编写好切面,并借助 Maven 等项目管理工具,在工程的编译期使用特殊的编译器(ajc等),将切面类中定义的通知织入到 Java 类中;

  • 对于类加载时期的织入,它的机制就是 LoadTimeWeaving (刚好就是字面意思);

  • 对于运行时创建对象的织入,它在早期有整合一个叫 AspectWerkz 框架,也是在运行时动态代理产生代理对象,只不过我们现在学习的是 Spring 整合 AspectJ ,那最终还是用基于 SpringFramework 底层的动态代理搞定了。

7.3 AspectJ的LoadTimeWeaving

下面咱还是通过一个简单的示例,来了解一下 LoadTimeWeaving 这个机制。

7.3.1 代码准备

还是跟之前的套路一样,一个 Service 一个 Aspect 一个 Configuration :

@Service
public class UserService {
    
    public void get(String id) {
        System.out.println("获取id为" + id + "的用户。。。");
    }
}
@Component
@Aspect
public class LogAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.g_weawer.service.UserService.*(..))")
    public void beforePrint() {
        System.out.println("LogAspect 前置通知 ......");
    }
}
@Configuration
@ComponentScan("com.linkedbear.spring.aop.g_weawer")
//@EnableAspectJAutoProxy
@EnableLoadTimeWeaving
public class LoadTimeWeavingConfiguration {
    
}

注意!此处不再使用 @EnableAspectJAutoProxy 注解,它是启用运行时的动态代理织入通知,而开启类加载时期的织入就需要使用另外的注解了,也就是上面提到的 @EnableLoadTimeWeaving 注解(或者在 xml 中声明 <context:load-time-weaver/> 标签)。

最后,编写测试启动类,套路还是都一样:

public class LoadTimeWeavingApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(LoadTimeWeavingConfiguration.class);
        UserService userService = ctx.getBean(UserService.class);
        userService.get("aaa");
    }
}

7.3.2 只声明注解并不会生效

此时运行 main 方法,控制台会抛出一个异常:

Caused by: java.lang.IllegalStateException: ClassLoader [sun.misc.Launcher$AppClassLoader] does NOT provide an 'addTransformer(ClassFileTransformer)' method. Specify a custom LoadTimeWeaver or start your Java virtual machine with Spring's agent: -javaagent:spring-instrument-{version}.jar

大概翻译一下,说是如果使用类加载器阶段的通知织入,要么自定义一个 LoadTimeWeaver ,要么导个 jar 包,而这个 jar 包叫 spring-instrument 。

这个家伙我们没见过,但它说了,那咱就导吧:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-instrument</artifactId>
    <version>${spring.framework.version}</version>
</dependency>

由于一开始我把工程中 SpringFramework 的版本全部定义了 5.2.8.RELEASE ,所以这里也就是 5.2.8 了。

导入之后还不行,注意异常的提示中还需要一个 vm 启动参数,叫 **-javaagent** ,那好吧,我们也把它加上:

-javaagent:E:/maven/repository/org/springframework/spring-instrument/5.2.8.RELEASE/spring-instrument-5.2.8.RELEASE.jar

注意 jar 包的位置要使用绝对路径,且小伙伴要记得修改这个 jar 包的路径呀。

这样声明好之后,重新运行 main 方法后发现还是不生效,控制台依然没有打印切面日志。。。

7.3.3 aop.xml

在 SpringFramework 整合 AspectJ 的规则中,规定了一点:如果要使用类加载级别的 AOP ,需要在 resources 的 **META-INF** 下编写一个 **aop.xml** 的配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
    <!-- 要织入切面的目标类 -->
    <weaver>
        <include within="com.linkedbear.spring.aop.g_weaving..*"/>
    </weaver>
    <!-- 切面类 -->
    <aspects>
        <aspect name="com.linkedbear.spring.aop.g_weaving.aspect.LogAspect"/>
    </aspects>
</aspectj>

注意,weaver 中包含的类要把切面类一起包含进去!否则无法正常织入切面。

这样写完之后,就算是终于都搞完了。重新运行 main 方法,LogAspect 的通知就被织入到 UserService 中了:

LogAspect 前置通知 ......
获取id为aaa的用户。。。

2.3.4 不好使?

可能有的小伙伴在实际编码时,会遇到按照小册的步骤一步一步来,但最后仍然没有打印切面日志!这种情况就需要另加一个步骤了:

在 vm-options 中再加入一行 javaagent :

-javaagent:E:\maven\repository\org\aspectj\aspectjweaver\1.9.5\aspectjweaver-1.9.5.jar

这样再执行 main 方法,就可以成功打印切面日志了。

不过这样写完之后,控制台会报一个警告:

[AppClassLoader@18b4aac2] error at com\linkedbear\spring\aop\g_weaving\aspect\LogAspect.java::0 class com.linkedbear.spring.aop.g_weaving.aspect.LogAspect is already woven and has not been built in reweavable mode [Xlint:nonReweavableTypeEncountered]

这个警告的意思也很明确,LogAspect 这个切面已经被使用过了,已经织入成功了,所以就不要再搞了。。。

出现这个问题的原因,是因为上面的 javaagent 与 @EnableLoadTimeWeaving 同时存在了,所以导致通知织入了两次。解决方法很简单,把注解配置类上的 @EnableLoadTimeWeaving 注解删掉即可。

上一页Spring AOP下一页Java FrameWorks

最后更新于2年前

这有帮助吗?

img
img
img
img