解决SpringBoot项目使用多线程处理任务时无法通过@Autowired注入bean问题
作者:Bug开发工程师
最近在做一个“温湿度控制”的项目,项目要求通过用户设定的温湿度数值和实时采集到的数值进行比对分析,因为数据的对比与分析是一个通过前端页面控制的定时任务,经理要求在用户开启定时任务时,单独开启一个线程进行数据的对比分析,并将采集到的温湿度数值存入数据库中的历史数据表,按照我们正常的逻辑应该是用户在请求开启定时任务时,前端页面通过调用后端接口,创建一个新的线程来执行定时任务,然后在线程类中使用 @Autowired
注解注入保存历史数据的service层,在线程类中调用service层保存历史数据的方法实现温湿度数据的保存,这时就出现了一个很尴尬的问题,在新开启的线程中使用 @Autowired
注解无法注入需要的bean(即:保存历史数据的service层),程序一直在报 NullPointerException
。
这是controller层,方法 startExperiment 和 stopExperiment 分别是开始定时任务和停止定时任务的方法,getData方法不属于本次讨论范围,不用管
package com.backstage.controller; import com.alibaba.fastjson.JSONObject; import com.backstage.entity.JsonResponse; import com.backstage.entity.Threshold; import com.backstage.service.MainPageService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; /** * @ProjectName: * @Package: com.backstage.controller * @ClassName: MainPageController * @Description: 主页面相关操作控制器 * @Author: wangzhilong * @CreateDate: 2018/8/29 9:49 * @Version: 1.0 */ @RestController @RequestMapping("/main") public class MainPageController { @Autowired private MainPageService mainPageService; /** * 开始实验 * * @param threshold */ @RequestMapping("/startExperiment") public JsonResponse startExperiment(HttpServletRequest request, Threshold threshold) { return mainPageService.startExperiment(request, threshold); } /** * 停止实验 */ @RequestMapping("/stopExperiment") public JsonResponse stopExperiment() { return mainPageService.stopExperiment(); } /** * 获取实时数据 * * @return */ @RequestMapping("/getData") public JSONObject getData() { return null; } }
service 层接口代码,没什么好说的,直接上代码:
package com.backstage.service; import com.alibaba.fastjson.JSONObject; import com.backstage.entity.JsonResponse; import com.backstage.entity.Threshold; import javax.servlet.http.HttpServletRequest; /** * @ProjectName: * @Package: com.backstage.service * @ClassName: MainPageService * @Description: 主页面相关操作业务层接口 * @Author: wangzhilong * @CreateDate: 2018/8/29 9:51 * @Version: 1.0 */ public interface MainPageService { /** * 开始实验 * * @param threshold */ JsonResponse startExperiment(HttpServletRequest request, Threshold threshold); /** * 停止实验 */ JsonResponse stopExperiment(); /** * 获取实时数据 * * @return */ JSONObject getData(); }
service 层实现类代码,关于springboot项目使用多线程进行业务处理不属于本章节的讨论范围,如有需要,请留言,我会在看到留言后第一时间更新相关技术文章,由于这里删除了一些与本章节无关的代码,如果复制到开发工具内有报错问题,麻烦大家提醒我一下,以便修改,非常感谢
package com.backstage.service.impl; import com.alibaba.fastjson.JSONObject; import com.backstage.entity.*; import com.backstage.monitor.TimingMonitoring; import com.backstage.service.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.Trigger; import org.springframework.scheduling.TriggerContext; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.support.CronTrigger; import org.springframework.stereotype.Service; import javax.servlet.http.HttpServletRequest; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.concurrent.ScheduledFuture; /** * @ProjectName: * @Package: com.backstage.service.impl * @ClassName: MainPageServiceImpl * @Description: 主页面相关操作业务层实现类 * @Author: wangzhilong * @CreateDate: 2018/8/29 9:51 * @Version: 1.0 */ @Service public class MainPageServiceImpl implements MainPageService { @Autowired private ThreadPoolTaskScheduler threadPoolTaskScheduler; private ScheduledFuture<?> future2; @Bean public ThreadPoolTaskScheduler threadPoolTaskScheduler() { return new ThreadPoolTaskScheduler(); } /** * 开始实验 * * @param threshold */ @Override public JsonResponse startExperiment(HttpServletRequest request, Threshold threshold) { TimingMonitoring timingMonitoring = new TimingMonitoring(); timingMonitoring.setThreshold(threshold, list, experiment.getId(), experimentData.getId()); future2 = threadPoolTaskScheduler.schedule(new TimingMonitoring(), new Trigger() { @Override public Date nextExecutionTime(TriggerContext triggerContext) { //设置定时任务的执行时间为3秒钟执行一次 return new CronTrigger("0/10 * * * * ?").nextExecutionTime(triggerContext); } }); return new JsonResponse(0,"开始实验!"); } /** * 停止实验 */ @Override public JsonResponse stopExperiment() { if (future2 != null) { experimentService.upd(getTime()); future2.cancel(true); } return new JsonResponse(0,"结束实验!"); } /** * 获取实时数据 * * @return */ @Override public JSONObject getData() { return null; } protected String getTime() { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return format.format(new Date()); } }
重点,线程类代码,大家注意看,我在代码最开始使用了spring的 @Autowired 注解注入需要的service,可在调用service中的add方法时,程序报空指针异常,一直认为是add方法或者sql语句有问题,找了一上午,也没发现任何问题,后来单独调用这个add方法是可以正常插入数据的,唯独在这个线程类中调用时报错,感觉和线程有莫大的关系,百度一搜,还真找到了,原来,在线程中为了线程安全,是防注入的,没办法,要用到这个类啊。只能从bean工厂里拿个实例了,继续往下看
package com.backstage.monitor; import com.backstage.entity.DetailedData; import com.backstage.entity.Threshold; import com.backstage.entity.ValveValue; import com.backstage.service.DetailedDataService; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; /** * @ProjectName: * @Package: com.backstage.monitor * @ClassName: TimingMonitoring * @Description: 定时监测温(湿)度 数据 * @Author: wangzhilong * @CreateDate: 2018/8/29 10:11 * @Version: 1.0 */ public class TimingMonitoring implements Runnable{ //历史数据业务层接口 @Autowired public DetailedDataService detailedDataService; private Threshold threshold; //阈值实体类 private List<ValveValue> settingData; //设定的温湿度数据 private Integer id; //实验记录id private Integer dataId; //历史数据主表id public void setThreshold(Threshold threshold, List<ValveValue> settingData, Integer id, Integer dataId) { this.threshold = threshold; this.settingData = settingData; this.id = id; this.dataId = dataId; } @Override public void run() { //模拟从PLC获取到的数据 String data = "001,50.5,002,37,003,45.6,004,40,005,55.2,006,58"; if (data == null || data.trim() == "") { return; //若获取到的数据为空,则直接停止该方法的执行 } double temperature = 0.0; //温度 double humidity = 0.0; //湿度 Integer type = null; //数据类型,1是温度,2是湿度 //解析数据,并将数据保存到历史数据数据库 String[] str = data.split(","); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS"); for (int i = 0; i < str.length; i++) { if (i == 1 || i == 5 || i == 9) { //温度 type = 1; temperature += Double.parseDouble(str[i]); //System.out.println("温度" + i + " -》 " + str[i-1] + ":" + str[i]); detailedDataService.add(new DetailedData(null, type, Double.parseDouble(str[i]), format.format(new Date()), str[i - 1], dataId)); } if (i == 3 || i == 7 || i == 11) { //湿度 type = 2; humidity += Double.parseDouble(str[i]); //System.out.println("湿度" + i + " -》 " + str[i-1] + ":" + str[i]); detailedDataService.add(new DetailedData(null, type, Double.parseDouble(str[i]), format.format(new Date()), str[i - 1], dataId)); } } } /** * 获取当前时间,精确到毫秒 * @return */ protected String getTime() { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS"); return format.format(new Date()); } }
获取bean对象的工具类,既然程序无法通过注解拿到需要的bean,那就只好自己写个工具类来获取喽,下面是工具类代码
package com.backstage.config; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; /** * @ProjectName: * @Package: com.backstage.config * @ClassName: ApplicationContextProvider * @Description: 获取bean对象的工具类 * @Author: wangzhilong * @CreateDate: 2018/8/31 13:26 * @Version: 1.0 */ /** * Author:ZhuShangJin * Date:2018/7/3 */ @Component public class ApplicationContextProvider implements ApplicationContextAware { /** * 上下文对象实例 */ private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } /** * 获取applicationContext * * @return */ public static ApplicationContext getApplicationContext() { return applicationContext; } /** * 通过name获取 Bean. * * @param name * @return */ public static Object getBean(String name) { return getApplicationContext().getBean(name); } /** * 通过class获取Bean. * * @param clazz * @param <T> * @return */ public static <T> T getBean(Class<T> clazz) { return getApplicationContext().getBean(clazz); } /** * 通过name,以及Clazz返回指定的Bean * * @param name * @param clazz * @param <T> * @return */ public static <T> T getBean(String name, Class<T> clazz) { return getApplicationContext().getBean(name, clazz); } }
这样呢,就可以在线程类中写一个无参的构造方法,在构造方法中,通过调用工具类中的 getBean() 方法就可以拿到实例了,程序在调用这个线程类时,会自动调用其无参的构造方法,在构造方法中我们将需要的bean对象注入,然后就可以正常使用了,下边是线程类修改后的代码,由于别的地方没有改动,所以这里只给大家改动的代码,省得大家看到一大堆代码头疼。
public TimingMonitoring() { //new的时候注入需要的bean this.detailedDataService = ApplicationContextProvider.getBean(DetailedDataService.class); }
总结
以上所述是小编给大家介绍的SpringBoot项目使用多线程处理任务时无法通过@Autowired注入bean 问题,希望对大家有所帮助,如果大家有任何疑问欢迎给我留言,小编会及时回复大家的!