我的GitHub: Powerveil · GitHub
我的Gitee: Powercs12 (powercs12) - Gitee.com
皮卡丘每天学Java
觉得昨天晚上只是分析和解决问题,很多人可能不知道出bug的场景,今天决定重现bug,让代码改回错误代码,重现看到一大串异常的情形[手动狗头]
问题出处:
B站:https://www.bilibili.com/video/BV1hq4y1F7zk?p=83
9:37时运行时报错,NullPointerException。
异常信息
2023-01-20 11:27:40.009 ERROR 15584 --- [ scheduling-1] o.s.s.s.TaskUtils$LoggingErrorHandler : Unexpected error occurred in scheduled task
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:
### Error updating database. Cause: java.lang.NullPointerException
### The error may exist in com/power/mapper/ArticleMapper.java (best guess)
### The error may involve com.power.mapper.ArticleMapper.updateById
### The error occurred while executing an update
### Cause: java.lang.NullPointerException
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:96) ~[mybatis-spring-2.0.6.jar:2.0.6]
at com.baomidou.mybatisplus.extension.toolkit.SqlHelper.executeBatch(SqlHelper.java:179) ~[mybatis-plus-extension-3.4.3.jar:3.4.3]
at com.baomidou.mybatisplus.extension.toolkit.SqlHelper.executeBatch(SqlHelper.java:204) ~[mybatis-plus-extension-3.4.3.jar:3.4.3]
at com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.executeBatch(ServiceImpl.java:248) ~[mybatis-plus-extension-3.4.3.jar:3.4.3]
at com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.updateBatchById(ServiceImpl.java:200) ~[mybatis-plus-extension-3.4.3.jar:3.4.3]
at com.baomidou.mybatisplus.extension.service.IService.updateBatchById(IService.java:177) ~[mybatis-plus-extension-3.4.3.jar:3.4.3]
at com.baomidou.mybatisplus.extension.service.IService$$FastClassBySpringCGLIB$$f8525d18.invoke() ~[mybatis-plus-extension-3.4.3.jar:3.4.3]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.7.jar:5.3.7]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779) ~[spring-aop-5.3.7.jar:5.3.7]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.7.jar:5.3.7]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-5.3.7.jar:5.3.7]
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) ~[spring-tx-5.3.7.jar:5.3.7]
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388) ~[spring-tx-5.3.7.jar:5.3.7]
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.7.jar:5.3.7]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.7.jar:5.3.7]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-5.3.7.jar:5.3.7]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692) ~[spring-aop-5.3.7.jar:5.3.7]
at com.power.service.impl.ArticleServiceImpl$$EnhancerBySpringCGLIB$$4ebc6fef.updateBatchById() ~[classes/:na]
at com.power.job.UpdateViewCountJob.updateViewCount(UpdateViewCountJob.java:74) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_321]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_321]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_321]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_321]
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84) ~[spring-context-5.3.7.jar:5.3.7]
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) ~[spring-context-5.3.7.jar:5.3.7]
at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:95) [spring-context-5.3.7.jar:5.3.7]
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_321]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_321]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_321]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [na:1.8.0_321]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_321]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_321]
at java.lang.Thread.run(Thread.java:750) [na:1.8.0_321]
Caused by: org.apache.ibatis.exceptions.PersistenceException:
### Error updating database. Cause: java.lang.NullPointerException
### The error may exist in com/power/mapper/ArticleMapper.java (best guess)
### The error may involve com.power.mapper.ArticleMapper.updateById
### The error occurred while executing an update
### Cause: java.lang.NullPointerException
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30) ~[mybatis-3.5.7.jar:3.5.7]
at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:196) ~[mybatis-3.5.7.jar:3.5.7]
at com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.lambda$updateBatchById$3(ServiceImpl.java:203) ~[mybatis-plus-extension-3.4.3.jar:3.4.3]
at com.baomidou.mybatisplus.extension.toolkit.SqlHelper.lambda$executeBatch$1(SqlHelper.java:208) ~[mybatis-plus-extension-3.4.3.jar:3.4.3]
at com.baomidou.mybatisplus.extension.toolkit.SqlHelper.executeBatch(SqlHelper.java:169) ~[mybatis-plus-extension-3.4.3.jar:3.4.3]
... 31 common frames omitted
Caused by: java.lang.NullPointerException: null
at com.power.utils.SecurityUtils.getLoginUser(SecurityUtils.java:24) ~[classes/:na]
at com.power.utils.SecurityUtils.getUserId(SecurityUtils.java:49) ~[classes/:na]
at com.power.handler.mybatisplus.MyMetaObjectHandler.updateFill(MyMetaObjectHandler.java:41) ~[classes/:na]
at com.baomidou.mybatisplus.core.MybatisParameterHandler.lambda$updateFill$1(MybatisParameterHandler.java:154) ~[mybatis-plus-core-3.4.3.jar:3.4.3]
at java.util.Optional.ifPresent(Optional.java:159) ~[na:1.8.0_321]
at com.baomidou.mybatisplus.core.MybatisParameterHandler.updateFill(MybatisParameterHandler.java:152) ~[mybatis-plus-core-3.4.3.jar:3.4.3]
at com.baomidou.mybatisplus.core.MybatisParameterHandler.process(MybatisParameterHandler.java:115) ~[mybatis-plus-core-3.4.3.jar:3.4.3]
at com.baomidou.mybatisplus.core.MybatisParameterHandler.processParameter(MybatisParameterHandler.java:81) ~[mybatis-plus-core-3.4.3.jar:3.4.3]
at com.baomidou.mybatisplus.core.MybatisParameterHandler.(MybatisParameterHandler.java:64) ~[mybatis-plus-core-3.4.3.jar:3.4.3]
at com.baomidou.mybatisplus.core.MybatisXMLLanguageDriver.createParameterHandler(MybatisXMLLanguageDriver.java:45) ~[mybatis-plus-core-3.4.3.jar:3.4.3]
at org.apache.ibatis.session.Configuration.newParameterHandler(Configuration.java:645) ~[mybatis-3.5.7.jar:3.5.7]
at org.apache.ibatis.executor.statement.BaseStatementHandler.(BaseStatementHandler.java:69) ~[mybatis-3.5.7.jar:3.5.7]
at org.apache.ibatis.executor.statement.PreparedStatementHandler.(PreparedStatementHandler.java:41) ~[mybatis-3.5.7.jar:3.5.7]
at org.apache.ibatis.executor.statement.RoutingStatementHandler.(RoutingStatementHandler.java:46) ~[mybatis-3.5.7.jar:3.5.7]
at org.apache.ibatis.session.Configuration.newStatementHandler(Configuration.java:658) ~[mybatis-3.5.7.jar:3.5.7]
at org.apache.ibatis.executor.BatchExecutor.doUpdate(BatchExecutor.java:57) ~[mybatis-3.5.7.jar:3.5.7]
at org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor.java:117) ~[mybatis-3.5.7.jar:3.5.7]
at org.apache.ibatis.executor.CachingExecutor.update(CachingExecutor.java:76) ~[mybatis-3.5.7.jar:3.5.7]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_321]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_321]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_321]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_321]
at org.apache.ibatis.plugin.Invocation.proceed(Invocation.java:49) ~[mybatis-3.5.7.jar:3.5.7]
at com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor.intercept(MybatisPlusInterceptor.java:106) ~[mybatis-plus-extension-3.4.3.jar:3.4.3]
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:62) ~[mybatis-3.5.7.jar:3.5.7]
at com.sun.proxy.$Proxy122.update(Unknown Source) ~[na:na]
at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:194) ~[mybatis-3.5.7.jar:3.5.7]
... 34 common frames omitted
这是定时任务执行的方法。
该定时任务的目的是将文章的浏览量从Redis中更新到Mysql中的文章表中
redisCache自定义的一个类,封装了RedisTemplate的一些方法
viewCountMap的key是文章id,value是文章的viewCount
下面程序运行时定时任务多打印一句话,这是昨天的图片,不想换了。
相关知识:Mybatis Plus、Spring Security、Redis、Mysql
本质原因:我们自己将Article字段的update字段做了自动填充导致自动填充时无法获取当前用户(程序刚启动时没有用户登录)。
为了代码更完善一点,我写了自动填充,但是这也导致了一个问题,在执行updateBatchById(articles)的时候,create相关字段会自动填充,看一下自动填充的具体代码。
getUserId()这个方法
看到这里大家可能就明白了,此时我们还没有登录,获取getLoginUser()就会出错,具体是jwt检验token的时候没有将当前用户信息存入存入SecurityContextHolder。其实也没必要存,因为我们做的是定时任务,不需要什么用户信息。
没有执行SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
所以Authentication没有我们想要的值。没看过源码,不知道有没有默认值(没有默认值,后面看异常出现的位置可以推测)
下图两个位置出现空指针异常(如果Authentication有默认值,1出报异常,没有默认值,2出报异常)
异常信息的下面
上图可见,1处报异常
所以上面讨论的Authentication没有默认值。
测试结果:
从根源修改:
保留自动填充,因为这里没有牵扯到用户,所以自动填充的时候要判断是否填充
public class SecurityUtils {
/**
* 获取用户
**/
public static LoginUser getLoginUser() {
if (!Objects.isNull(getAuthentication())) {
return (LoginUser) getAuthentication().getPrincipal();
}
return null;
}
/**
* 获取Authentication
*/
public static Authentication getAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
}
public static Boolean isAdmin() {
if (!Objects.isNull(getLoginUser())) {
Long id = getLoginUser().getUser().getId();
return SystemConstants.USER_ADMIN_ID.equals(id);
}
return false;
}
public static Long getUserId() {
if (!Objects.isNull(getLoginUser())) {
return getLoginUser().getUser().getId();
}
return null;
}
测试结果:
因为刚才着急测试结果,次数多了几次。
如果某个字段已经被填充,那么这个字段不必填充,
我在定时任务中填充这些字段。
下面是定时任务的修改
@Scheduled(cron = "0/10 * * * * ?")
public void updateViewCount() {
//获取redis中的浏览量
Map viewCountMap = redisCache.getCacheMap("article:viewCount");
//获取Id集合
List collect = viewCountMap.keySet()
.stream()
.map(Long::valueOf)
.collect(Collectors.toList());
//从数据库查到的数据
List articles = new ArrayList<>(articleService.listByIds(collect));
//这个主要简化更改内容的,如果直接使用articles也可以,但是很多无效修改
List articles1 = new ArrayList<>();
for (Article temp : articles) {
Article article = new Article(temp.getId(), temp.getViewCount());
article.setUpdateBy(temp.getUpdateBy());
article.setUpdateTime(temp.getUpdateTime());
articles1.add(article);
}
//更新到数据库中
articleService.updateBatchById(articles1);
}
为了将控制台中输出的日志减少,我不直接在articles中修改veiwCount,而使用articles1(精简版articles)作为参数
关于减少日志(个人看法)
执行SQL语句的时候,控制台显示SQL语句和参数,文章内容占控制台的地方太多了,而且也会增加日志了日志的无效内容,例如文章内容并没有修改却出现在了控制台上。
其中SecurityUtils使用还是方案2的。
查看效果
我电脑当前的时间和updateTime时间是不相同的。
可能有人会问这个不是输出updateBy is null了吗?
在数据库中这个数据的update_by是null
我这里是判断,如果为空就填充
可能又有人会问,如果数据库中就是有个文章的update_by为null,那这不就是一个Bug吗?
这种情况可能是程序的bug,让update_by为null,或者人为修改。
我只能我的能力有限,如果有大佬有思路,留下宝贵的建议。
可能有人看到之前的insertFill是可以修改的,因为SecurityUtils.getUserId()在内部已经处理了为空的情况,不会抛出异常
这次我把这个完善了一下