SpringBoot的AOP是默认开启的,不需要加注解@EnableAspectJAutoProxy____听说SpringAOP 有坑?那就来踩一踩

img

@Aspect
@Component
public class CustomerServiceInterceptor {@Before("execution(public * org.example.aop.demo..*.*(..))")public void doBefore() {System.out.println("do some important things before...");}
}

另外SpringBoot默认是cglib动态代理,开启jdk代码需要修改配置

spring:aop:proxy-target-class: false

以上基于2.2.10版本,至于设置spring.aop.proxy-target-class=false 用aop对controller代理竟然导致接口404 ,复现这个问题controller必须实现接口(有些项目会定义一个BaseController接口),没实现接口的controller不会有这个问题。spring.aop.proxy-target-class=false 会让实现了接口类在被代理时使用jdk代理,设置为true时,统一使用cglib代理。

文章摘录:404的原因是JDK生成的代理类没有被代理的controller的类注解 @Controller以及方法上的@GetMapping,所以springmvc在扫描bean无法为controller建立起请求地址和 controller方法的映射关系。

jdk动态代理

img

这里判断 是否需要生成映射关系就是看你 类上有没有@Controller和@RequestMapping 注解。

img

JDK生成的代理对象 的 类型 没有 这两个注解 所以 不需要处理。

cglib

cglib采用继承被代理类的方式, 是可以溯源 到 父类(被代理类) 去找 注解@Controller 注解的。所以这里 生成接口映射 是 正常运行的。

img


听说SpringAOP 有坑?那就来踩一踩

前言

前几日,有朋友分享了这样一个案例:

原来的项目一直都正常运行,突然有一天发现代码部分功能报错。经过排查,发现Controller里部分方法为private的,原来是同事为Controller添加了AOP日志功能,导致原来的方法报错。

当然了,解决方案就是把private修饰的方法改为public,一切就都正常了。

不过这究竟是为什么呢?如果你也说不太清楚,就跟着笔者一起来探探究竟。

一、SpringBoot添加AOP

我们先为SpringBoot项目添加一个切面功能。

在这里,笔者的SpringBoot的版本为2.1.5.RELEASE,对应的Spring版本为5.1.7.RELEASE

我们必须要先添加AOP的依赖:

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

然后来定义一个切面,来拦截Controller中的所有方法:

@Component
@Aspect
public class ControllerAspect {@Pointcut(value = "execution(* com.viewscenes.controller..*.*(..))")public void pointcut(){}@Before("pointcut()")public void before(JoinPoint joinPoint){System.out.println("前置通知");}@After("pointcut()")public void after(JoinPoint joinPoint){System.out.println("后置通知");}@AfterReturning(pointcut="pointcut()",returning = "result")public void result(JoinPoint joinPoint,Object result){System.out.println("返回通知:"+result);}
}

然后写一个Controller:

@RestController
public class UserController {@AutowiredUserService userService;@RequestMapping("/list")public List<User> list() {return userService.list();}
}

好了,现在访问/list方法,AOP就已经正常工作了。

前置通知
后置通知
返回通知:
[
User(id=59ffbdca-6b50-4466-936d-dddd693aa96b, name=0), 
User(id=ff600c29-2013-493a-aab1-e66329251666, name=1), 
User(id=85527844-bb3d-4cd3-98a1-786f0f754a98, name=2)
]

二、CGLIB原理

首先,我们要知道的是,在SpringBoot中,默认使用的就是CGLIB方式来创建代理。

在它的配置文件中,spring.aop.proxy-target-class默认是true。

{"name": "spring.aop.proxy-target-class","type": "java.lang.Boolean","description": "Whether subclass-based (CGLIB) proxies are to be created (true), as opposed to standard Java interface-based proxies (false).","defaultValue": true
}

然后再回顾下CGLIB的原理:

动态生成一个要代理类的子类,子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用java反射的JDK动态代理要快。

我们看到,CGLIB代理的重要条件是生成一个子类,然后重写要代理类的方法。

下面我们看看CGLIB最基础的应用。

假如我们有一个Student类,它有一个eat()方法。

public class Student {public void eat(String name) {System.out.println(name+"正在吃饭...");}
}

然后,创建一个拦截器,在CGLIB中,它是一个回调函数。

