1. 问题描述

由于过于自信,写的接口没有测试就给到了前端,前端在调用后接口出现NullPointerException空指针异常:

2. 问题排查

根据详细的错误日志发现,代码中通道@Autowired注解注入的Service类对象为NULL。
有人可能会说会不会是Service层的实现类上忘记加了@Service注解,显然不是。如果Service层的实现类没有加@Service注解,而又在Controller层进行注入,程序在启动时就会报错,以下是报错信息:
在这里插入图片描述

我做了一个测试,以下是测试Service层实现类代码:

//@Service
public class UserServiceImpl implements UserService {
    /**
     * 获取用户姓名
     */
    @Override
    public String getUserName() {
        return "张三";
    }
}

都不用到启动这一步,如果你使用了IDEA开发中举,IDEA就会直接给出警告:

无法自动装配。找不到 'UserService' 类型的 Bean。

在这里插入图片描述
所以肯定不是实现类没有加注解的原因。
后来经过筛查以及和其他Controller层的类进行对比差异,发现有个接口方法的修饰符是private,而且只有这一个方法是被private修饰的。于是我将其改为public,最后进行测试,不会报错了。

在这里插入图片描述
错误示例:

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

    @Autowired
    private UserService userService;

    @GetMapping("/getUserName")
    private String getUserName() {
        return userService.getUserName();
    }
}

3. 问题复现

在上面我把private改成public后,再去测试就好了,但原因是什么呢?
下面我自己建了一个测试demo进行了问题复现。

  • Service层
public interface UserService {
    /**
     * 获取用户姓名
     */
    String getUserName();
}
  • Service实现类
@Service
public class UserServiceImpl implements UserService {
    /**
     * 获取用户姓名
     */
    @Override
    public String getUserName() {
        return "张三";
    }
}
  • Controller层(直接将Controller类的接口方法修饰符改为private用于测试)
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/getUserName")
    private String getUserName() {
        return userService.getUserName();
    }
}
  • HTTP请求
GET http://localhost:8080/user/getUserName

运行后,请求结果如下:

在这里插入图片描述
根据结果可以看出,改成private后也是可以正常运行的呀!这是怎么回事啊?
我又排查了一遍,发现项目使用的框架有全局日志切面。难道AOP无法代理private方法吗?在百度求证查了半天有些人是这么说过。

下来就来测试一下,写一个测试AOP:

@Aspect
@Component
@Slf4j
public class AopTest {
    @Pointcut("execution(* com.example.demo.demos.web.controller..*(..))")
    public void controllerPointcut() {
    }

    // 在切入点之前执行的通知
    @Before("controllerPointcut()")
    public void beforeControllerMethod() {
        System.out.println("Before executing Controller method");
        // 可以在这里添加自定义的拦截逻辑
    }
}

在上述代码中,controllerPointcut()方法使用execution表达式定义了一个切入点,该表达式匹配com.example.demo.demos.web.controller包及其子包下的所有类的所有方法。然后,在beforeControllerMethod()方法中使用@Before("controllerPointcut()")注解指定在切入点匹配的方法执行之前执行的通知。

然后再测试一下:

在这里插入图片描述
OK,问题复现出来了。下面进行原理分析。

4. 原因分析

加上了动态代理以后,空指针的异常问题被复现出来了,那默认的代理是什么呢?背过八股文的都知道肯定是:cglib

cglib的代理方式是setSuperClass,是不会代理父类的private方法的。也就是说AOP无法代理private,那么到底是不是这原因导致bean=null呢?

下面做一个对比测试,Controller层的方法先改为public修饰,然后在return userService.getUserName();打上断点

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

    @Autowired
    private UserService userService;

    @GetMapping("/getUserName")
    public String getUserName() {
        return userService.getUserName();
    }
}

这个时候userService不为空,证明是可以正常调用并输出结果的

在这里插入图片描述
结果如下:
在这里插入图片描述
在这里插入图片描述
然后将Controller层的方法改为private修饰,在return userService.getUserName();打上断点观察

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

    @Autowired
    private UserService userService;

    @GetMapping("/getUserName")
    private String getUserName() {
        return userService.getUserName();
    }
}

在这里插入图片描述
从上图可以看出,这时候的userService为NULL。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