背景
在我的SpringBoot项目中,类ExcelController调用ExcelServiceImpl类,而ExcelServiceImpl类有一个私有对象excelResList,是个有状态的类。这在默认单例的情况下ExcelServiceImpl类线程不安全。
因此,为了避免并发场景下,单例的ExcelServiceImpl类的excelResList对象被共享,需要将ExcelServiceImpl类设置为多例。代码如下:
@RestController
@RequestMapping("/money/excel")
public class ExcelController {
@Autowired
private ExcelService excelService;
@ApiOperation(value = "Excel批量导入数据")
@PostMapping("import/{userId}")
public Result batchImport(
@ApiParam(value = "用户id", required = true)
@PathVariable("userId") Integer userId,
@ApiParam(value = "Excel文件", required = true)
@RequestParam("file") MultipartFile file)
throws DemoException {
InputStream inputStream = null;
try {
inputStream = file.getInputStream();
List<Bill> bills = excelService.batchImport(inputStream, userId);
return Result.success("批量导入成功", bills);
} catch (Exception e) {
e.printStackTrace();
throw new DemoException("Excel数据导入错误");
}
}
}
@Service
//由于该类有状态,需要设置成多例
@Scope("prototype")
public class ExcelServiceImpl implements ExcelService {
//每个对象都有自己的excelResList对象
private List<Bill> excelResList = new ArrayList<>();
@Override
public void addExcelResList(List<Bill> bills){
excelResList.addAll(bills);
}
...
}
问题
但是,测试时发现ExcelServiceImpl类仍然是单例的。即@Scope(“prototype”)无法生效。
原因
原来,单纯在ExcelServiceImpl类上声明原型域,是无法实现多例的。因为调用者ExcelController是单例的,在实例化时只有一次设置属性ExcelServiceImpl的机会,在运行时是无法创建新的bean的。所以ExcelServiceImpl类仍然是单例的。
解决方式
我的解决方法是:在ExcelController类上加@Scope(“request”)。
但这种方式会导致ExcelController实现了请求域,即变为了多例,一定程度上浪费了资源。
所以第二种解决方式是:Method injection。官方文档: Method injection。
这种方法使用Lookup方法注入,当需要使用ExcelServiceImpl类时新创建一个实例。代码修改后的如下(注释处是关键代码):
@RestController
@RequestMapping("/money/excel")
//将类声明为abstract
public abstract class ExcelController {
//添加Lookup方法注入,该方法可以新创建ExcelServiceImpl实例。
@Lookup("excelServiceImpl")
protected abstract ExcelService createExcelService();
@ApiOperation(value = "Excel批量导入数据")
@PostMapping("import/{userId}")
public Result batchImport(
@ApiParam(value = "用户id", required = true)
@PathVariable("userId") Integer userId,
@ApiParam(value = "Excel文件", required = true)
@RequestParam("file") MultipartFile file)
throws DemoException {
InputStream inputStream = null;
try {
inputStream = file.getInputStream();
//在这里调用方法,由于ExcelServiceImpl是原型域,所以每次都是新的实例
List<Bill> bills = createExcelService().batchImport(inputStream, userId);
return Result.success("批量导入成功", bills);
} catch (Exception e) {
e.printStackTrace();
throw new DemoException("Excel数据导入错误");
}
}
}
@Service
//同样也需要设置多例
@Scope("prototype")
public class ExcelServiceImpl implements ExcelService {
//使得每个对象都有自己的excelResList对象
private List<Bill> excelResList = new ArrayList<>();
@Override
public void addExcelResList(List<Bill> bills){
excelResList.addAll(bills);
}
...
}
经过测试,这两种方法都能解决并发问题。
多例Bean销毁问题
SpringBoot会将单例Bean进行自动销毁,而不会对多例Bean进行销毁。多例Bean留给JVM进行销毁,待多例Bean没有被引用后就会被垃圾回收,正常情况下不用我们手动销毁多例Bean。
总结
第二种方法较第一种方式好处在于减少了调用者ExcelController的多例,减少了资源浪费。因此,最好的方法是Method injection。