public class TargetInterceptor implements MethodInterceptor {@Overridepublic Object intercept(Object obj, Method method, Object[] params, MethodProxy proxy) throws Throwable {System.out.println("调用前");Object result = proxy.invokeSuper(obj, params);System.out.println("调用后");return result;}
}

然后我们测试它:

public static void main(String[] args){//创建字节码增强器Enhancer enhancer =new Enhancer();//设置父类enhancer.setSuperclass(Student.class);//设置回调函数enhancer.setCallback(new TargetInterceptor());//创建代理类Student student=(Student)enhancer.create();student.eat("王二杆子");
}

这样就完成了通过CGLIB对Student类的代理。

上面代码中的Student就是通过CGLIB创建的代理类,它的Class对象如下:

class com.viewscenes.test.Student$$EnhancerByCGLIB$$121a496f

既然CGLIB是通过生成子类的方式来创建代理,那么它生成的子类就要继承父类咯。

关于Java中的继承,有一条很重要的特性就是:

  • 子类拥有父类非 private 的属性、方法。

看到这里,也许你已经明白了一大半,不过咱们继续看。如果照这样说法,如果父类中有private方法,生成的代理类中是看不到的。

上面的Student类中,学生不仅要吃饭,也许还会偷偷睡觉,那我们给它加一个私有方法:

public class Student {public void eat(String name) {System.out.println(name+"正在吃饭...");}private void sleep(String name){System.out.println(name+"正在偷偷睡觉...");}
}

不过,怎么测试呢?这私有方法在外面也调用不到呀。没关系,我们用反射来试验:

//创建代理类
Student student=(Student)enhancer.create();Method eat = student.getClass().getMethod("eat", String.class);
eat.invoke(student,"王二杆子");Method sleep = student.getClass().getMethod("sleep", String.class);
sleep.invoke(student,"王二杆子");

输出结果如下:

调用前
王二杆子正在吃饭...
调用后
Exception in thread "main" java.lang.NoSuchMethodException: com.viewscenes.test.Student$$EnhancerByCGLIB$$121a496f.sleep(java.lang.String)at java.lang.Class.getMethod(Class.java:1786)at com.viewscenes.test.Test.main(Test.java:23)

很明显,在调用sleep方法的时候,抛出了java.lang.NoSuchMethodException异常。

至此,我们更加确定了一件事:

CGLIB创建的代理类,不会包含父类中的私有方法。

三、为啥其他属性无法注入

我们看完了上面的测试,现在把Controller中的方法也改成private

再访问的时候,会报出java.lang.NullPointerException异常,是因为UserService为null,没有成功注入。

这就不太对了呀?如果说因为私有方法的原因,导致代理类不会包含此方法的话,那么最多AOP不会生效,为什么UserService也没有注入进来呢?

带着这个问题,笔者又翻了翻Spring aop相关的源码,这才理解咋回事。

在这里,我们首先要记住一件事:不管方法是否为私有的,UserController这个Bean是已经确定被代理了的。

1、SpringMVC处理请求

我们的一个HTTP请求,会先经过SpringMVC中的DispatcherServlet,然后找到与之对应的HandlerMethod来处理。在后面,会先通过Spring的参数解析器,把Request参数解析出来,最后通过Method来调用方法。

img

2、反射调用

img

上面代码就是通过反射来调用Controller中的方法。

上面我们说:

不管方法是否为私有的,UserController这个Bean是已经确定被代理了的。

在这里,this.getBean()拿到的就是被代理后的对象。它长这样:

img

可以看到,在这个代理对象中,userService对象为NULL。那么,按理说,不管你方法是否为私有的,这样直接调用也都是要报空指针异常的呀。那么,为啥只有私有方法才会报错,而公共方法不会呢?

3、有啥不一样

在这里,他们的method是一样的,都是java.lang.reflect包中的对象。

如果是私有方法,那么在代理类中,不会包含这个方法。此时通过Method.invoke()来调用目标方法,传入的实例对象是userController的代理类,而这个代理类中的userService为NULL,所以,执行的时候,才会看到userService没有注入,导致空指针异常。

如果是公共方法,在代理类中,就有它的子类实现,则会先调用到代理类的拦截器MethodInterceptor。拦截器负责链式调用AOP方法和目标方法。在拦截器执行过程中,又调用了方法。但不同的是,此时传入的实例对象并不是代理类,而是代理类的目标对象。

