一、分享目的
1.1 统一代码规范
1.2 提高编程技巧
二、分享内容
2.1 代码规范
2.1.1 包名划分
2.1.2 命名规范
2.1.2.1 领域对象命名
包名 | 命名规范 | 属性规范 | 备注 |
---|---|---|---|
constants | xxxConstants | 根据常量的功能进行划分 | xxx代表功能模块名称,例如:AnswerConstants、SystemConstants等 |
entity | xxxEntity | 所有属性和DB表数据类型一致 | xxx代表表名的驼峰字符串 |
model | xxxModel | 和页面展示用的数据结构一致 | xxx代表页面模块的名称 |
param | xxxParam/TXxxParam | 接口请求参数对象 | T开头代表thrift接口 |
result | xxxResult/TXxxResult | 接口返回参数对象 | T开头代表thrift接口 |
convert | xxxConvert | 对象转换用的工具类,静态方法 | xxx代表需要转换的对象名称 |
2.1.2.2 对外接口命名
接口协议 | HTTP | Thrift |
---|---|---|
命名规则 | XxxxController | XxxxThriftService |
2.1.2.3 接口方法命名
方法内容最好不要超过30行,不然很难看得懂,利用继承、多态等拆分方法,实在不行也可以用私有方法。
方法功能 | 命名规则 | 含义 |
---|---|---|
保存数据 | save(Object entity) | 写入数据到数据库,有id更新,无id插入 |
插入数据 | insert(Object entity) | 插入数据到数据库 |
更新数据 | update(Object entity) | 更新数据库中数据 |
删除数据 | delete(Object entity) | 删除数据库中数据 |
获取单条记录 | get(Long id) | 根据id查询数据库记录,不用写getById(),从参数名我们已经知道含义了 |
查询多条记录(不分页) | list(Object entity) | 根据查询条件查询数据库记录列表,不带分页,动态sql全部涵盖 |
查询多条记录(分页) | find(Object entity) | 根据查询条件查询数据库总记录数,同时查询分页内的记录,最后组合返回 |
2.1.2.4 配置文件命名
采用spring-boot构建项目,统一配置文件名称:
目录 | 名称 | 含义 |
---|---|---|
/resources | application.properties | springboot的默认配置文件,公共的配置放在这里 |
/resources/config | application-prod.properties application-test.properties | springboot区分环境的配置文件,不同环境的配置放在这里,启动项目时指定-Dspring.profiles.active=[prod|test] |
2.1.3 接口规范
2.1.3.1 HTTP接口
统一返回值类型,内容如下:
代码块
Java
public class HttpBaseResult {
private Integer code;//统一HTTP协议code码,例如:200为成功,500为服务端错误,400为客户端错误
private String message; //错误提示信息,例如:请求参数xxx不能为空等
private Object data; //具体数据
}
2.1.3.2 Thrift接口
统一RPC接口返回值类型,如下:
代码块
Java
enum TBaseResultCode {
SUCCESS = 200, //成功
CLIENT_ERROR = 400, //客户端异常
SERVER_ERROR = 500 //服务端异常
}
//以下为接口的内容
include "TBaseResult.thrift"
// 意图说法数据
struct TIntentGrammarResult {
1: optional TBaseResult.TBaseResultCode code; //接口返回状态码
2: optional string message; //接口返回消息
3: optional string grammar; //接口返回数据
}
2.1.3.3 分页接口
代码块
Java
//请求参数
@Data
public class PagerParam {
private Integer page = 1; //当前第几页
private Integer pageSize = 20; //每页多少行
private String sort; //排序字段
private String order = "asc"; //排序规则 asc || desc
private Integer pageIndex; //分页索引位置,根据page和pageSize计算出来的,前端不用传
}
//返回结果
@Data
public class PagerResult<T> {
private Integer page; //当前第几页
private Integer pageSize; //每页多少行
private Integer totalPage; //总共多少页
private String sort; //排序字段
private String order; //排序规则
private Long total; //总共多少条记录
private List<T> records; //当前页数据
}
//帮助类
public class PagerResultHelper { /**
* 获取分页的起始索引位置
*
* @param page
* @param pageSize
* @return
*/
public static Integer getPageIndex(Integer page, Integer pageSize) {
return (page - 1) * pageSize;
}
/**
* 获取总页数
*
* @param total
* @param pageSize
* @return
*/
private static Integer getTotalPage(Long total, Integer pageSize) {
return total == 0L ? 0 : (int) ((total - 1) / pageSize + 1);
}
/**
* 转换查询结果为分页查询结果
*
* @param param 分页对象
* @param records 记录行转换后的列表
* @return
*/
public static <T> PagerResult<T> getPageResult(PagerParam param, Long total, List<T> records) {
PagerResult<T> result = new PagerResult<>();
result.setPage(param.getPage());
result.setPageSize(param.getPageSize());
result.setTotal(total);
result.setTotalPage(getTotalPage(total, param.getPageSize()));
result.setSort(param.getSort());
result.setOrder(param.getOrder());
result.setRecords(records);
return result;
}
}
2.2 高级编程
2.2.1 统一接口切面
所有对外暴露的接口,一般都需要打印请求参数列表、返回参数列表、耗时、异常等信息,但是你会在每个方法中都写如下一段代码吗?
代码块
Java
public HttpBaseResult save(@RequestBody IntentAnswerParam param) {
try {
LOGGER.info("当前方法是:{},请求参数为:{}", "saveIntent", param.toString());
Long id = intentAnswerService.save(param);
HttpBaseResult result = HttpResultHelper.buildSuccess(id);
LOGGER.info("当前方法是:{},请求参数为:{},返回结果为:{}", "saveIntent", param.toString(), result.toString());
return result;
} catch (Exception e) {
LOGGER.error("当前方法是:{},请求参数为:{},异常参数为:{}", "saveIntent", param.toString(), e);
return HttpResultHelper.buildFailure(HttpCodeEnum.SERVER_ERROR);
}
}
当然不是,学习过面向切面编程的同学肯定会想到的是用切面统一处理,具体代码如下:
代码块
Java
private static final Logger LOGGER = LoggerFactory.getLogger(DialogPortalAspect.class);
private static final String LOG_NOT_PARAM_TEMPLATE = "当前类:{},当前方法:{},返回结果:{},耗时:{}ms";
private static final String LOG_THROWABLE_TEMPLATE = "当前类:{},当前方法:{},请求参数:{},异常:{}";
private static final String LOG_ALL_TEMPLATE = "当前类:{},当前方法:{},请求参数:{},返回结果:{},耗时:{}ms";
/**
* 统一使用该环绕通知切面来处理API接口的请求参数日志、返回结果日志、耗时日志、异常处理等
*
* @param point 切点
* @return 接口返回的实际数据类型
2.2.2 使用设计模式
2.2.2.1 单例模式
使用了spring之后,单例、工厂模式貌似变成了理所当然的事情,可是一旦脱离了spring,还是要考虑在合适的地方使用单例。
代码块
Java
/**
* 解析XML、转换XML、生成XML的代理对象
*/
public class LoadXmlDelegate {
private static final Logger LOGGER = LoggerFactory.getLogger(LoadXmlDelegate.class);
private static LoadXmlDelegate delegate = null;
private LoadXmlDelegate() {
}
//简单的一个单例
public static synchronized LoadXmlDelegate getInstance() {
if (delegate == null) {
delegate = new LoadXmlDelegate();
}
return delegate;
}
}
2.2.2.2 策略模式
假如你的代码接入方有两个业务方,内部逻辑不同,但是接口方法相同,你还会用if-else区分代码吗?为什么不用策略模式区分呢?
代码块
Java
/**
* 转换BPMN为OpenDial的格式
*
* @param process
* @param sourceType
* @param configs
*/
public Document handle(BpmnProcessDomain process, BpmnSourceEnum sourceType, String configs) {
Document document = this.writeDocument();
//策略模式
AbstractBpmnHandler handler;
if (sourceType == BpmnSourceEnum.CSC) {
handler = new CscBpmnBaseHandler(document, process, configs);
} else if (sourceType == BpmnSourceEnum.APP) {
handler = new CscBaseHandler(document, process, configs);
} else {
handler = new MosesBpmnBaseHandler(document, process, configs);
}
handler.init();
handler.buildTree(process.getStartEvent());
handler.handle(process.getStartEvent());
return document;
}
2.2.2.3 模板方法模式
总感觉有段代码像一个模板,一段逻辑在控制:先干什么,在干什么,最后干什么,但是每一段的具体逻辑又不同,怎么办呢?
代码块
Java
/**
* 处理某个节点
*
* @param node
* @return
*/
default Optional<List<RuleModel>> buildRule(TreeNodeModel node, ParseContextModel context) {
List<RuleModel> rule;
if (node instanceof StartEventModel) {
rule = this.buildStartEventRule((StartEventModel) node);
} else if (CollectionUtils.isEmpty(node.getChildren())) {
RuleModel endEventRule = this.buildEndRule((BpmnComponentModel) node, context);
rule = endEventRule == null ? new ArrayList<>() : Collections.singletonList(endEventRule);
} else if (node instanceof ExclusiveGatewayModel) {
rule = this.buildGatewayRule((ExclusiveGatewayModel) node, context);
} else {
RuleModel callActivityRule = this.buildCallActivityRule((CallActivityModel) node, context);
rule = callActivityRule == null ? new ArrayList<>() : Lists.newArrayList(callActivityRule);
}
return Optional.ofNullable(rule);
}
List<RuleModel> buildStartEventRule(StartEventModel node);
RuleModel buildEndRule(BpmnComponentModel node, ParseContextModel context);
List<RuleModel> buildGatewayRule(ExclusiveGatewayModel node, ParseContextModel context);
RuleModel buildCallActivityRule(CallActivityModel node, ParseContextModel context);
2.2.2.4 责任链模式
有没有经历过一段代码,是先调接口A,再调接口B,再调接口C,等等,像是一个链路,你还在一个方法里面写吗?你不觉得方法片段很长吗?
代码块
Java
/**
* 用责任链模式解耦代码
* 请不要全部写在一个方法中!!!!!!!
* Created by kangxiongwei on 2019-04-15 17:17.
*/
public abstract class AbstractDmTaskService {
//当前类的下一级处理器
protected AbstractDmTaskService handler;
/**
* 执行下一级任务
*
* @param params 请求参数
*/
public abstract void doTask(Object params);
}
public class DmTaskVersionServiceImpl extends AbstractDmTaskService implements DmTaskVersionService {
/**
* 更新TaskInfo的Trigger信息
*/
@Override
public void doTask(Object param) {
this.handler = dmTaskInfoService; //设置下一级责任链
DmTaskVersionParam version = (DmTaskVersionParam) param;
Long taskId = dmTaskVersionMapper.getTaskId(version.getId());
DmTaskInfoEntity task = new DmTaskInfoEntity();
task.setId(taskId);
task.setTaskTrigger(version.getTrigger() == null ? null : JSON.toJSONString(version.getTrigger()));
task.setUpdateTime(new Date());
User user = UserUtils.getUser();
task.setModifier(user == null ? SystemConstants.SYSTEM : user.getLogin());
dmTaskInfoMapper.updateByPrimaryKeySelective(task);
if (this.handler == null) return;
handler.doTask(task); //下一级责任链开始处理逻辑
}
}
2.2.2.5 其他设计模式
以上是我简单举得几个例子,个人认为首先需要掌握常用的设计模式的应用场景,然后在编码过程中多寻思自己的代码写的好不好,哪里还有优化空间?是否需要某种设计模式来解决?以下为GoF提出的23种设计模式:
其中个人认为最常用的设计模式有以下几个:
单例、工厂、适配器、代理、责任链、观察者、策略、模板方法
各种设计模式的实现在网站上很容易找到,我的github上也有一部分实现:JavaInterview/JavaInterview/src/main/java/com/kxw/pattern at master · kangxiongwei/JavaInterview · GitHub),这里不再详细介绍。
2.2.3 DB操作
对于数据库操作频繁的SQL,一定要做好SQL优化,SQL优化分为两步:1.定位问题 2.优化问题
2.2.3.1 定位问题
sql | 作用 | 备注 |
show [session|global] status [like 'Com_%']; | 查看sql执行频率,默认为session | 关注:Com_select,Com_update,Com_insert,Com_delete,Innodb_rows_read, Innodb_rows_inserted, Innodb_rows_updated, Innodb_rows_deletedConnections, Uptime, Slow_queries |
启动mysql用--log-slow-queries [=file_name] | 指定慢查询日志,根据日志定位执行效率较慢的sql | |
show [full] processlist | 查看mysql进程,查看是否锁表等 | |
explain select * from a; explain partitions ; show warnings; | 根据执行计划查看效率 select_type: simple、primary、union、subquery type: [all|index|range|ref|eq_ref|const|system|null]性能由差到好 explain extended: 可以看到mysql在执行sql前,对sql做了哪些优化 | type=all:全表扫描 type=index:索引全扫描 type=range:索引范围扫描 type=ref:使用非唯一索引扫描或唯一索引的前缀扫描 type=eq_ref:类似于ref,但是索引为唯一索引,多表中唯一key所为关联条件 type=const/system:单表中最多一个匹配行,主键ID或者唯一索引查询 type=null:不用访问表就能得到结果 |
show profile[s] show profile for query 4; //查看各状态消耗时间 select @@have_profiling; //查看是否支持profile select @@profiling; | 通过profile可以知道sql执行耗时主要消耗到了哪里 | 开启profile:set profiling = 1; 其他用法: show profile cpu for query 4; show profile source for query 4; //查看源码 |
set optimizer_trace="enabled=on",end_markers_in_json=on; set optimizer_trace_max_mem_size=10000000; select * from aaa; select * from information_schema.optimizer_trace; | 通过跟踪trace,分析优化器为什么选择了A计划而不是B计划 |
2.2.3.2 解决问题
使用索引 | 避免有索引,但是使用不到索引的情况 | a. like左边有%号 b. or语句有一部分用不到索引 c. 复合索引不满足最左原则 d. sql中存在隐式转换 e. 使用索引比全表扫描更慢 |
定期分析、检查、优化表 analyze table aaa; check table aaa; optimize table aaa; | 表优化,会锁表 | |
load data 'aaa.txt' into table aaa; alter table aaa [disable|enable] keys; //只对MyISAM有效 | 大批量插入数据,在前后禁用和启用索引 对于InnoDB,需要用以下方式: 1. 文件按照主键顺序排序 2. 在导入前,set unique_checks = 0; 导入后,再设为1 3. 在导入前,set autocommit = 0; 导入后,再设为1 | |
insert into test values (1, 2), (3,4) ..... | 优化insert语句 | |
order by | 1. where和order by使用相同字段,如果字段有索引 2. 排序字段ASC还是DESC尽量一致 3. 适当增大buffer_sort_size参数 | |
group by | group by后可以指定order by null | |
子查询 | 尽量使用join代替子查询 | |
or | 两个子句必须都有索引,考虑使用union | |
分页查询 | 1. 用带索引子查询返回排序的数据,然后关联表查询 2. 记录上次查询的位置,然后利用位置查询 | 1. select a.id, a.name from aaa a inner join (select id from aaa order by id limit 50,10) b on (a.id = b.id) 2. select a.id, a.name from aaa order by id desc limit 400, 10 //记录此次执行完后最后一行的id号,比如1560 select a.id, a.name from aaa where a.id < 1560 order by id desc limit 410, 10; //利用上次的查询结果查询 |
SQL提示 | select count(*) from aaa use index (idx_id); select count(*) from aaa ingnore index(idx_id); select count(*) from aaa force index(idx_id); | 让数据库不考虑其他索引 |
善于正则表达式,替换like | ||
使用group by with rollup子句 | 分组后不仅可以获取想要的数据,还有其他的数据,如总金额等 和分组字段的顺序有关 | 和limit互斥的 |
2.2.4 异步调用
代码中为了更好地效率,经常需要将有些方法设置为异步调用,你还在用new Thread(........).start()或者线程池中创建线程异步调用吗?其实spring-boot提供了异步方法,如下:
代码块
Java
/**
* 异步任务线程池配置
* Created by kangxiongwei on 2019-01-29 14:43.
*/
@Configuration
@EnableAsync
public class AsyncConfiguration implements AsyncConfigurer {
/**
* The {@link Executor} instance to be used when processing async
* method invocations.
*/
@Override
public Executor getAsyncExecutor() {
int corePoolSize = Runtime.getRuntime().availableProcessors();
int maxPoolSize = 100;
int keepAliveTime = 1;
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(10000);
return new ThreadPoolExecutor(corePoolSize * 2, maxPoolSize, keepAliveTime, TimeUnit.MINUTES, queue);
}
}
/**
* 异步增加操作日志
*/
@Async
@Override
public void insert(ActionLogEntity entity) {
actionLogMapper.insert(entity);
}
2.2.4 单元测试
最简单的单元测试,当属测试一段代码,查看代码是否有异常,人工检测数据库数据是否符合预期
代码块
Java
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) //随机端口,防止和springboot的默认端口冲突
public class DmTaskServiceTest {
@Resource
private DmTaskInfoService dmTaskInfoService;
@Test
public void testListTask() {
List<TTaskInfoModel> list = dmTaskInfoService.list("1bfe8f02-cd0a-48aa-84c6-1dc1a2bab8da");
System.out.println(list);
}
}