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。