img

有朋友对这块不理解,其实就是JDK中java.lang.reflect.Method的内容,来借助测试再看一下。

还是拿上面的Student为例,我们通过Method来获取它的方法并调用。

//创建代理类
Student student=(Student)enhancer.create();Method eat = Student.class.getDeclaredMethod("eat", String.class);
eat.setAccessible(true);
eat.invoke(student,"王二杆子");System.out.println("----------------------");
Method sleep = Student.class.getDeclaredMethod("sleep", String.class);
sleep.setAccessible(true);
sleep.invoke(student,"王二杆子");

上面的代码中,先通过反射拿到Method对象,其中eat是公共方法,sleep是私有方法。invoke传入的对象都是通过CGLIB生成的代理对象,结果就是eat执行了代理,而sleep并没有。

调用前
王二杆子正在吃饭...
调用后
----------------------
王二杆子正在偷偷睡觉...

这也就解释了,为啥同样是调用method.invoke(),私有方法没有注入成功,而公共方法正常。

四、JDK代理

既然说,CGLIB是通过继承的方式实现代理。那私有方法能不能通过JDK动态代理的方式来呢?

不瞒各位,笔者当时确实想到了这个,不过马上被右脑打脸。JDK动态代理是通过接口来的,接口里怎么可能有私有方法?

哈哈,看来此路不通。不过笔者却发现了另外一个有意思的现象。

至此,我们不再讨论公有私有方法的问题,仅仅看Controller是否可以改为JDK动态代理的方式。

1、改为jdk动态代理

首先,我们需要在配置文件中,设置spring.aop.proxy-target-class=false

然后还需要搞一个接口,这个接口还必须包含一个方法。否则Spring在生成代理的时候,还会判断,如果不包含这些条件,还会是CGLIB的代理方式。

public interface BaseController {default void print(){System.out.println("-------------");}
}

然后让我们的Controller实现这个接口就行了。现在代理方式就变成了JDK动态代理

ok,现在访问/list,你会得到一个友好的404提示:

{"status": 404,"error": "Not Found","message": "No message available","path": "/list"
}

2、为何404?

这是为啥捏?

SpringMVC初始化的时候,会先遍历所有的Bean,过滤包含Controller注解和RequestMapping注解的类,然后查找类上的方法,获取方法上的URL。最后把URL和方法的映射注册到容器。

如果你对这一过程不理解,可以参阅笔者文章 - Spring源码分析(四)SpringMVC初始化

在过滤的时候,大概有三个条件:

  • 对象本身是否包含Controller相关注解
  • 对象的父类是否包含Controller相关注解
  • 对象的接口是否包含Controller相关注解

此时我们的userController是一个JDK的代理对象,这三条件都不满足呀,所以Spring认为它并不是一个Controller

因此,我们需要在它接口BaseController上添加一个@RestController注解才行。

加完之后,过滤条件满足了。SpringMVC终于认识它是一个Controller了。不过,如果你现在去访问,还会得到一个404。

3、为何还是404?

笔者当时也是崩溃的,为啥还是404呢?

if (beanType != null && this.isHandler(beanType)) {this.detectHandlerMethods(beanName);
}

原来通过isHandler条件判断之后,还需要通过detectHandlerMethods检测bean上的方法,注册url和对象method的映射关系。

但是这里有个坑~

我们知道,不管是JDK动态代理还是CGLIB动态代理,此时的bean都是代理对象。检测bean上的方法,一定得检测真实的目标对象才有意义。

Spring也正是这样做的,它通过ClassUtils.getUserClass(handlerType);来获取真实对象。

然后看到这段代码的时候,才发现:

img

这里只处理了CGLIB代理的情况。。换言之,如果是JDK的代理对象,这里返回的还是代理对象。

那么在外层,拿着这个代理对象去selectMethods查找方法,当然一无所获。最后的结果就是,没有把这个url和对象method映射起来,当我们访问/list的时候,会报出404。

这里的SpringMVC版本为5.1.7.RELEASE,不知道其他版本是不是也是这样处理的。欢迎探讨~

总结

以前老听一些人说,在Controller里面不要用私有方法,也知道可能会产生问题。

但具体会产生哪些问题?产生问题的根源在哪里?却一直很朦胧,通过本文也许你对这个问题就有了更新的认识。

