开发规范拟定--初版

7 篇文章 0 订阅

介绍

好的开发规范不仅能够使得项目变得易维护,易升级。一些通用的规范可以参考《阿里巴巴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规范

【强制】条件查询超过三个参数的,使用criteriaQuerypredicates 而不能使用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";//数据
    }
    ...
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值