本文为笔者实习期间个人学习记录
絮絮叨叨
要说实习这段时间印象最深的,不是认识了很多之前没接触过的技术,而是没完没了地修改controller和service层的代码…
现在项目结束了再回望,大致可分为三个阶段:
- 非常粗略的实现了增删改查的功能
- 代码完善与逻辑修改(含部分Bug修复)
- 公共部分抽取(工具类编写)
返工颇多,分析后主要是因为之前没有开发经验,前期设计考虑不足,并且减少代码重复率的意识不强,一味地复制黏贴导致较多冗余.
原本抱有这样的想法:都是相似的代码,修改起来很快。【x】 然而 并 不 是.
6个模块,6个controller+6个service的抽象类和6个service的实现类+6个以上DTO+至少3*6个VO,有好几十个类都有可能进行重复的、类似的修改,一是修改起来很麻烦工作量大,二是非常枯燥且没有什么意义.
因此下面复盘总结一下自己重构整理代码的过程,以后少走弯路,减少返工.
仅以CategoryController.java与CategoryServiceImpl.java为例
1. 初步实现
第一遍写的代码非常粗糙,仅仅是实现了基本功能使得前后端能够调通,逻辑乱,存在较多Bug.
/**
* @author : huangyuhui
* @version : 1.0
* @date : 2019/9/3
*/
@RestController
public class CategoryController {
@Autowired
private CategoryService categoryService;
@GlobalExceptionLog
@CrossOrigin
@PostMapping("api/deleteCategories")
@ApiLog
public CommonResponse<String> delete(@RequestBody CommonRequest<List<CategoryDataItemVO>> commonRequest){
List<CategoryDataItemVO> volist= commonRequest.getBody();
if(volist!=null && volist.size()>0){
//vo列表转dto列表
List<CategoryDTO> dtos=new ArrayList<CategoryDTO>();
CategoryDTO categoryDTO=null;
for(CategoryDataItemVO vo: volist){
categoryDTO=new CategoryDTO();
BeanUtils.copyProperties(vo,categoryDTO);
dtos.add(categoryDTO);
}
//调用service里面的对应方法进行删除
int result=0;
try {
result = categoryService.delete(dtos);
}catch (ServiceException serviceException){
//抛出自定义异常
throw new BusinessException(serviceException);
}
//返回CommonResponse给前端
CommonResponse<String> response=new CommonResponse<>();
ResponseHead head = new ResponseHead();
head.setEncryption(0);
head.setCode("0");
//
if(result>0){
head.setMessage("删除类别成功");
}else{
head.setMessage("删除类别失败");
}
response.setResponseHead(head);
return response;
}
return null;
}
@GlobalExceptionLog
@CrossOrigin
@PostMapping("api/addCategory")
@ApiLog
public CommonResponse<String> add(@RequestBody CommonRequest<CategoryDataItemVO> commonRequest ){
CategoryDataItemVO vo=commonRequest.getBody();
if(vo!=null) {
CategoryDTO categoryDTO = new CategoryDTO();
BeanUtils.copyProperties(vo, categoryDTO);
int result = 0;
try {
result = categoryService.add(categoryDTO);
} catch (ServiceException serviceException) {
throw new BusinessException(serviceException);
}
//返回CommonResponse给前端
CommonResponse<String> response = new CommonResponse<>();
ResponseHead head = new ResponseHead();
head.setEncryption(0);
head.setCode("0");
//
if (result > 0) {
head.setMessage("增加类别成功");
} else {
head.setMessage("增加类别失败");
}
response.setResponseHead(head);
return response;
}
return null;
}
@GlobalExceptionLog
@CrossOrigin
@PostMapping("api/updateCategory")
@ApiLog
public CommonResponse<String> update(@RequestBody CommonRequest<CategoryDataItemVO> commonRequest){
CategoryDataItemVO vo=commonRequest.getBody();
if(vo!=null) {
CategoryDTO categoryDTO = new CategoryDTO();
BeanUtils.copyProperties(vo, categoryDTO);
int result = 0;
try {
result = categoryService.update(categoryDTO);
} catch (ServiceException serviceException) {
throw new BusinessException(serviceException);
}
//返回CommonResponse给前端
CommonResponse<String> response = new CommonResponse<>();
ResponseHead head = new ResponseHead();
head.setEncryption(0);
head.setCode("0");
//
if (result > 0) {
head.setMessage("修改类别成功");
} else {
head.setMessage("修改类别失败");
}
response.setResponseHead(head);
return response;
}
return null;
}
@CrossOrigin
@GetMapping("api/loadCategories")
@ApiLog
public List<CategoryDTO> list() throws Exception {
CategoryDTO dto = new CategoryDTO();
return categoryService.queryByCondition(dto);
}
@GlobalExceptionLog
@CrossOrigin
@PostMapping("api/queryCategory")
@ApiLog
public List<CategoryDTO> query(@RequestBody CommonRequest<CategoryQueryConditionVO> commonRequest) {
CategoryQueryConditionVO vo=commonRequest.getBody();
if(vo!=null){
CategoryDTO dto=new CategoryDTO();
BeanUtils.copyProperties(vo,dto);
System.out.println(dto);
List<CategoryDTO> list=categoryService.queryByCondition(dto);
return list;
}
return null;
}
}
- 其实一开始我们设计的是:以get方式请求load方法(因为load返回所有数据,不需要传递参数),以post方式请求query方法.但是后期这种方案就无意义了,因为前端传递给后端的数据统一成了CommonRequest,load方法也改成了有参的post请求,这样就没必要分两个方法写了,于是后期load被删去,统一用query进行查询.
- 存在返回null的情况,这是不允许的,返回参数应该统一为CommonResponse,然后前端依据这个自定义报文的返回码判断是否执行成功或者异常.
/**
* @author : huangyuhui
* @version : 1.0
* @date : 2019/9/2 0002
*/
@Service
public class CategoryServiceImpl implements CategoryService {
@Autowired
private CategoryDao categoryDao;
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
/**
* 增加题目类别
* @param categoryDTO
* @return
*/
public int add(CategoryDTO categoryDTO) {
if(categoryDTO!=null){
SnowFlake snowFlake=new SnowFlake(2,3);
Date createdTime= DateUtils.getDate();
//设置id(雪花算法获得)
categoryDTO.setId(snowFlake.nextId());
//更新时间和创建时间一样
categoryDTO.setCreatedTime(createdTime);
categoryDTO.setUpdatedTime(createdTime);
//创建者和更新者后续从登录信息中获取
categoryDTO.setCreatedBy((long)9527);
categoryDTO.setUpdatedBy((long)9527);
//设置版本
categoryDTO.setVersion((long)1.0);
try{
categoryDao.insert(categoryDTO);
return 1;
}catch (Exception e){
e.printStackTrace();
}
}
return 0;
}
/**
* 删除题目类别
* @param categoryDtos
* @return
*/
public int delete(List<CategoryDTO> categoryDtos) {
CategoryDTO categoryDTO=new CategoryDTO();
if(categoryDtos!=null){
for(CategoryDTO categoryDto : categoryDtos){
categoryDTO=categoryDto;
categoryDao.delete(categoryDTO);
}
}
return 0;
}
/**
* 修改题目类别信息
* @param categoryDTO
* @return
*/
public int update(CategoryDTO categoryDTO) {
//省略版本号对比
Category category =new Category();
BeanUtils.copyProperties(categoryDTO,category);
category.setUpdatedTime(DateUtils.getDate());
return categoryDao.updateByPrimaryKeySelective(category);
}
/**
* 通过id查找题目类别
* @param id
* @return
*/
public CategoryDTO getByPrimaryKey(Long id) {
Category category=categoryDao.selectByPrimaryKey(id);
CategoryDTO dto= new CategoryDTO();
BeanUtils.copyProperties(category,dto);
return dto;
}
/**
* 根据条件查找
* @param categoryDTO
* @return
*/
public List<CategoryDTO> queryByCondition(CategoryDTO categoryDTO) {
Condition condition = new Condition(Category.class);
Example.Criteria criteria=condition.createCriteria();
if(!StringUtils.isEmpty(categoryDTO.getName())){
criteria.andLike("name","%"+categoryDTO.getName()+"%");
}
List<Category> categories=categoryDao.selectByExample(condition);
List<CategoryDTO> dtos=null;
CategoryDTO dto=null;
if(categories!=null){
dtos=new ArrayList<CategoryDTO>(categories.size());
for(Category category:categories){
dto=new CategoryDTO();
BeanUtils.copyProperties(category,dto);
dtos.add(dto);
}
}else{
dtos=new ArrayList<CategoryDTO>();
}
return dtos;
}
/**
* 查找所有题目类别
* @return List<CategoryDTO> 不会为null
*/
public List<CategoryDTO> queryAll() {
List<CategoryDTO> dtos=new ArrayList<CategoryDTO>();
List<Category> categories=categoryDao.selectAll();
CategoryDTO dto=null;
if(categories!=null){
for(Category category:categories){
dto=new CategoryDTO();
BeanUtils.copyProperties(category,dto);
dtos.add(dto);
}
}
return dtos;
}
}
- 没有进行异常处理
- 没有进行分页处理
- 还存在未实现的,如版本对比和事务管理
- 代码不规范
2. 封装前后端传递的参数
入参:CommonRequest<xxxVO>
返参:CommonResponse<xxx> 不会有返回为null的情况,约定报文头部的code为“0”时为表示成功,其余均为异常/失败
@GlobalExceptionLog
@CrossOrigin
@PostMapping("/delete")
@ApiLog
public CommonResponse<String> delete(@RequestBody CommonRequest<List<CategoryDataItemVO>> commonRequest){
//返回CommonResponse给前端
CommonResponse<String> response=new CommonResponse<>();
ResponseHead head = new ResponseHead();
List<CategoryDataItemVO> volist= commonRequest.getBody();
int result=0;
if(volist!=null && volist.size()>0){
//vo列表转dto列表
List<CategoryDTO> dtos=new ArrayList<CategoryDTO>(volist.size());
CategoryDTO categoryDTO=null;
for(CategoryDataItemVO vo: volist){
categoryDTO=new CategoryDTO();
categoryDTO.setId(vo.getId());
categoryDTO.setVersion(vo.getVersion());
dtos.add(categoryDTO);
}
//调用service里面的对应方法进行删除
result= categoryService.delete(dtos);
if(result>0){
head.setCode("0");
head.setMessage("删除题目类别成功");
}else {
head.setMessage("删除题目类别失败");
}
}else {
head.setMessage("请求参数为空");
}
head.setEncryption(0);
response.setResponseHead(head);
response.setBody(""+result);
return response;
}
- 修改后,所有方法开头均为声明CommonResponse及其头部,然后取出CommonRequest的body部分进行处理.
- api路径原来是方法上加注解@GetMapping(“api/loadCategories”)
更改为,类上加@RequestMapping("/api/Category"),方法上只写描述该方法功能的路径,如:@PostMapping("/delete") - 还是有较多重复.
/**
* 删除题目类别
* @param categoryDtos
* @return
*/
@Override
public int delete(List<CategoryDTO> categoryDtos) {
int result=0;
if(categoryDtos!=null){
for(CategoryDTO categoryDTO : categoryDtos){
Category category=categoryDao.selectByPrimaryKey(categoryDTO.getId());
//判断数据是否存在
if(StringUtils.isEmpty(category)){
//如果为空则抛出不存在异常
throw new ServiceException(BesDataExceptionEnum.CATEGORY_NON_EXISTENT);
}
//判断数据版本是否一致
if(!category.getVersion().equals((categoryDTO.getVersion()))){
throw new ServiceException(BesDataExceptionEnum.CATEGORY_VERSION_DISACCORD);
}
if(category.getStatus()==1){
//数据正在被使用,不允许删除
throw new ServiceException(BesDataExceptionEnum.CATEGORY_IN_USE);
}
try{
int res=categoryDao.deleteByPrimaryKey(categoryDTO.getId());
result+=res;
}catch (Exception e){
throw new ServiceException(BesDataExceptionEnum.CATEGORY_IN_USE);
}
}
}
return result;
}
- 添加了异常处理,如果异常,则封装成自定义的统一异常类抛出,由于Controller层方法前添加了 @GlobalExceptionLog注解,会对异常进行截获,将异常码和异常信息封装到CommonResponse的body后返回.
3. 查询返回分页数据
- 上一版本:直接返回body为List<xxxVO>的CommonResponse
- 修改后:使用PageHelper进行分页处理,返回的CommonResponse的body为PageInfo<xxxVO>
@GlobalExceptionLog
@CrossOrigin
@PostMapping("/query")
@ApiLog
public CommonResponse<PageInfo<CategoryDataListVO>> query(@Valid @RequestBody CommonRequest<CategoryQueryConditionVO> commonRequest) {
CategoryQueryConditionVO vo=commonRequest.getBody();
CommonResponse<PageInfo<CategoryDataListVO>> response= new CommonResponse<>();
ResponseHead head=new ResponseHead();
CategoryDTO dto=new CategoryDTO();
if(vo!=null){
BeanUtils.copyProperties(vo,dto);
}
PageInfo<CategoryDTO> categoryDTOPageInfo=categoryService.queryByConditions(dto);
PageHelper.clearPage();
//将List<DTO>装换为List<VO>
List<CategoryDataListVO> voList=new ArrayList<>();
CategoryDataListVO categoryDataListVO=null;
for(CategoryDTO categoryDTO:categoryDTOPageInfo.getList()){
categoryDataListVO=new CategoryDataListVO();
BeanUtils.copyProperties(categoryDTO,categoryDataListVO);
voList.add(categoryDataListVO);
}
//PageInfo<DTO>转化为PageInfo<DataListVO>
PageInfo<CategoryDataListVO> categoryDataListVOPageInfo=new PageInfo<>();
BeanUtils.copyProperties(categoryDTOPageInfo,categoryDataListVOPageInfo);
categoryDataListVOPageInfo.setList(voList);
//设置返回数据格式
head.setCode("0");
head.setMessage("查询成功");
response.setBody(categoryDataListVOPageInfo);
response.setResponseHead(head);
return response;
}
/**
* 根据条件查找
* @param categoryDTO
* @return
*/
@Override
public PageInfo<CategoryDTO> queryByConditions(CategoryDTO categoryDTO) {
//设置查询条件
Condition condition = new Condition(Category.class);
Example.Criteria criteria=condition.createCriteria();
if(!StringUtils.isEmpty(categoryDTO.getName())){
criteria.andLike("name","%"+categoryDTO.getName()+"%");
}
if(categoryDTO.getParentId()!=null){
criteria.andEqualTo("parentId",categoryDTO.getParentId());
}
if(categoryDTO.getId()!=null){
criteria.andEqualTo("id",categoryDTO.getId());
}
List<Category> categories=null;
PageHelper.startPage(categoryDTO.getIndex(),categoryDTO.getPageSize());
try {
categories= categoryDao.selectByExample(condition);
}catch (Exception e){
throw new ServiceException(BesDataExceptionEnum.CATEGORY_QUERY_ERROR);
}
PageInfo<Category> categoryPageInfo=new PageInfo<>(categories);
List<CategoryDTO> dtos=new ArrayList<>();
CategoryDTO dto=null;
for(Category category : categoryPageInfo.getList()){
dto=new CategoryDTO();
BeanUtils.copyProperties(category,dto);
dtos.add(dto);
}
//将entity分页信息转化为DTO分页
PageInfo<CategoryDTO> categoryDTOPageInfo=new PageInfo<>();
BeanUtils.copyProperties(categoryPageInfo,categoryDTOPageInfo);
//修改list对象
categoryDTOPageInfo.setList(dtos);
return categoryDTOPageInfo;
}
4. 添加List转换类
由于存在大量的List<xxxVO>和List<xxxDTO>的转换,因此抽取出来编写了工具类TransformClass.java.
上一版本:
//vo列表转dto列表
List<CategoryDTO> dtos=new ArrayList<CategoryDTO>();
CategoryDTO categoryDTO=null;
for(CategoryDataItemVO vo: volist){
categoryDTO=new CategoryDTO();
BeanUtils.copyProperties(vo,categoryDTO);
dtos.add(categoryDTO);
}
修改后:
List<CategoryDTO> dtos=TransformClass.convertList(volist,CategoryDTO.class);
原本五六行需要重复写的代码,使用了工具类以后只需一行就搞定了,而且逻辑清晰一目了然.
修改后的delete方法:
@GlobalExceptionLog
@CrossOrigin
@PostMapping("/delete")
@ApiLog
public CommonResponse<String> delete(@RequestBody CommonRequest<List<CategoryDataItemVO>> commonRequest){
//返回CommonResponse给前端
CommonResponse<String> response=new CommonResponse<>();
ResponseHead head = new ResponseHead();
List<CategoryDataItemVO> volist= commonRequest.getBody();
int result=0;
if(volist!=null && volist.size()>0){
//vo列表转dto列表
List<CategoryDTO> dtos=TransformClass.convertList(volist,CategoryDTO.class);
//调用service里面的对应方法进行删除
result= categoryService.delete(dtos);
if(result>0){
head.setCode("0");
head.setMessage("题目类别删除成功");
}else {
head.setMessage("题目类别删除失败");
}
}else {
head.setMessage("请求参数为空");
}
head.setEncryption(0);
response.setResponseHead(head);
response.setBody(""+result);
return response;
}
TransformClass内部还需要注意什么呢?
- 是否具有通用性. 这里可以采用泛型实现.
- 是否进行了异常处理.
- 是否覆盖了所有情况,是否对参数进行了判空等操作,是否一定有返回值.
5. 封装对入参和返参的处理工具类
在第2步的时候,已经将入参和返参规范起来了,统一为CommonRequest和CommonResponse,但是这样做仍然存在较多重复操作,而且对于从入参取得的body部分判空等操作逻辑也不够清晰,所以编写了统一处理的工具类CommonRequestUtils和CommonResponseUtils.
- CommonRequestUtils:主要用于对入参进行检查,如头部检查以及body部分参数判空和List长度检查等等
- CommonResponseUtils:用于封装返回成功的报文. 前面提到过,对于操作失败/异常的处理是使用注解进行注入返回的,因此controller本身代码只需声明返回成功的报文,遇到异常会被截获. 那既然都是一样的成功报文,也就不必要每次都在方法一开始声明报文和头部最后设code为“0”再返回,我们把这一样的操作封装成函数success(),参数为要返回的body及其类型.
下面来看修改后的controller代码:
/**
* 删除题目类别
* @param commonRequest 前端请求报文[body:List<CategoryDataItemVO>]
* @return 响应报文[body:删除成功的记录数result]
*/
@GlobalExceptionLog
//@CrossOrigin
@PostMapping("/delete")
@ApiLog
public CommonResponse<String> delete(@RequestBody CommonRequest<List<CategoryDataItemVO>> commonRequest){
CommonRequestUtils.checkCommonRequest(commonRequest);
try {
List<CategoryDTO> dtos=TransformClass.convertList(commonRequest.getBody(),CategoryDTO.class);
//调用service里面的对应方法进行删除
int result= categoryService.delete(dtos);
return CommonResponseUtils.success(String.valueOf(result));
} catch (ServiceException exception) {
throw new BusinessException(exception);
}
}
简洁明了了许多,大大提高了代码的整洁度和可读性
6. service层公用字段设值改用注解
实体存在公用字段,如创建时间、修改时间、创建人、修改人等,可采用自定义注解进行注入,原本设置相关数据的代码可以删去.
ps:其实这里创建人和修改人的设置原本就未曾实现,就算不使用注解也需要修改成从redis中取得用户的id再设值,那么改成注解之后可以一并处理了.
/**
* 增加题目类别
* @param categoryDTO
* @return
*/
@Override
@SetCommonField(methodType= CommonFieldAspect.TYPE_INSERT)
@Transactional(rollbackFor = {RuntimeException.class, Exception.class, ServiceException.class})
public int add(CategoryDTO categoryDTO) {
int result=0;
if(categoryDTO!=null){
if(categoryDao.selectByPrimaryKey(categoryDTO.getId())!=null){
throw new ServiceException(BesDataExceptionEnum.CATEGORY_REPEAT);
}
//将DTO对象转换为ENTITY对象
Category category=new Category();
BeanUtils.copyProperties(categoryDTO,category);
//设置id(雪花算法获得)
SnowFlake snowFlake=new SnowFlake(2,3);
category.setId(snowFlake.nextId());
//设置版本
category.setVersion(System.currentTimeMillis());
try {
result = categoryDao.insert(category);
}catch (Exception e){
throw new ServiceException(BesDataExceptionEnum.CATEGORY_INSERT_ERROR);
}
}
return result;
}
7.细节补充
- 版本号对比
- 事务注解
- 增加radis
- 增加注释
总结
可以看到,controller层代码经过了一个逐渐丰富完善到重构降低重复率的过程,最终一个方法只需十几二十行即可,且逻辑清晰明了,可读性高. 我认为这整个过程也许无法避免,但是可以尽量预见到.
如,一旦发现有经常重复的代码,可以抽象成工具类对其进行简化;巧用自定义注解进行异常捕获或常用/公用数据设值;一些必要的操作,如分页,能提前写好就不要留到后面修改,否则前端接因为收到的数据不同也要进行相应更改;养成随时写注释的习惯,不要到最后再补.
service层代码一般是越来越多的,因为逻辑都在这层,但也要注意过长的方法要尽量拆分成多个短方法.