springboot 項目使用多線程處理任務時,線上程中無法通過 @Autowired 註入所需的bean ...
最近在做一個“溫濕度控制”的項目,項目要求通過用戶設定的溫濕度數值和實時採集到的數值進行比對分析,因為數據的對比與分析是一個通過前端頁面控制的定時任務,經理要求在用戶開啟定時任務時,單獨開啟一個線程進行數據的對比分析,並將採集到的溫濕度數值存入資料庫中的歷史數據表,按照我們正常的邏輯應該是用戶在請求開啟定時任務時,前端頁面通過調用後端介面,創建一個新的線程來執行定時任務,然後線上程類中使用 @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); }
好了,至此呢,問題就得到解決了,文章中如錯誤或不足,請指出,不勝感激,本人小白一枚,如有不足,請多多包含,也請各位大佬能不吝賜教,抱拳
參考地址:https://blog.csdn.net/zsj777/article/details/80965081