作者:清幽之地
原文链接:https://juejin.im/post/5d01e088f265da1b7f2978c3

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

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

相关文章

mysql 开启远程访问_QxOrm 访问 MySQL

在前面的 QxOrm 章节中&#xff0c;我们已经介绍了对本地数据库的操作&#xff0c;现在是时候介绍对远程数据库的访问了&#xff0c;那么就以最常用的 MySQL 为例吧&#xff01;在开始之前&#xff0c;首先要安装 MySQL。如果条件允许&#xff0c;建议将其安装在 Linux 系统上&…

当泛型遇到重载

当泛型遇到了重载&#xff0c;好戏&#xff0c;就发生了。 请看下面代码&#xff1a; 问题&#xff1a;代码能正确编译吗&#xff1f; 这个题目是一个考察泛型的题目。java里面&#xff0c;泛型实际上是“伪泛型”&#xff0c;并不像C#那样是实际上的泛型。 IDE会提示我们下…

redis查询所有key命令_三歪推荐:Redis常见的面试题

本文公众号来源&#xff1a;科技缪缪作者&#xff1a;科技缪缪本文已收录至我的GitHub说说Redis基本数据类型有哪些吧字符串&#xff1a;redis没有直接使用C语言传统的字符串表示&#xff0c;而是自己实现的叫做简单动态字符串SDS的抽象类型。C语言的字符串不记录自身的长度信息…

springboot系列——redisTemplate和stringRedisTemplate对比、redisTemplate几种序列化方式比较

文章目录一、redisTemplate和stringRedisTemplate对比1、StringRedisTemplate2、RedisTemplate二、redisTemplate序列化方式比较1、性能测试对比2、性能总结3、方案一、考虑效率和可读性&#xff0c;牺牲部分空间4、方案二、空间敏感&#xff0c;忽略可读性和效率影响5、使用示…

mysql查询默认排序规则_深究 mysql 默认排序, order by 的顺序【收藏】

mysql 语句中如果没有使用 order by 来排序&#xff0c;通常会用 主键正序排列&#xff0c;但是有的时候不是这样&#xff0c;来看一个实例。实例群友问&#xff1a;请教一个问题&#xff0c;mysql 默认排序问题&#xff0c;当sql 语句 的排序没有指定 主键&#xff08;id&…

Spring Boot jackson配置使用详解

Spring Boot系列-json框架jackson配置详解 T1 - 前言 目前Java最常见的3中JSON操作框架分别为Gson、Jackson、FastJson&#xff0c;该篇文章主要讲解jackson在SpringBoot环境中各配置项的具体作用。 T2 - 环境依赖 jackson是spring-boot的web/webflux框架默认依赖的json库&…

频率统计表用c语言_空间矢量脉宽调制建模与仿真(基于C语言的SIMULINK仿真模型 | 基于SVPWM模块的仿真)...

文末有仿真模型下载方式1.1 基于C语言的SIMULINK仿真模型使用C语言在MATLAB/SIMULINK中仿真&#xff0c;需要借助s-function builder模块实现。七段式SVPWM仿真模型如图1-1所示。仿真解算器&#xff08;Solver&#xff09;选择变步长&#xff08;Variable-step&#xff09;、od…

php基本语法 格式,PHP 基本语法格式

