开发规范拟定--初版

来源:互联网 发布:寻求网络黑客高手帮忙 编辑:程序博客网 时间:2024/05/01 02:05

介绍

好的开发规范不仅能够使得项目变得易维护,易升级。一些通用的规范可以参考《阿里巴巴java开发手册》
本文档主要针对我们现在使用的框架提出一些开发规范,欢迎补充

包结构规范

以短信邮件项目(mail-sms)为例,介绍包结构命名规范。
项目目录结构
短信邮件项目主要包含短信,邮件两个子模块

【强制】 包分层–通用
一般每个项目都包含下面六个模块,还有一些各自扩展的模块
1. api #api接口定义,用于暴露服务
2. api-impl #api接口实现
3. app #应用
4. admin #后台页面
5. web #前台页面
6. model #实体
关于项目结构的介绍可以参考《项目结构说明.md》,他们的包分层应当统一

api:sinosoftgz.message.apiapi-impl:sinosoftgz.message.apiapp:sinosoftgz.message.appadmin:sinosoftgz.message.adminmodel: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.ymlmotan:  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.ymlmotan:  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";//数据    }    ...}
原创粉丝点击