如何规范Java代码?既能保证质量的同时兼顾性能?

tech2025-07-05  0

在自己的工作过程中,对于代码的规范总结出以下几点,如果有不足的希望大家指出。

枚举类的使用

在业务中特别是状态的值,在对外发布api的vo对象中,加上状态枚举值的注释,可以让调用方一目了然。示例:

public class ProductVo implements Serializable { /** * 审核状态 * {@link ProductStatus} */ @ApiModelProperty("状态") private Integer status; } 业务对象转换

业务中,依照阿里巴巴研发规范的要求,存在DO(数据库表结构一致的对象),BO(业务对象),DTO(数据传输对象),VO(显示层对象),Query(查询对象)。在实际开发中存在不同业务对象之间的转换,推荐使用mapstruct,可以灵活的控制的不同属性值之间的转换规格,比org.springframework.beans.BeanUtils.copyProperties()方法更加灵活。示例:

public interface CategoryConverter { CategoryConverter INSTANCE = Mappers.getMapper(CategoryConverter.class); @Mappings({ @Mapping(target = "ext", expression = "java(getCategoryExt(updateCategoryDto.getStyle(),updateCategoryDto.getGoodsPageSize()))")}) Category update2Category(UpdateCategoryDto updateCategoryDto); @Mappings({ @Mapping(target = "ext", expression = "java(getCategoryExt(addCategoryDto.getStyle(),addCategoryDto.getGoodsPageSize()))")}) Category add2Category(AddCategoryDto addCategoryDto); } 查询过滤条件

避免在sql层书写过滤条件,筛选条件移植到服务层做处理。sql代码如下:

SELECT * from product where status != -1 and shop_status != 6

在服务层设置查询条件如下:

