本文档内容
- saas参考示例及说明
- 强制性编程规范
- 建议性编程规范
- 参考书籍
一、saas参考示例
web层
- 参考示例
package com.bwdz.fp.saas.controller.demo;
import com.bwdz.biz.Result;
import com.bwdz.biz.ResultEnums;
import com.bwdz.biz.exception.BizException;
import com.bwdz.fp.saas.controller.demo.vo.DemoReqVO;
import com.bwdz.fp.saas.controller.demo.vo.DemoResVO;
import com.bwdz.fp.saas.service.demo.DemoService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import javax.annotation.Resource;
/**
* 1. controller 只做参数校验,调用service, 数据转换。不做主要业务逻辑。
* @author chenyao
* @date 2018年5月31日
*/
@Controller
@RequestMapping(value = "/test")
public class DemoController {
/**
* 1. Logger,LoggerFactory 使用org.slf4j类库下定义
* 2. 声明为 private static final
*/
private static final Logger logger = LoggerFactory.getLogger(DemoController.class);
@Resource
private DemoService demoService;
/**
* 返回值为json形式
* 1. 请求参数,以bean方式封装
* 2. 返回值,同一返回Result
* 3. 不关心的异常,无需自己try catch处理异常,统一抛,由Advice处理
* 4. 禁止自己操作Response,返回jsp or json data; json统一使用@ResponseBody,jsp统一返回ModelAndView
* 5. 文件下载推荐使用ResponseEntity。
* @param demoReqVO 请求参数,如果字段很多,使用vo接收;若不多,直接使用参数列表接收。不推荐使用request.getParameter方式
* @return json统一返回Result,方便公共异常处理,封装
*/
@RequestMapping(value = "resultJson")
@ResponseBody
public Result resultJson(DemoReqVO demoReqVO) {
//controller 只做参数校验,调用service, 数据转换。不做主要业务逻辑。
//1.一般情况,不关心的异常,可以try catch, 由公共的异常处理Advice处理
//2. 需要把异常情况,展示到前端,只需,抛对应的异常,由公共异常处理Advice处理,封装成Result
if(demoReqVO == null || StringUtils.isBlank(demoReqVO.getFlag())) {
throw new BizException(ResultEnums.ILLEGAL_ARGUMENT.getCode(), ResultEnums.ILLEGAL_ARGUMENT.getMessage());
}
//其他业务操作
demoService.update();
//json统一返回Result,方便公共异常处理,封装
return Result.success(new DemoResVO());
}
@RequestMapping(value = "resultJsp")
public ModelAndView resultJsp() {
//和resultJson处理方式一样,不同之处
//1. 返回值都应该为ModelAndView, 不推荐使用返回String
return new ModelAndView("yxdagl/yxsmlr");
}
}
- controller 只做参数校验,调用service, 数据转换。不做主要业务逻辑。
- 一般情况,不关心的异常,无需try catch, 由公共的异常处理Advice处理
- 需要把异常情况,展示到前端,只需抛对应的异常,由公共异常处理Advice处理,封装成Result
4.json格式返回值统一返回Result
,返回jsp统一使用modelAndView
5.详细方法编写规范,参考用例中注释。
service层
1.service层,必须定义接口
public interface DemoService {
HealthGoldLogDTO getById(Long id);
/**
* 这里是注释
* @return 更新条目
*/
int update();
/**
* 锁的使用
* @return
*/
int lock(HealthGoldLogDTO healthGoldLogDTO);
}
- 实现
package com.bwdz.fp.saas.service.demo;
import com.bwdz.biz.ResultEnums;
import com.bwdz.biz.exception.ServiceException;
import com.bwdz.biz.manager.lock.LockExecutor;
import com.bwdz.fp.saas.core.model.HealthGoldLog;
import com.bwdz.fp.saas.core.repository.HealthGoldLogRepository;
import com.bwdz.util.Converters;
import com.google.common.base.Supplier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.Resource;
/**
* 1. 面向接口编程,需要定义service接口
* 2. 注意重复代码问题。可以把Service 中通用的组件抽取出来,放在biz包中
* 3. 保持函数尽量短小,公共部分,或无关细节,要进行抽取,使该函数描述在同一抽象层。
* 4. 使用transactionTemplate 处理事务,只有在必要时候添加事务。
* @author chenyao
* @date 2018/5/31.
*/
@Service
public class DemoServiceImpl implements DemoService {
@Resource
private HealthGoldLogRepository healthGoldLogRepository;
//事务相关
@Resource
private TransactionTemplate transactionTemplate;
//锁
@Resource
private LockExecutor lockExecutorImpl;
@Override
public HealthGoldLogDTO getById(Long id) {
//处理异常
if(id == null) {
throw new ServiceException(ResultEnums.ILLEGAL_ARGUMENT.getCode(), ResultEnums.ILLEGAL_ARGUMENT.getMessage());
}
//主要业务在这里
return Converters.convert(healthGoldLogRepository.getById(id), HealthGoldLogDTO.class);
}
@Override
public int update() {
//处理事务
return transactionTemplate.execute(new TransactionCallback() {
@Override
public Integer doInTransaction(TransactionStatus status) {
//update1
HealthGoldLog log = new HealthGoldLog();
log.setAmount("11");
log.setChannel("1");
log.setUserId(123L);
healthGoldLogRepository.save(log);
//update2
//update3
return 1;
}
});
}
@Override
public int lock(final HealthGoldLogDTO healthGoldLogDTO) {
//lockey 确保对该用户唯一
//Supplier 为 guava 中Supplier,以后切换java8 后可以lambda简化
//由于lockExecutorImpl.exec 底层实现已经加上事务了,这里无需在添加事务
return lockExecutorImpl.exec("lock_key_" + healthGoldLogDTO.getUserId(), new Supplier() {
@Override
public Integer get() {
//锁使用场景。非原子性场景:若不存在则添加 等等
HealthGoldLog healthGoldLog = healthGoldLogRepository.getByUserId(healthGoldLogDTO.getUserId());
if(healthGoldLog != null) {
//这里根据业务需求是返回默认值,或者抛异常
//throw new ServiceException(100, "以存在,不可添加");
return 0;
}
return healthGoldLogRepository.save(Converters.convert(healthGoldLogDTO, HealthGoldLog.class));
}
});
/*return lockExecutorImpl.exec("", () -> {
HealthGoldLog healthGoldLog = healthGoldLogRepository.getByUserId(healthGoldLogDTO.getUserId());
if(healthGoldLog != null) {
//这里根据业务需求是返回默认值,或者抛异常
//throw new ServiceException(100, "以存在,不可添加");
return 0;
}
return healthGoldLogRepository.save(Converters.convert(healthGoldLogDTO, HealthGoldLog.class));
});*/
}
}
- service必须定义接口。面向接口编程,而不是具体实现
- service放主要的业务逻辑实现
- 如果多个service,出现重复代码问题,可以把service中通用的组件抽取出来,再biz包中。
- 保持函数尽量短小,公共部分,或无关细节,要进行抽取,使该函数描述在同一抽象层。
- 用transactionTemplate 处理事务,只有在必要时候添加事务。
- 使用LockExecutor 处理加锁。也只要在必要时候加锁。
- 异常处理:无关心,异常直接转换成serviceException抛出。由公共处理
8.详细方法编写规范,参考用例中注释。
仓储层
@Repository
public class HealthGoldLogRepositoryImpl implements HealthGoldLogRepository {
@Resource
private HealthGoldLogMapper healthGoldLogMapper;
public HealthGoldLog getById(Long id) {
return Converters.convert(healthGoldLogMapper.selectByPrimaryKey(id), HealthGoldLog.class);
}
@Override
public HealthGoldLog getByUserId(Long userId) {
return null;
}
@Override
public int save(HealthGoldLog healthGoldLog) {
return healthGoldLogMapper.insert(Converters.convert(healthGoldLog, HealthGoldLogDO.class));
}
}
与mybatis整合
import com.bwdz.fp.saas.dal.model.HealthGoldLogDO;
import org.apache.ibatis.annotations.Param;
/**
* 1. 返回值:以定义好bean的方式返回,不推荐返回map
* 2. 请求值,如果参数不多,可以使用@Param,如果参数多,使用Bean方式接收。不推荐以map方式传入
*/
public interface HealthGoldLogMapper {
int insert(HealthGoldLogDO record);
HealthGoldLogDO selectByPrimaryKey(Long id);
int countByUserIdOfToday(@Param("userId") Long userId, @Param("channel") String channel);
int countByUserId(@Param("userId") Long userId, @Param("channel") String channel);
}
id, user_id, channel, amount, gmt_created, gmt_modified
insert into dental_health_gold_log (id, user_id, channel,amount,
gmt_created, gmt_modified)
values (#{id,jdbcType=BIGINT}, #{userId,jdbcType=BIGINT}, #{channel,jdbcType=CHAR}, #{amount,jdbcType=VARCHAR},
now(), now())
- 返回值:以定义好bean的方式返回,不推荐返回map
- 请求值,如果参数不多,可以使用@Param,如果参数多,使用Bean方式接收。不推荐以map方式。
公共组件
- 公共组件放在com.bwdz.biz.manager包下面,参考:
LockExecutor
/**
* 所有需要加锁执行的服务都通过本接口执行
* 由于使用jdk7,不便于使用jdk8的函数式接口,所以使用guava函数式接口替代
*/
public interface LockExecutor {
T exec(String lockey, Supplier s);
}
- 公共组件不仅仅是把重复代码移到这边。要站在使用者角度,设计更加通用,好用。
util 工具包
- until工具包放在com.bwdz.util中,参考
convert
package com.bwdz.util;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import java.util.List;
/**
* bean 转换工具
* 1. 工具类,要考虑全面,做到通用,如果不通用,或者只是再某一个package下使用。可以放入对应下面,而不是放在这里
* 2. 接口定义要尽量好用,站在调用方考虑。
* 3. 尽量确保测试覆盖到所有路径
* Created by chenyao on 2017/7/25.
*/
public class Converters {
/**
* 转换bean
* @param source 要转换的bean
* @param destClass 结果bean class类型
* @return
*/
public static D convert(S source, Class destClass) {
if(source == null) {
return null;
}
D dest;
try {
dest = BeanUtils.instantiate(destClass);
BeanUtils.copyProperties(source, dest, destClass);
} catch (Exception e) {
throw new RuntimeException("convert bean error");
}
return dest;
}
/**
* source bean List convert to dest bean list
* @param sourceList 要转换bean的集合
* @param destClass 目标bean class类型
* @return
*/
public static List covertList(List sourceList, Class destClass) {
List list = Lists.newArrayList();
if(sourceList == null || sourceList.isEmpty()) {
return list;
}
for(S source : sourceList) {
list.add(convert(source, destClass));
}
return list;
//return sourceList.parallelStream().map(s -> convert(s, destClass)).collect(Collectors.toList());
}
/**
* Returns a string containing the string representation of each of {@code parts}, using the
* previously configured separator between each.
* @param iterables 可遍历集合
* @param separator 分隔符
* @return
*/
public static String convertToString(Iterable> iterables, String separator) {
if(iterables == null) {
return StringUtils.EMPTY;
}
return Joiner.on(separator).skipNulls().join(iterables);
}
}
- 工具类,要考虑全面,做到通用,如果不通用,或者只是再某一个package下使用。可以放入对应下面,而不是放在这里
- 接口定义要尽量好用,站在调用方考虑。
- 尽量确保测试覆盖到所有路径
- 不要重复造轮子,可以先找找有没有类库实现该功能。比如guava, spring-core, commons, joda-time,jdk类库
- 从网上找的util不要直接拿来使用,要进行改进,确保好用且通用。
二、强制
编程规约
类名使用 UpperCamelCase 风格,必须遵从驼峰形式,但以下情形例外: DO / BO / DTO / VO / AO;
方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格,必须遵从驼峰形式,exp:
localValue / getHttpMessage() / inputUserId
;代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式和中文缩写方式;
说明:正确的英文拼写和语法可以让阅读者易于理解,避免歧义。即使纯拼音命名方式,也要避免(注:alibaba / taobao / youku / hangzhou 等国际通用的名称, 可视同英文); 拼音缩写方式,更要避免。常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长;
exp: MAX_STOCK_COUNT不允许任何魔法值(即未经定义的常量) 直接出现在代码中;
代码格式,使用eclipse idea 默认格式
不能使用过时的类或方法;
线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题;SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为
static,必须加锁,或者使用 DateUtils 工具类;
注意线程安全,使用 DateUtils。亦推荐如下处理:
private static final ThreadLocal df = new ThreadLocal(){
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
- 并发修改同一记录时,避免更新丢失, 需要加锁。 要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据;
如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次;
异常
- 捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
- 使用流情况确保关闭,可以使用 jdk7提供的try resource结构,或者 使用try finally结构,使用IOUtils 下面关闭流方法。
日志规范
- 面向接口编程,使用slf4j 类库中定义的接口,方便后期替换实现,而无需改动代码
exp:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(JlfWxdSmkpController.class);
- 日志级别要根据情况打印,重要业务路径用
info
级别,错误日志用error
级别,错误但不影响功能只是提醒用warn
- 需要打印参数信息,使用占位符方式,而不是拼接方式。exp:
logger.info("param={},param2={}", param, param2);
//e 为exception, 在参数后面抛出
logger.error("param={}", param, e);
数据库
表名、字段名必须使用小写字母或数字, 禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。
说明: MySQL 在 Windows 下不区分大小写,但在 Linux 下默认是区分大小写。因此,数据库名、表名、字段名,都不允许出现任何大写字母,避免节外生枝。
正例:aliyun_admin, rdc_config, level3_name
反例:AliyunAdmin, rdcConfig, level_3_name
主键索引名为 pk_字段名; 唯一索引名为 uk_字段名; 普通索引名则为 idx_字段名;
小数类型为 decimal,禁止使用 float 和 double。
说明: float 和 double 在存储的时候,存在精度损失的问题,很可能在值的比较时,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数分开存储。如果修改字段含义或对字段表示的状态追加时,需要及时更新字段注释;
字段允许适当冗余,以提高查询性能,但必须考虑数据一致。冗余字段应遵循:
1) 不是频繁修改的字段。
2) 不是 varchar 超长字段,更不能是 text 字段。
正例: 商品类目名称使用频率高, 字段长度短,名称基本一成不变, 可在相关联的表中冗余存储类目名称,避免关联查询超过三个表禁止 join。需要 join 的字段,数据类型必须绝对一致; 多表关联查询时,保证被关联的字段需要有索引。
说明: 即使双表 join 也要注意表索引、 SQL 性能不得使用外键与级联,一切外键概念必须在应用层解决;
说明:以学生和成绩的关系为例,学生表中的 student_id 是主键,那么成绩表中的 student_id则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新, 即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群; 级联更新是强阻塞,存在数据库更新风暴的风险; 外键影响数据库的插入速度。禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。
不允许直接拿 HashMap 与 Hashtable 作为查询结果集的输出。
说明: resultClass=”Hashtable”, 会置入字段名和属性值,但是值的类型不可控;表必备三字段: id, gmt_create, gmt_modified
三、建议规范
避免出现重复的代码(Don’t Repeat Yourself) ,即 DRY 原则。
随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。各层命名规约:
A) Service/DAO 层方法命名规约
1) 获取单个对象的方法用 get 做前缀。
2) 获取多个对象的方法用 list 做前缀。
3) 获取统计值的方法用 count 做前缀。
4) 插入的方法用 save/insert 做前缀。
5) 删除的方法用 remove/delete 做前缀。
6) 修改的方法用 update 做前缀。
B) 领域模型命名规约
1) 数据对象: xxxDO, xxx 即为数据表名。
2) 数据传输对象: xxxDTO, xxx 为业务领域相关的名称。
3) 展示对象: xxxVO, xxx 一般为网页名称。
4) POJO 是 DO/DTO/BO/VO 的统称,禁止命名成 xxxPOJO。表达异常的分支时, 少用 if-else 方式, 这种方式可以改写成:
if (condition) {
...
return obj;
}
说明:超过 3 层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现;
- 除常用方法(如 getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。
正例:
// 伪代码如下
final boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
if (existed) {
...
}
反例:
if ((file.open(fileName, "w") != null) && (...) || (...)) {
...
}
谨慎注释掉代码。 在上方详细说明,而不是简单地注释掉。 如果无用,则删除;
说明: 代码被注释掉有两种可能性: 1) 后续会恢复此段代码逻辑。 2) 永久不用。前者如果没有备注信息,难以知晓注释动机。后者建议直接删掉(代码仓库保存了历史代码) 。对于注释的要求:第一、能够准确反应设计思想和代码逻辑; 第二、能够描述业务含义,使别的程序员能够迅速了解到代码背后的信息。完全没有注释的大段代码对于阅读者形同天书,注释是给自己看的,即使隔很长时间,也能清晰理解当时的思路; 注释也是给继任者看的,使其能够快速接替自己的工作。
好的命名、代码结构是自解释的,注释力求精简准确、表达到位。避免出现注释的
一个极端:过多过滥的注释,代码的逻辑一旦修改,修改注释是相当大的负担。及时清理不再使用的代码段或配置信息。
说明: 对于垃圾代码或过时配置,坚决清理干净,避免程序过度臃肿,代码冗余。
正例: 对于暂时被注释掉,后续可能恢复使用的代码片断,在注释代码上方,统一规定使用三个斜杠(///)来说明注释掉代码的理由。-
图中默认上层依赖于下层,箭头关系表示可直接依赖,如:开放接口层可以依赖于
Web 层,也可以直接依赖于 Service 层
提高代码质量书籍
- java编码规范-阿里巴巴
- 代码整洁之道
- 编写可读代码的艺术
- 代码大全(第二版)
- 重构 改善既有代码的设计
- effective java 第二版