介绍
好的开发规范不仅能够使得项目变得易维护,易升级。一些通用的规范可以参考《阿里巴巴java开发手册》
本文档主要针对我们现在使用的框架提出一些开发规范,欢迎补充
包结构规范
以短信邮件项目(mail-sms)为例,介绍包结构命名规范。
短信邮件项目主要包含短信,邮件两个子模块
【强制】 包分层–通用
一般每个项目都包含下面六个模块,还有一些各自扩展的模块
1. api #api接口定义,用于暴露服务
2. api-impl #api接口实现
3. app #应用
4. admin #后台页面
5. web #前台页面
6. model #实体
关于项目结构的介绍可以参考《项目结构说明.md》,他们的包分层应当统一
api:
sinosoftgz.message.api
api-impl:
sinosoftgz.message.api
app:
sinosoftgz.message.app
admin:
sinosoftgz.message.admin
model:
sinosoftgz.message.model
格式如下:公司名.模块名.层次名
包名应当尽量使用能够概括模块总体含义,单词义,单数,不包含特殊字符的单词
【正例】: sinosoftgz.message.admin
【反例】: sinosoftgz.mailsms.admin
sinosoftgz.mail.sms.admin
【推荐】包分层–业务
当项目模块的职责较为复杂,且考虑到以后拓展的情况下,单个模块依旧包含着很多小的业务模块时,应当优先按照业务区分包名
【正例】:
sinosoftgz.message.admin
config
模块公用Config.java
service
模块公用Service.java
web
模块公用Controller.java
IndexController.java
mail
service
Mail私有Service.java
MailTemplateService.java
MailMessageService.java
web
Mail私有Controller.java
MailTemplateController.java
MailMessageController.java
sms
service
Sms私有Service.java
SmsTemplateService.java
SmsMessageService.java
web
Sms私有Controller.java
SmsTemplateController.java
SmsMessageController.java
MailSmsAdminApp.java
【反例】:
sinosoftgz.message.admin
config
模块公用Config.java
service
模块公用Service.java
mail
Mail私有Service.java
MailTemplateService.java
MailMessageService.java
sms
Sms私有Service.java
SmsTemplateService.java
SmsMessageService.java
web
模块公用Controller.java
IndexController.java
mail
Mail私有Controller.java
MailTemplateController.java
MailMessageController.java
sms
Sms私有Controller.java
SmsTemplateController.java
SmsMessageController.java
MailSmsAdminApp.java
service和controller以及其他业务模块相关的包相隔太远,或者干脆全部丢到一个包内,单纯用前缀区分,会形成臃肿,充血的包结构。如果是项目结构较为单一,可以仅仅使用前缀区分;如果是项目中业务模块有明显的区分条件,应当单独作为一个包,用包名代表业务模块的含义。
数据库规范
【强制】必要的地方必须添加索引,如唯一索引,以及作为条件查询的列
【强制】生产环境,uat环境,不允许使用jpa.hibernate.ddl-auto: create
自动建表,每次ddl的修改需要保留脚本,统一管理
【强制】业务数据不能使用deleteBy…而要使用逻辑删除setDelete(true),查询时,findByxxxAndisDelete(xxx,false)
ORM规范
【强制】条件查询超过三个参数的,使用criteriaQuery
,predicates
而不能使用springdata的findBy
【正例】
public Page<MailTemplateConfig> findAll(MailTemplateConfig mailTemplateConfig, Pageable pageable) {
Specification querySpecification = (Specification<MailTemplateConfig>) (root, criteriaQuery, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
predicates.add(criteriaBuilder.isFalse(root.get("isDelete")));
//级联查询mailTemplate
if (!Lang.isEmpty(mailTemplateConfig.getMailTemplate())) {
//短信模板名称
if (!Lang.isEmpty(mailTemplateConfig.getMailTemplate().getTemplateName())) {
predicates.add(criteriaBuilder.like(root.join("mailTemplate").get("templateName"), String.format("%%%s%%", mailTemplateConfig.getMailTemplate().getTemplateName())));
}
//短信模板类型
if (!Lang.isEmpty(mailTemplateConfig.getMailTemplate().getTemplateType())) {
predicates.add(criteriaBuilder.equal(root.join("mailTemplate").get("templateType"), mailTemplateConfig.getMailTemplate().getTemplateType()));
}
}
//产品分类
if (!Lang.isEmpty(mailTemplateConfig.getProductType())) {
predicates.add(criteriaBuilder.equal(root.get("productType"), mailTemplateConfig.getProductType()));
}
//客户类型
if (!Lang.isEmpty(mailTemplateConfig.getConsumerType())) {
predicates.add(criteriaBuilder.equal(root.get("consumerType"), mailTemplateConfig.getConsumerType()));
}
return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
};
return mailTemplateConfigRepos.findAll(querySpecification, pageable);
}
【说明】条件查询是admin模块不可避免的一个业务功能,使用criteriaQuery
可以轻松的添加条件,使得代码容易维护,他也可以进行分页,排序,连表操作,充分发挥jpa面向对象的特性,使得业务开发变得快捷。
【反例】
public Page<GatewayApiDefine> findAll(GatewayApiDefine gatewayApiDefine,Pageable pageable){
if(Lang.isEmpty(gatewayApiDefine.getRole())){
gatewayApiDefine.setRole("");
}
if(Lang.isEmpty(gatewayApiDefine.getApiName())){
gatewayApiDefine.setApiName("");
}
if(Lang.isEmpty(gatewayApiDefine.getEnabled())){
return gatewayApiDefineDao.findByRoleLikeAndApiNameLikeOrderByLastUpdatedDesc("%"+gatewayApiDefine.getRole()+"%","%"+gatewayApiDefine.getApiName()+"%",pageable);
}else{
return gatewayApiDefineDao.findByRoleLikeAndApiNameLikeAndEnabledOrderByLastUpdatedDesc("%"+gatewayApiDefine.getRole()+"%","%"+gatewayApiDefine.getApiName()+"%",gatewayApiDefine.getEnabled(),pageable);
}
}
【说明】在Dao层定义了大量的findBy方法,在Service写了过多的if else判断,导致业务逻辑不清晰
禁止使用魔鬼数字
【模型层与业务层】
一些固定业务含义的代码可以使用枚举类型,或者final static常量表示,在设值时,不能直接使用不具备业务含义的数值。
【正例】:使用final static常量:
//实体类定义
/**
* 发送设置标志
*
* @see sendFlag
*/
public final static String SEND_FLAG_NOW = "1"; //立即发送
public final static String SEND_FLAG_DELAY = "2"; //预设时间发送
/**
* 发送成功标志
*
* @see sendSuccessFlag
*/
public final static Map<String, String> SEND_SUCCESS_FLAG_MAP = new LinkedHashMap<>();
public final static String SEND_WAIT = "0";
public final static String SEND_SUCCESS = "1";
public final static String SEND_FAIL = "2";
static {
SEND_SUCCESS_FLAG_MAP.put(SEND_WAIT, "未发送");
SEND_SUCCESS_FLAG_MAP.put(SEND_SUCCESS, "发送成功");
SEND_SUCCESS_FLAG_MAP.put(SEND_FAIL, "发送失败");
}
/**
* 发送设置标志 (1:立即发送 2:预设时间发送 )
*/
@Column(columnDefinition = "varchar(1) comment '发送设置标志'")
protected String sendFlag;
//业务代码赋值使用
MailMessage mailMessage = new MailMessage();
mailMessage.setSendSuccessFlag(MailMessage.SEND_WAIT);
mailMessage.setValidStatus(MailMessage.VALID_WAIT);
mailMessage.setCustom(true);
【反例】
//实体类定义
/**
* 发送设置标志 (1:立即发送 2:预设时间发送 )
*/
@Column(columnDefinition = "varchar(1) comment '发送设置标志'")
protected String sendFlag;
//业务代码赋值使用
MailMessage mailMessage = new MailMessage();
mailMessage.setSendSuccessFlag("1");
mailMessage.setValidStatus("0");
mailMessage.setCustom(true);
【说明】魔鬼数字不能使代码一眼能够看明白到底赋的是什么值,并且,实体类发生变化后,可能会导致赋值错误,与预期赋值不符合且错误不容易被发现。
【正例】:也可以使用枚举类型避免魔鬼数字
protected String productType;
protected String productName;
@Enumerated(EnumType.STRING)
protected ConsumerTypeEnum consumerType;
@Enumerated(EnumType.STRING)
protected PolicyTypeEnum policyType;
@Enumerated(EnumType.STRING)
protected ReceiverEnum receiver;
public enum ConsumerTypeEnum {
PERSONAL, ORGANIZATION;
public String getLabel() {
switch (this) {
case PERSONAL:
return "个人";
case ORGANIZATION:
return "团体";
default:
return "";
}
}
}
【视图层】
例如,页面迭代select的option,不应该在view层判断,而应该在后台传入map在前台迭代
【正例】:
model.put("typeMap",typeMap);
模板类型:<select type="text" name="templateType">
<option value="">全部</option>
<#list typeMap?keys as key>
<option <#if ((mailTemplate.templateType!"")==key)>selected="selected"</#if>value="${key}">${typeMap[key]}</option>
</#list>
</select>
【反例】:
模板类型:<select type="text" name="templateType">
<option value="">全部</option>
<option <#if ${xxx.templateType!}=="1"
selected="selected"</#if> value="1">承保通知</option>
...
<option <#if ${xxx.templateType!}=="5"
selected="selected"</#if> value="5">核保通知</option>
</select>
【说明】:否则修改后台代码后,前端页面也要修改,设计模式的原则,应当是修改一处,其他全部变化。且 1,2…,5的含义可能会变化,不能从页面得知value和option的含义是否对应。
并发注意事项
项目中会出现很多并发问题,要做到根据业务选择合适的并发解决方案,避免线程安全问题
【强制】simpleDateFormat有并发问题,不能作为static类变量
【反例】:
这是我在某个项目模块中,发现的一段代码
Class XxxController{
public final static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
@RequestMapping("/xxxx")
public String xxxx(String dateStr){
XxxEntity xxxEntity = new XxxEntity();
xxxEntity.setDate(simpleDateFormat.parse(dateStr));
xxxDao.save(xxxEntity);
return "xxx";
}
}
【说明】SimpleDateFormat 是线程不安全的类,不能作为静态类变量给多线程并发访问。如果不了解多线程,可以将其作为实例变量,每次使用时都new一个出来使用。不过更推荐使用ThreadLocal来维护,减少new的开销。
【正例】一个使用ThreadLocal维护SimpleDateFormat的线程安全的日期转换类:
public class ConcurrentDateUtil {
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static Date parse(String dateStr) throws ParseException {
return threadLocal.get().parse(dateStr);
}
public static String format(Date date) {
return threadLocal.get().format(date);
}
}
【推荐】名称唯一性校验出现的线程安全问题
各个项目的admin模块在需求中经常会出现要求名称不能重复,即唯一性问题。通常在前台做ajax校验,后台使用select count(1) from table_name where name=?
的方式查询数据库。这么做无可厚非,但是在极端的情况下,会出现并发问题。两个线程同时插入一条相同的name,如果没有做并发控制,会导致出现脏数据。如果仅仅是后台系统,那么没有必要加锁去避免,只需要对数据库加上唯一索引,并且再web层或者service层捕获数据异常即可。
【正例】:
//实体类添加唯一索引
@Entity
@Table(name = "mns_mail_template",
uniqueConstraints = {@UniqueConstraint(columnNames = {"templateName"})}
)
public class MailTemplate extends AbstractTemplate {
/**
* 模板名称
*/
@Column(columnDefinition = "varchar(160) comment '模板名称'")
private String templateName;
}
//业务代码捕获异常
@RequestMapping(value = {"/saveOrUpdate"}, method = RequestMethod.POST)
@ResponseBody
public AjaxResponseVo saveOrUpdate(MailTemplate mailTemplate) {
AjaxResponseVo ajaxResponseVo = new AjaxResponseVo(AjaxResponseVo.STATUS_CODE_SUCCESS, "操作成功", "邮件模板定义", AjaxResponseVo.CALLBACK_TYPE_CLOSE_CURRENT);
try {
//管理端新增时初始化一些数据
if (Lang.isEmpty(mailTemplate.getId())) {
mailTemplate.setValidStatus(MailTemplate.VALID_WAIT);
}
mailTemplateService.save(mailTemplate);
} catch (DataIntegrityViolationException ce) {
ajaxResponseVo.setStatusCode(AjaxResponseVo.STATUS_CODE_ERROR);
ajaxResponseVo.setMessage("模板名称已经存在");
ajaxResponseVo.setCallbackType(null);
logger.error(ce.getMessage());
} catch (Exception e) {
ajaxResponseVo.setStatusCode(AjaxResponseVo.STATUS_CODE_ERROR);
ajaxResponseVo.setMessage("操作失败!");
ajaxResponseVo.setCallbackType(null);
logger.error(e.getMessage(), e);
}
return ajaxResponseVo;
}
【说明】关于其他一些并发问题,不仅仅是一篇文档能够讲解清楚的,需要对开发有很深的理解,我还记录了一些并发问题,仅供参考:http://blog.csdn.net/u013815546/article/details/56481842
moton使用注意事项
【注意】包的扫描
每个模块都要扫描自身的项目结构
mail-sms-admin:application.yml
motan:
client-group: sinosoftrpc
client-access-log: false
server-group: sinosoftrpc
server-access-log: false
export-port: ${random.int[9001,9999]}
zookeeper-host: 127.0.0.1:2181
annotaiong-package: sinosoftgz.message.admin
app模块由于将api-impl脱离出了自身的模块,通常还需要扫描api-impl的模块
//配置pom依赖 pom.xml
<dependency>
<groupId>sinosoftgz</groupId>
<artifactId>mail-sms-api-impl</artifactId>
</dependency>
//配置spring ioc扫描 AutoImportConfig.java
@ComponentScans({
@ComponentScan(basePackages = {"sinosoftgz.message.app", "sinosoftgz.message.api"})
})
//配置motan扫描 mail-sms-app:application.yml
motan:
annotaiong-package: sinosoftgz.message.app,sinosoftgz.message.api
client-group: sinosoftrpc
client-access-log: true
server-group: sinosoftrpc
server-access-log: true
export-port: ${random.int[9001,9999]}
zookeeper-host: localhost:2181
【注意】motan跨模块传输实体类时懒加载失效
遇到的时候注意一下,由于jpa,hibernate懒加载的问题,因为其内部使用动态代理去实现的懒加载,导致懒加载对象无法被正确的跨模块传输,此时需要进行深拷贝。
【正例】:
/**
* 深拷贝OrderMain对象,主要用于防止Hibernate序列化懒加载Session关闭问题
* <p/>
* // * @param order
*
* @return
*/
public OrderMain cpyOrder(OrderMain from, OrderMain to) {
OrderMain orderMainNew = to == null ? new OrderMain() : to;
Copys copys = Copys.create();
List<OrderItem> orderItemList = new ArrayList<>();
List<SubOrder> subOrders = new ArrayList<>();
List<OrderGift> orderGifts = new ArrayList<>();
List<OrderMainAttr> orderMainAttrs = new ArrayList<>();
OrderItem orderItemTmp;
SubOrder subOrderTmp;
OrderGift orderGiftTmp;
OrderMainAttr orderMainAttrTmp;
copys.from(from).excludes("orderItems", "subOrders", "orderGifts", "orderAttrs").to(orderMainNew).clear();
if (!Lang.isEmpty(from.getOrderItems())) {
for (OrderItem i : from.getOrderItems()) {
orderItemTmp = new OrderItem();
copys.from(i).excludes("order").to(orderItemTmp).clear();
orderItemTmp.setOrder(orderMainNew);
orderItemList.add(orderItemTmp);
}
orderMainNew.setOrderItems(orderItemList);
}
SubOrderItem subOrderItem;
List<SubOrderItem> subOrderItemList = new ArrayList<>();
if (from.getSubOrders() != null) {
for (SubOrder s : from.getSubOrders()) {
subOrderTmp = new SubOrder();
copys.from(s).excludes("order", "subOrderItems").to(subOrderTmp).clear();
subOrderTmp.setOrder(from);
for (SubOrderItem soi : s.getSubOrderItems()) {
subOrderItem = new SubOrderItem();
copys.from(soi).excludes("order", "subOrder", "orderItem").to(subOrderItem).clear();
subOrderItem.setOrder(orderMainNew);
subOrderItem.setSubOrder(subOrderTmp);
subOrderItemList.add(subOrderItem);
if (!Lang.isEmpty(soi.getOrderItem())) {
for (OrderItem i : orderMainNew.getOrderItems()) {
if (i.getId().equals(soi.getOrderItem().getId())) {
subOrderItem.setOrderItem(soi.getOrderItem());
} else {
subOrderItem.setOrderItem(soi.getOrderItem());
}
}
}
}
subOrderTmp.setSubOrderItems(subOrderItemList);
subOrders.add(subOrderTmp);
}
orderMainNew.setSubOrders(subOrders);
}
if (from.getOrderGifts() != null) {
for (OrderGift og : from.getOrderGifts()) {
orderGiftTmp = new OrderGift();
copys.from(og).excludes("order").to(orderGiftTmp).clear();
orderGiftTmp.setOrder(orderMainNew);
orderGifts.add(orderGiftTmp);
}
orderMainNew.setOrderGifts(orderGifts);
}
if (from.getOrderAttrs() != null) {
for (OrderMainAttr attr : from.getOrderAttrs()) {
orderMainAttrTmp = new OrderMainAttr();
copys.from(attr).excludes("order").to(orderMainAttrTmp).clear();
orderMainAttrTmp.setOrder(orderMainNew);
orderMainAttrs.add(orderMainAttrTmp);
}
orderMainNew.setOrderAttrs(orderMainAttrs);
}
return orderMainNew;
}
公用常量规范
【强制】模块常量
模块自身公用的常量放置于模块的Constants 类中,以final static的方式声明
public class Constants {
public static final String BUSINESS_PERFIX_PATH = "/mail-sms-app";
}
【强制】项目常量
项目公用的常量放置于util模块的GlobalContants类中,以静态内部类和final static的方式声明
public abstract class GlobalContants {
/**
* 返回的状态
*/
public class ResponseStatus{
public static final String SUCCESS = "success";//成功
public static final String ERROR = "error";//错误
}
/**
* 响应状态
*/
public class ResponseString{
public static final String STATUS = "status";//状态
public static final String ERROR_CODE = "error";// 错误代码
public static final String MESSAGE = "message";//消息
public static final String DATA = "data";//数据
}
...
}