public PageData<ProductVo> findCustPage(Query query ){ // 产品上线,显示状态 query.setStatus(ProductStatus.ONSHELF); // 产品显示状态 query.setHideState(HideState.VISIBAL); // 店铺未下线 query.setNotStatus(ShopStatus.OFFLINE); return productService.findProductVoPage(query); } 参数校验

在通常的业务中,一般的操作顺序是先进行参数校验,然后再开始真正的逻辑开发。为了让开发专注于业务开发,常见的参数校验使用javax.validation实现,详细可以参考,如何使用validation校验参数?。

乐观锁与悲观锁的使用

乐观锁(使用Spring AOP+注解基于CAS方式实现java的乐观锁)设置重试次数以及重试时间,在简单的对象属性修改使用乐观锁,示例如下:

@Transactional(rollbackFor = Exception.class) @OptimisticRetry public void updateGoods(GoodsUpdateDto dto) { Goods existGoods = this.getGoods(dto.getCode()); // 属性逻辑判断 // if (0 == goodsDao.updateGoods(existGoods, dto)) { throw new OptimisticLockingFailureException("update goods optimistic locking failure!"); } }

悲观锁在业务场景比较复杂,关联关系比较多的情况下使用。例如修改SKU属性时,需要修改商品的价格,库存,分类,等等属性,这时可以对关联关系的聚合根产品进行加锁,代码如下:

@Transactional public void updateProduct(Long id,ProductUpdateDto dto){ Product existingProduct; // 根据产品id对数据加锁 Assert.notNull(existingProduct = lockProduct(id), "无效的产品id!"); // TODO 逻辑条件判断 // TODO 修改商品属性,名称,状态 // TODO 修改价格 // TODO 修改库存 // TODO 修改商品规格 } 读写分离的使用

开发中,经常使用mybatisplus实现读写分离。常规的查询操作,就走从库查询,查询请求可以不加数据库事务,例如列表查询,示例如下:

@Override @DS("slave_1") public List<Product> findList(ProductQuery query) { QueryWrapper<Product> queryWrapper = this.buildQueryWrapper(query); return this.baseMapper.selectList(queryWrapper); }

mybatisplus动态数据源默认是主库,写操作为了保证数据一直性,需要加上事务控制。简单的操作可以直接加上@Transactional注解,如果写操作涉及到非必要的查询,或者使用到消息中间件,reids等第三方插件,可以使用声明式事务,避免查询或者第三方查询异常造成数据库长事务问题。示例,产品下线时,使用reids生成日志code,产品相关写操作执行完成后,发送消息,代码如下:

public void offlineProduct(OfflineProductDto dto){ // TODO 修改操作为涉及到的查询操作 // TODO 使用redis生成业务code // 使用声明式事务控制产品状态修改的相关数据库操作 boolean status = transactionTemplate.execute(new TransactionCallback<Boolean>() { @Nullable @Override public Boolean doInTransaction(TransactionStatus status) { try { // TODO 更改产品状态 } catch (Exception e) { status.setRollbackOnly(); throw e; } return true; } }); // TODO 使用消息中间件发送消息 } spring cache整合redis缓存

通常业务中使用redis作为缓存,结合spring cache使用注解的形式快速使用,整合redis并且自定义默认时间设置可以参考(Spring Cache+redis自定义缓存过期时间)。设置redis默认的缓存时间为7200秒+100秒随机生成数,防止缓存同一时间集体失效。缓存的获取和查询使用如下:

/** * 使用CacheEvict注解更新指定key的缓存 */ @Override @CacheEvict(value = {ALL_PRODUCT_KEY,ONLINE_PRODUCT_KEY}, allEntries = true) public Boolean add(ProductAddDto dto) { // TODO 添加商品更新cache } @Override @Cacheable(value = {ALL_PRODUCT_KEY}) public List<ProductVo> findAllProductVo() { return this.baseMapper.selectList(null); } @Override @Cacheable(value = {ONLINE_PRODUCT_KEY}) public ProductVo getOnlineProductVo() { // TODO 设置查询条件 return this.baseMapper.selectList(query); } 数据库自动给容灾

结合配置中心,简单实现数据库的自动容灾。以nacous配置中心为例,如何使用Nacos实现数据库连接的自动切换?。在springboot启动类加上@EnableNacosDynamicDataSource配置注解,即可无侵入的实现数据库连接的动态切换,示例如下':

@EnableNacosDynamicDataSource public class ProductApplication { public static void main(String[] args) { SpringApplication.run(ProductApplication.class, args); } } 多租户数据过滤

在中台的开发中,数据采用不同平台code的列实现不同平台业务数据的隔离。基于mybatis插件机制的多租户过滤机制实现可以参考如何使用MyBatis的plugin插件实现多租户的数据过滤?。在dao层的方法或者接口上加上自定义过滤条件即可,示例如下:

@Mapper @Repository @MultiTenancy(multiTenancyQueryValueFactory = CustomerQueryValueFactory.class) public interface ProductDao extends BaseMapper<Product> { } 测试用例的编写

基于TDD的原则,结合junit和mockito实现服务功能的测试用例。添加或者修改对象时,需要校验入参的有效性,并且校验操作以后的对象的各类属性。以添加类目的api测试用例为例,如下,添加类别,成功后,校验添加参数以及添加成功后的属性,以及其他默认字段例如状态,排序等字段,源码如下:

// 添加类别的测试用例 @Test public void success2addCategory() throws Exception { AddCategoryDto addCategoryDto = new AddCategoryDto(); addCategoryDto.setName("服装"); addCategoryDto.setLevel(1); addCategoryDto.setSort(1); Response<CategorySuccessVo> responseCategorySuccessVo = this.addCategory(addCategoryDto); CategorySuccessVo addParentCategorySuccessVo = responseCategorySuccessVo.getData(); org.junit.Assert.assertNotNull(addParentCategorySuccessVo); org.junit.Assert.assertNotNull(addParentCategorySuccessVo.getId()); org.junit.Assert.assertEquals(addParentCategorySuccessVo.getPid(), ROOT_PID); org.junit.Assert.assertEquals(addParentCategorySuccessVo.getStatus(), CategoryEnum.CATEGORY_STATUS_DOWN.getValue()); org.junit.Assert.assertEquals(addParentCategorySuccessVo.getName(), addCategoryDto.getName()); org.junit.Assert.assertEquals(addParentCategorySuccessVo.getLevel(), addCategoryDto.getLevel()); org.junit.Assert.assertEquals(addParentCategorySuccessVo.getSort(), addCategoryDto.getSort()); } // 新增类目,成功添加后,返回根据id查询CategorySuccessVo public CategorySuccessVo add(AddCategoryDto addCategoryDto, UserContext userContext) { Category addingCategory = CategoryConverter.INSTANCE.add2Category(addCategoryDto); addingCategory.setStatus(CategoryEnum.CATEGORY_STATUS_DOWN.getValue()); if (Objects.isNull(addCategoryDto.getLevel())) { addingCategory.setLevel(1); } if (Objects.isNull(addCategoryDto.getSort())) { addingCategory.setSort(100); } categoryDao.insert(addingCategory); return getCategorySuccessVo(addingCategory.getId()); }

也需要对添加类目的参数进行校验,例如,名称不能重复的校验,示例如下:

// 添加类目的入参 public class AddCategoryDto implements Serializable { private static final long serialVersionUID = -4752897765723264858L; // 名称不能为空,名称不能重复 @NotEmpty(message = CATEGORY_NAME_IS_EMPTY, groups = {ValidateGroup.First.class}) @EffectiveValue(shouldBeNull = true, message = CATEGORY_NAME_IS_DUPLICATE, serviceBean = NameOfCategoryForAddValidator.class, groups = {ValidateGroup.Second.class}) @ApiModelProperty(value = "类目名称", required = true) private String name; @ApiModelProperty(value = "类目层级") private Integer level; @ApiModelProperty(value = "排序") private Integer sort; } //添加失败的校验校验测试用例 @Test public void fail2addCategory() throws Exception { AddCategoryDto addCategoryDto = new AddCategoryDto(); addCategoryDto.setName("服装"); addCategoryDto.setLevel(1); addCategoryDto.setSort(1); // 名称为空 addCategoryDto.setName(null); Response<CategorySuccessVo> errorResponse = this.addCategory(addCategoryDto); org.junit.Assert.assertNotNull(errorResponse); org.junit.Assert.assertNotNull(errorResponse.getMsg(), CATEGORY_NAME_IS_EMPTY); addCategoryDto.setName("服装"); // 成功添加类目 this.addCategory(addCategoryDto); // 名称重复 errorResponse = this.addCategory(addCategoryDto); org.junit.Assert.assertNotNull(errorResponse); org.junit.Assert.assertNotNull(errorResponse.getMsg(), CATEGORY_NAME_IS_DUPLICATE); }

 

最新回复(0)