PHP 基本语法格式标准代码如下:复制代码 代码如下:...?>短标签模式(此模式需要修改PHP配置&#xff0c;让PHP支持短标签模式)&#xff1a;复制代码 代码如下:...?>注释&#xff1a;复制代码 代码如下:/* ...*///#时间&#xff1a; 2009-12-14Abs: 取得绝对值. Acos: 取…

linux 服务器启用端口,linux服务器放行端口

一、默认使用iptables的系统(例如centos6)1、关闭所有的 INPUT FORWARD OUTPUT 只对某些端口开放。下面是命令实现&#xff1a;iptables -P INPUT DROPiptables -P FORWARD DROPiptables -P OUTPUT DROP再用命令iptables -L -n查看 是否设置好&#xff0c; 好看到全部 DROP 了这…

MySQL 无符号和有符号的区别

随笔记录: mysql无符号和有符号的区别 无符号unsigned 表示设置的的数据为0或者正数&#xff1b; 有符号则可以是负数 -&#xff1b; 内存占比 有符号 0-255 无符号 -127~127

linux下tomcat启动后无进程,Linux中Tomcat shutdown.sh后进程仍然存在解决办法

最近我们在使用Jenkins自动化部署项目时&#xff0c;在生产liunx环境下&#xff0c;使用脚本shutdown.sh停止tomcat服务&#xff0c;然后再start之后发现应用无法访问了&#xff0c;后台查看tomcat进程是发现有个2个tomcat进程&#xff0c;说明之前的shutdown并没有完全停掉tom…

扫地机器人电路原理图_扫地机有这一台就够了:石头扫地机器人T6 首拆

听说集齐13台扫地机器人可以召唤扫地机神兽&#xff0c;所以集齐了22台扫地机后我的神兽呢&#xff1f;自从上个月入手了石头科技出品的小瓦青春版扫地机之后已经集齐了所有小米(石头)系扫地机。小瓦青春版是一款无序清洁的扫地机产品&#xff0c;也是石头科技价格最低的入门级…

让我放弃FastDFS拥抱MinIO的8个瞬间

目前可用于文件存储的网络服务选择有很多&#xff0c;比如阿里云OSS、七牛云、腾讯云等等&#xff0c;但是收费都有点小贵。为了帮公司节约成本&#xff0c;之前一直是使用fastDFS作为文件服务器&#xff0c;准确的说是图片服务器。直到我发现了MinIO&#xff0c;我决定放弃Fas…

http 和 https_Golang设置https访问,以及http如何重定向到https

设置https访问&#xff1a;初始代码为http监听&#xff1a;func main() { server : &http.Server{ Addr: ":8080", ... } go func() { if err : server.ListenAndServe(); err ! nil && err ! http.ErrServerClosed { log.Fa…

盘点分布式文件存储系统____分布式文件存储系统简介

盘点分布式文件存储系统 在项目的数据存储中&#xff0c;结构化数据通常采用关系型数据库&#xff0c;非结构化数据&#xff08;文件&#xff09;的存储就有很多种方式&#xff0c;服务器本地存储、Nas挂载、ftp等等&#xff0c;今天就来盘点一下&#xff0c;分布式文件存储系统…

常见分布式文件存储介绍、选型比较、架构设计

数据正成为世界上最有价值的资源&#xff0c;分布式文件存储是应对数据爆炸的最好解决方案&#xff0c;那就会涉及到分布式文件存储方案、选型、架构设计等。 分布式文件存储的来源 在这个数据爆炸的时代&#xff0c;产生的数据量不断地在攀升&#xff0c;从GB,TB,PB,ZB.挖掘…

为什么python工程师掌握这些就够了_Python工程师薪资飙升,Python这些技能你掌握了吗...

Python的火热&#xff0c;也带动了工程师们的就业热。那么&#xff0c;Python的市场需求和工程师待遇到底如何呢&#xff1f;今天我们来看看具体数据。2019年招聘python工程师薪资飙升&#xff0c;Python这些技能你掌握了吗?Python岗位和待遇和要求怎么样&#xff1f;下面从招…

canvas js 绘图插件_Canvas专题—绘制柱状图(2)

目标&#xff1a;在我们了解过前两章节的知识点以后&#xff0c;为了把知识点进行连结&#xff0c;拓展&#xff0c;现在我们做一个图表吧。看一个echartst图表当今Canvas库有很多供我们选择&#xff0c;比如&#xff0c;echarts在很大程度上能够提高我们的工作效率&#xff0c…

界面优美的linux,Ubuntu 11.04新版发布 诠释精美用户界面

主要改进内容如下&#xff1a;Unity界面在Alpha 3中进行了重大改进&#xff0c;重新设计了Dash面板的功能&#xff0c;可以按类别显示应用程序&#xff0c;此外还包括更好的搜索过滤、支持全屏以及其他视觉效果的提升。Ubuntu 11.04 Alpha 3升级了安装程序&#xff0c;用户如今…

springboot 使用 minio

springboot 使用 minio yml 配置 yml 配置 # ↓↓↓↓↓↓ MinIO文件服务器 ↓↓↓↓↓↓ minio:url: http://114.67.110.190:9001accessKey: minioadminsecretKey: minioadminbucketName: zpfmaven: <dependency><groupId>io.minio</groupId><artifa…