V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
anzu
V2EX  ›  Java

Spring Boot 一小坑浪费一下午,虽然解决了但还是不知道原因

  •  
  •   anzu · 2019-03-28 17:06:34 +08:00 · 5005 次点击
    这是一个创建于 2072 天前的主题,其中的信息可能已经有所发展或是发生改变。

    代码并不复杂,写完 Controller 的一点基本代码后,想给传入参数做个验证,于是用了框架提供的 validation,然后噩梦开始了!

    要给 @ RequestParam 的参数做验证,则需要在 controller 上注解 @ Validated,而一旦加上此注解,controller 内用 @ Autowired 注解的 service 则无法被注入,其值为 null。

    // 如果删除下面这行,则 Autowired 正常
    @Validated
    @RestController
    @RequestMapping("/test")
    public class UserController {
        @Autowired
        private UserService userService;
    
        @GetMapping("/hello")
        private String sayHello(
            @RequestParam @Length(min = 5) String username,
            @RequestParam @Range(min = 1, max = 100) Integer age
        ) {
            // 如果保留 Validated,则 userService == null,下面这行报错
            return userService.getHello(username, age);
        }
    }
    

    这种诡异的现象一度让我怀疑 @ Validated 与 @ Autowired 有冲突,然而搜索了很多网页都未找到有人遇到类似的问题。

    更让人崩溃的是这些注解仿佛魔法一般,所有教程都告诉你,只要加上这个小东西就能轻松方便地完成功能了哦,但具体是什么原理一两句话说不清楚。

    于是代码很难调试,不如说我根本无从下手,调试第一步——断点该设置在哪里呢?毫无头绪。

    折腾过程略,我直接说解决方法吧:删除方法的 private 修饰,只要方法不为 private 即可。为什么?我也不太清楚,猜测可能 @ Validated 用了 Spring AOP,导致 private 方法无法被代理。希望知道确切原因的好心人能讲解一下,谢谢。

    26 条回复    2019-04-02 10:23:00 +08:00
    lufer
        1
    lufer  
       2019-03-28 17:22:04 +08:00
    valid 注解为什么不加在参数前边,另外接口多参数的话你写个 vm 统一校验不会优雅很多吗
    MoHen9
        2
    MoHen9  
       2019-03-28 17:30:24 +08:00 via Android
    Validated 是加在 VO 和接口方法上的,你加在 controller 是不对的。
    MoHen9
        3
    MoHen9  
       2019-03-28 17:32:28 +08:00 via Android
    建议点进去看注释说明
    gaius
        4
    gaius  
       2019-03-28 17:35:13 +08:00
    你在哪看的加到 controller 上
    jinue9900
        5
    jinue9900  
       2019-03-28 17:40:26 +08:00
    参数校验用的是 @Valid
    mushishi
        6
    mushishi  
       2019-03-28 17:47:38 +08:00
    我一般是用在 BO 里面,然后 controller 里面接收 Bo 做基本的参数校验,隐约记得是不能放在 private 方法上的,没研究过。。。
    ```java
    @Validated ValidBo bo
    ```
    amwyyyy
        7
    amwyyyy  
       2019-03-28 17:53:12 +08:00
    我这样用没遇到你的问题,建议检查下 Spring 与 SpringMVC 父子容器之间的关系,检查下注解扫描是否有重叠。
    airfling
        8
    airfling  
       2019-03-28 18:04:13 +08:00 via Android
    不建议你这样对界面参数进行检查,而且 @validated 你加在 controller 层面会在传参数的时候,进行 poro 绑定也就是把 controller 的 set 方法全部执行一边,这样 autowared 进来的自然就是 null 了,@validated 这个注解应该加在 poro 就是普通的参数封装类里面
    kosmosr
        9
    kosmosr  
       2019-03-28 18:10:12 +08:00 via Android
    姿势问题
    zhazi
        10
    zhazi  
       2019-03-28 18:10:17 +08:00   ❤️ 1
    anzu
        11
    anzu  
    OP
       2019-03-28 18:26:23 +08:00
    不觉得用法有错。如果接口只有一两个参数,为什么仅仅为了做验证还要费力写个多余的 Class ?
    官方示例是放置在 Service,并没有限定使用范围
    https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-validation.html
    也有教程放置在 Controller,没什么区别
    https://www.mkyong.com/spring-boot/spring-rest-validation-example/
    galaxyyao
        12
    galaxyyao  
       2019-03-28 18:46:54 +08:00   ❤️ 3
    比较奇怪的应该是为什么要把 Controller 方法标记为 private 吧。Spring 的开发估计没预料到会有人这么做吧。。。
    我不记得哪个官方范例里曾经这么做过。

    https://stackoverflow.com/questions/17573742/best-practice-to-keep-method-visibility-with-spring-controller-stereotype
    这篇回答里有人提到虽然可以这么做,但会被 IDE 认为方法不被使用而标记为灰色,也会无法进行单元测试。
    我也想不出将方法设为 private 有什么好处。如果 LZ 有什么特殊意图的话可以补充一下。

    另外官方的范例是把验证注解放在实体类上的:
    https://spring.io/guides/gs/validating-form-input/
    对应 @RequestBody+POST 方法接收参数。以 LZ 的范例而言,name 中很可能包含特殊字符,放在 pathvariable 或 requestparam 中都可能会引起异常,所以从避免 bug 的角度,改为 @RequestBody+实体类验证的话会比较合适。(并不是说不能用 RequestParam )
    我个人写的时候,RequestParam 一般只接收主键或 id 之类的单个参数,一般也懒得加校验了。查不出来资源就返回 null 对象也符合 restful 的设计。
    Kyle18Tang
        13
    Kyle18Tang  
       2019-03-28 18:48:18 +08:00 via Android
    controller 方法用 private 的??我一直用 public。。。没出现过这问题。
    watzds
        14
    watzds  
       2019-03-28 19:33:12 +08:00 via Android
    接口还 private,这藏得死死的哈哈还叫接口吗
    wc951
        16
    wc951  
       2019-03-28 20:29:47 +08:00 via Android
    控制变量法呗,把 validated 注解去掉看能不能注入
    Infernalzero
        17
    Infernalzero  
       2019-03-29 00:07:13 +08:00   ❤️ 5
    因为你加了 @Validated,所以会触发 MethodValidationPostProcessor 的 postProcess 逻辑,然后 getbean 的对象都变成了 cglib 创建的代理了,因为是 cglib 创建的代理类,所以那个 field 是 null
    这里还有个原因就是因为你的这个方法是 private 的,如果是 public 的情况,cglib 创建的代理可以拦截这个方法,看下 CglibAopProxy 里的 DynamicAdvisedInterceptor 这个类的拦截实现就明白了,会取 targetSource 来调用,而 targetSource 就是原本的对象,field 就不是 null 了,但如果方法是 private 的情况就无法拦截直接调用代理类的方法了
    charles2java
        18
    charles2java  
       2019-03-29 00:13:03 +08:00 via Android
    private 当然不能在 controller 修饰对外方法,自己用法不规范
    anzu
        19
    anzu  
    OP
       2019-03-29 10:01:52 +08:00
    用 private 是因为以前刚开始学习 Java 的时候不知在哪里看到过,最佳实践是尽量给方法加上最严格访问权限,保证安全性和封装性。所以自己写之前随便一想好像 Controller 也没有被其它地方调用,顺手就写了 private。

    Java 的注解、反射、切面编程带来方便之余,也破坏了代码的封装性和安全性,扰乱了代码正常执行流程,使 Bug 更难追踪。

    关键在于,在没有加入 @ Validated 注解之前程序一切正常,令我很疑惑。
    x7395759
        20
    x7395759  
       2019-03-29 10:41:55 +08:00
    你都已经用框架了,还考虑 bug 更难追踪、代码正常执行流程??? spring boot 的启动流程去看个 10 遍你也不知道是怎么执行的。。。
    zhazi
        21
    zhazi  
       2019-03-29 11:12:40 +08:00
    访问修饰符不提供安全性
    zhazi
        22
    zhazi  
       2019-03-29 11:14:06 +08:00
    @charles2java 请教一下,哪个方法是对外的
    hmellochan
        23
    hmellochan  
       2019-03-29 11:53:53 +08:00
    controller 就是用来给外部访问的,还 private,另外 private 并不具有安全性,反射可解。
    Seney
        24
    Seney  
       2019-03-29 12:29:45 +08:00
    可以用 aop 拦截请求做校验 这样解耦 又不是侵入式的
    thinkmore
        25
    thinkmore  
       2019-04-01 14:51:22 +08:00
    @Infernalzero 在理,表示赞同
    imcoming
        26
    imcoming  
       2019-04-02 10:23:00 +08:00
    没有用的代码为什么要写出来,写出来还加上 private 保证不被访问,不是脱了裤子放屁么
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3340 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 12:27 · PVG 20:27 · LAX 04:27 · JFK 07:27
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.