一、总结
- Mybatis中当parameterType为基本数据类型的时候,统一采用_parameter来代替基本数据类型变量。
- Mybatis中resultMap返回一个对象,resultType返回一个Map简单数据类型(由于需要缓存到JVM中)的映射关系。
- String类型转Integer类型;String类型转int类型用到的方法是不一样的。
- 方法入口处第一行写new Date(),防止时间在23:59:59跨界对逻辑带来影响。
- 考虑到上线app_resource表忘记配置供应商比例,在代码中逻辑中注意要加入空指针判断,增强代码健壮性。
- 核心代码处要加注释,关键代码处要打日志,业务逻辑执行失败要考虑是否需要告警邮件。
- 变量命名要规范;测试工单的工单标题命名要规范。
- 代码逻辑中有if使用的地方,尽量想想else使用的场景,保证逻辑严谨性。
- VPN软件的使用;热部署的使用(http://docs.alibaba-inc.com/)。
二、Bug描述:Mybatis中parameterType使用
mapper层中使用parameterType="java.lang.Integer"基本类型,代码报错:
//org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException: // There is no getter for property named 'siteId' in 'class java.lang.Integer'
解决办法,当入参为基本数据类型的使用,使用_parameter代替基本数据类型,如下:
<select id="getRulesInfoBysiteId" parameterType="java.lang.Integer" resultMap="RulesMap" > SELECT a.site_id, a.site_name, b.id AS city_id, b.`name` AS city_name, c.id AS region_id, c.`name` AS region_name FROM idc_site a, city b, area c WHERE a.region = c.`name` AND a.city = b.`name` AND a.is_deleted = 'n' AND b.is_deleted = 'n' AND c.is_deleted = 'n' <if test="_parameter != null"> AND a.site_id = #{_parameter,jdbcType=INTEGER} </if> </select>
或者在mapper层的接口中,给这个方法的参数加上@param(value=“siteId”),这样就能在.xml中使用#{siteId,jdbcType=INTEGER}了,仅使用于基本数据类型。
//mapper层对应的接口中必须加@Resource注解,否则在Dao层注入*Ext会失败
@Resource public interface SiteMapperExt extends SiteMapper { //mapper层对应的接口中加mybatis提供的注解@Param("siteId") public RulesInfo getRulesInfoBysiteId(@Param("siteId")Integer siteId); }
更多使用详情请看最后附文。
三、Bug描述:
/** * 自动分配物流供应商 */ @Override public void autoAssignSupplier(RuleInfos ruleInfos, String deviceType, WorkOrderMain workOrder, int amounts) { // 精确匹配规则制定 物流供应商 LogisticsAssignRules bean = getExactMatchSPId(ruleInfos.getSourceRegionId(), ruleInfos.getTargetRegionId(), ruleInfos.getSourceCityId(), ruleInfos.getTargetCityId(), ruleInfos.getSourceSiteName(), ruleInfos.getTargetSiteName(), deviceType, amounts); if (null == bean) { // 按比例规则制定物流供应商 Map<String, String> supplierRatesMap = getSpRates(); Map<String, String> logicOf90DaysBefore = getAssignRates(); String supplierId = getSupplierIdBy90Days(supplierRatesMap, logicOf90DaysBefore, amounts); logisticsWorkOrderBo.LogisticsAssigned(workOrder, WorkOrderStatsCst.LogisticsOrderState.unassigned, supplierId, getSpRatesDesc(), WorkOrderCst.DEFAULT_VALUE_YES); logger.info("auto assign supplier as rates,supplierId = {}, description = {}", supplierId, getSpRatesDesc()); } else { // 精确匹配,直接分配物流供应商 logisticsWorkOrderBo.LogisticsAssigned(workOrder, WorkOrderStatsCst.LogisticsOrderState.unassigned, bean.getSpId().toString(), bean.getRuleJsonVal(), WorkOrderCst.DEFAULT_VALUE_YES); logger.info("auto assign supplier start as rules, supplierId = {}, description = {}", bean.getSpId().toString(), getSpRatesDesc()); } }
在接口调用中,当传递属性过多的时候,可以考虑用对象来传递,方便以后的扩展。如本代码中,当后续添加规则时,需要更新方法。另外对于公用的东西,尽量维护在静态枚举值中。
四、Bug描述:方法入口处统一获取当前时间new Date()
在代码中的时间要作为条件来筛选数据,如果同一个方法中,在多个地方出现new Date(),算上程序执行的纳秒级别的时间,可能在当前日期的“23:59:59 纳秒”产生跨界时间的问题,给代码造成概率极低的隐患。
SELECT d.sp_id, COUNT(a.sn) AS asset_counts FROM idc_asset_list a LEFT JOIN idc_work_order_main b ON a.order_id = b.id LEFT JOIN idc_order_atomic_list c ON c.order_id = b.id LEFT JOIN idc_atomic_logistics d ON d.atomic_id = c.atomic_id WHERE a.is_deleted = 'n' AND b.is_deleted = 'n' AND c.is_deleted = 'n' AND d.is_deleted = 'n' AND d.sp_id IS NOT NULL AND b.gmt_create < CONCAT('2016-08-04', '23:59:59') AND b.gmt_create > date_sub( '2016-08-04 00:00:00', INTERVAL 3 MONTH ) AND ( b.state != 'cancle' OR b.sub_state != 'cancle' ) GROUP BY d.sp_id ORDER BY sp_id DESC
因为要将上述数据缓存到JVM中,数据结构在集群中的一台机器上只维护一份。一天最多查询8次。
使用到的SQL如下:
<select id = "getLogisticsList90DaysBefore" parameterType="java.lang.String" resultType ="java.util.Map"> SELECT d.sp_id AS spId, COUNT(a.sn) AS assetCounts FROM idc_asset_list a LEFT JOIN idc_work_order_main b ON a.order_id = b.id LEFT JOIN idc_order_atomic_list c ON c.order_id = b.id LEFT JOIN idc_atomic_logistics d ON d.atomic_id = c.atomic_id WHERE a.is_deleted = 'n' AND b.is_deleted = 'n' AND c.is_deleted = 'n' AND d.is_deleted = 'n' AND d.sp_id IS NOT NULL AND (b.state != 'cancle' OR b.sub_state != 'cancle') <if test = "_parameter != null and _parameter !=''"> AND a.gmt_create <= CONCAT(#{yesterday},' 23:59:59') AND a.gmt_create >= DATE_SUB(CONCAT(#{yesterday},' 00:00:00'), INTERVAL 3 MONTH) </if> GROUP BY d.sp_id ORDER BY sp_id DESC </select>
mapper层的代码中,我们使用了mysql函数date_sub(concat(""), interval 3 month),并且返回resultType="java.util.Map",我们使用结构List<String,Map<String,Object>>结构来接收查询结果,而没有采用resultMap封装对象来接收结果。
SQL执行之后的返回结果为list,通过断点跟踪获悉sp_id为Integer类型,asset_counts为Long类型。
//获取spId Integer spId = map.get("spId"); //获取assetCounts Long assetCounts = map.get("assetCounts");
故使用如下代码获取查询结果,但是代码中封装了数据类型,所以统一采用Object来获取。
五、Bug描述:考虑到线上缺失配置文件,添加空指针判断;为程序健壮性,必须在前后端同时对参数完整性作出校验。
/** * 校验参数的完整性 {设备类型与数量必填,用于规则匹配校验} */ private void checkParameters(AssignSupplierRulesDTO dto) { // 数量合理性校验 if (StringUtils.isNotBlank(dto.getAssetNum())) { if (dto.getAssetNum().toCharArray().length <= 1) { throw new ServiceException(ErrorCode.Params_Lost); } else { if (!(StringUtils.isNumeric(dto.getAssetNum().substring(1)))) { throw new ServiceException(ErrorCode.Params_Invalid); } if (!("><=≤≥≠".contains(dto.getAssetNum().substring(0, 1)))) { throw new ServiceException(ErrorCode.Params_Invalid); } } } // 供应商必填 if (null == dto.getSpId()) { throw new ServiceException(ErrorCode.Params_Lost); } // 规则名称必填 if (StringUtils.isBlank(dto.getRuleName())) { throw new ServiceException(ErrorCode.Params_Lost); } // 当指定规则类型的时候,关联性校验 if (StringUtils.isNotBlank(dto.getRuleType())) { // 同城校验 if (dto.getRuleType().equals(WorkOrderCst.RelocationType.SameCity.name())) { if (!(WorkOrderCst.MATCH_ALL_PARAMETERS.equals(dto.getSourceCity())) && !(WorkOrderCst.MATCH_ALL_PARAMETERS.equals(dto.getSourceCity()))) { if (dto.getSourceCity() != dto.getTargetCity()) { throw new ServiceException(ErrorCode.Params_Invalid); } } } // 同区域内校验 if (dto.getRuleType().equals(WorkOrderCst.RelocationType.RegionalIn)) { if (!(WorkOrderCst.MATCH_ALL_PARAMETERS.equals(dto.getSourceRegion())) && !(WorkOrderCst.MATCH_ALL_PARAMETERS.equals(dto.getTargetRegion()))) { if (StringUtils.isNotBlank(dto.getSourceRegion()) && StringUtils.isNotBlank(dto.getTargetRegion())) { // 区域必须相等 if (!(dto.getSourceRegion().equals(dto.getTargetRegion()))) { throw new ServiceException(ErrorCode.Params_Invalid); } } } if (!(WorkOrderCst.MATCH_ALL_PARAMETERS.equals(dto.getSourceCity())) && !(WorkOrderCst.MATCH_ALL_PARAMETERS.equals(dto.getTargetCity()))) { if (StringUtils.isNotBlank(dto.getSourceCity()) && StringUtils.isNotBlank(dto.getTargetCity())) { if (!addressBo.whetherCityInTheSameArea(dto.getSourceCity(), dto.getTargetCity())) { throw new ServiceException(ErrorCode.Params_Invalid); } } } if (!(WorkOrderCst.MATCH_ALL_PARAMETERS.equals(dto.getSourceSite())) && !(WorkOrderCst.MATCH_ALL_PARAMETERS.equals(dto.getTargetSite()))) { if (StringUtils.isNotBlank(dto.getSourceSite()) && StringUtils.isNotBlank(dto.getTargetSite())) { if (!addressBo.whetherSiteInTheSameArea(dto.getSourceSite(), dto.getTargetSite())) { throw new ServiceException(ErrorCode.Params_Invalid); } } } } // 不同区域的校验 if (dto.getRuleType().equals(WorkOrderCst.RelocationType.RegionalOut)) { if (!(WorkOrderCst.MATCH_ALL_PARAMETERS.equals(dto.getSourceRegion())) && !(WorkOrderCst.MATCH_ALL_PARAMETERS.equals(dto.getTargetRegion()))) { if (StringUtils.isNotBlank(dto.getSourceRegion()) && StringUtils.isNotBlank(dto.getTargetRegion())) { if (dto.getSourceRegion().equals(dto.getTargetRegion())) { throw new ServiceException(ErrorCode.Params_Invalid); } } } if (!(WorkOrderCst.MATCH_ALL_PARAMETERS.equals(dto.getSourceCity())) && !(WorkOrderCst.MATCH_ALL_PARAMETERS.equals(dto.getTargetCity()))) { if (StringUtils.isNotBlank(dto.getSourceCity()) && StringUtils.isNotBlank(dto.getTargetCity())) { if (addressBo.whetherCityInTheSameArea(dto.getSourceCity(), dto.getTargetCity())) { throw new ServiceException(ErrorCode.Params_Invalid); } } } if (!(WorkOrderCst.MATCH_ALL_PARAMETERS.equals(dto.getSourceSite())) && !(WorkOrderCst.MATCH_ALL_PARAMETERS.equals(dto.getTargetSite()))) { if (StringUtils.isNotBlank(dto.getSourceSite()) && StringUtils.isNotBlank(dto.getTargetSite())) { if (addressBo.whetherSiteInTheSameArea(dto.getSourceSite(), dto.getTargetSite())) { throw new ServiceException(ErrorCode.Params_Invalid); } } } } } }
六、Bug描述:String转Integer;String转int的熟练使用。
public class Test { public static void main(String[] args) { String number = "520"; Integer a = 521; int b = 522; //String转Integer Integer.valueOf(number); //String转int Integer.parseInt(number); new Integer(number).intValue(); //Integer转String a.toString(); //Integer转int a.intValue(); //int转String String.valueOf(b); Integer.toString(b); String str = "" + b; //int转Integer new Integer(b); //String转BigDecimal new BigDecimal(number); //获取今天日期 new Date(System.currentTimeMillis()); // Fri Aug 05 20:16:07 CST 2016 DateFormat.getDateInstance().format(new Date()); // 2016-8-5 } }
七、List和数组的转换
public class Test { public static void main(String[] args) { String[] family = { "XuG", "XuX", "GaiZ", "LianW" }; List<String> list = new ArrayList<String>(Arrays.asList("XuG", "XuX", "GaiZ", "LianW")); //数组转list List<String> list_01 = new ArrayList<String>(Arrays.asList(family)); //list转数组 String[] str = (String[])list.toArray(); } }
八、Bug描述:变量命名规范。
变量的命名规范要有意义,在数据库建表,创建java bean的时候,一定要保证单词使用的正确性。如label和lable;region和regin。要注意到变量的命名可能跟数据库的关键字或java的关键字有冲突,可以采用下划线的原则处理关键字冲突。
九、Bug描述:逻辑严谨性。
private String getSupplierIdBy90Days(Map<String, String> supplierRatesMap, Map<String, String> logicOf90DaysBefore, int dispatchNum) { int ratesCount = 0, dispatchCount = 0; for (String spId : supplierRatesMap.keySet()) { ratesCount = ratesCount + Integer.parseInt(supplierRatesMap.get(spId)); } for (String spId : logicOf90DaysBefore.keySet()) { dispatchCount = dispatchCount + Integer.parseInt(logicOf90DaysBefore.get(spId)); } Map<String, String> idealizedMap = new HashMap<String, String>(); for (String spId : supplierRatesMap.keySet()) { Integer dispathNum = (dispatchCount * Integer.parseInt(supplierRatesMap.get(spId))) / ratesCount; idealizedMap.put(spId, dispathNum.toString()); } int gap = -1; String supplierId = StringUtils.EMPTY; if (CollectionUtils.isNotEmpty(logicOf90DaysBefore.keySet())) { for (String spId : logicOf90DaysBefore.keySet()) { if (null != idealizedMap.get(spId)) { int mix = Integer.parseInt(idealizedMap.get(spId)) - Integer.parseInt(logicOf90DaysBefore.get(spId)); if (mix < gap) { gap = mix; supplierId = spId; } } else { supplierId = spId; // 新添加的供应商比例 } } } else { supplierId = new ArrayList<String>(supplierRatesMap.keySet()).get(0); } return supplierId; }
有判断if条件的地方,要考虑到else的可能出现情况,尤其是if else 嵌套多层的时候,可能某些else的情况遗漏,会给程序带来问题。如上述代码中的else的缺失,可能在“新添加供应商比例”的情况下,出现没有分配供应商的情况。
十、VPN工具
VPN工具下载使用:Cisco AnyConnect VPN Client 64位下载
附:Mybatis中parameterType和resultType的相关用法
1、parameterType用法,parameterType的传入参数类型大致分为如下几类:
基本数据类型(_parameter接收参数)
<!-- mapper对应接口层 --> public RulesInfo getRulesInfoBysiteId(Integer siteId); <!-- mapper中指定Integer类型,用_parameter来接收 --> <select id="getRulesInfoBysiteId" parameterType="java.lang.Integer" resultMap="RulesMap" > SELECT a.site_id, a.site_name, b.id AS city_id, b.`name` AS city_name, c.id AS region_id, c.`name` AS region_name FROM idc_site a, city b, area c WHERE a.region = c.`name` AND a.city = b.`name` AND a.is_deleted = 'n' AND b.is_deleted = 'n' AND c.is_deleted = 'n' <if test="_parameter != null"> AND a.site_id = #{_parameter,jdbcType=INTEGER} </if> </select>
数组类型(foreach中的collection必须是array,不管变量的具体名称是什么。如下面变量名为ids,collection却是array)
<!-- mapper对应接口层:根据工单角色获取有权限的机房 --> public List<User> findUserListByIdList(int[] ids); <!-- mapper中指定java.util.List类型,一般用于MySQL中IN语句中 --> <select id="findUserListByIdList" parameterType="java.util.HashList" resultType="User"> select * from user <where> user.ID in ( <foreach item="guard" index="index" collection="array" separator=","> #{guard} </foreach> ) </where> </select>
List类型(单独传入list时,foreach中的collection必须是list,不管变量的具体名称是什么。比如代码中变量名为staffIds,collection中确实list)
<!-- mapper对应接口层 --> public List<IdcStaff> selectStaffsbyIdnumbers(List<String> staffIds); <!-- mapper中指定java.util.List类型,一般用于MySQL中IN语句中 --> <select id="selectStaffsbyIdnumbers" parameterType="java.util.List" resultMap="BaseResultMap"> SELECT <include refid="Base_Column_List"/> FROM idc_staff where id_number IN <foreach item="item" index="index" collection="list" open="(" separator="," close=")"> #{item} </foreach> AND IS_DELETED='n' ORDER BY staff_status </select>
Map类型(MAP中有list或array时,foreach中的collection必须是具体list或array的变量名。同上)
<!--*BoImpl.java层构造传入的map参数--> public List<SiteUserVo> getSiteUserPermission(String siteName, Long roleId, String workNo) { Map<String, Object> siteByUser = new HashMap<String, Object>(); if (StringUtils.isNotBlank(siteName) && !"".equals(siteName.trim())) { siteByUser.put("site", siteName); } if (null != roleId && 0 != roleId) { siteByUser.put("roleId", roleId); } if (StringUtils.isNotBlank(workNo) && !"".equals(workNo.trim())) { siteByUser.put("workNo", workNo); } return siteMapperExt.getSiteUserPermission(siteByUser); } <!-- mapper对应接口层:根据工单角色获取有权限的机房 --> public List<SiteUserVo> getSiteUserPermission(Map siteByUser); <!-- mapper中指定java.util.List类型,一般用于MySQL中IN语句中 --> <select id="getSiteUserPermission" parameterType="java.util.HashMap" resultMap="SiteUserVoMap"> SELECT i.site_name, t.role_id, h.work_no FROM idc_site_user t LEFT JOIN idc_site i ON t.site_id = i.site_id LEFT JOIN app_user h ON t.user_id = h.id WHERE t.IS_DELETED = 'n' AND h.IS_DELETED = 'n' AND i.IS_DELETED = 'n' <IF test = "site!=null" > AND i.site_name = #{site} </IF > <IF test = "roleId!=null" > AND t.role_Id = #{roleId} </IF > <IF test = "workNo!=null" > AND h.work_no = #{workNo} </IF > </select>
Java对象
<!-- mapper对应接口层:根据工单角色获取有权限的机房 --> List<AssignRulesVo> selectByQuery(QueryAssignRulesrDo query); <!-- mapper中指定对象类型,对象的属性名可以直接使用;如果要在SQL中判定对象是否为空,还要用_parameter --> <select id="selectByQuery" parameterType="com.alibaba.tboss.dal.mysql.workOrder.query.QueryAssignRulesrDo" resultMap="BaseResult"> SELECT a.id, a.is_valid, a.rule_lable, a.rule_name, a.type, b.sp_id, b.sp_name, a.rule_content, c.user_name, a.gmt_modified, a.ordering, a.rule_json_val FROM idc_logistics_assign_rules a LEFT JOIN app_user c on c.work_no=a.modifier and c.is_deleted='n', idc_sp_info b WHERE a.is_deleted = 'n' AND b.is_deleted = 'n' AND a.sp_id = b.sp_id <if test="ruleId != null"> AND a.id = #{ruleId,jdbcType=BIGINT} </if> <if test="ruleName != null and ruleName != ''"> AND a.rule_name IN (${ruleName}) </if> ORDER BY ordering asc <if test="doPage == true"> limit #{skip,jdbcType=INTEGER}, #{take,jdbcType=INTEGER} </if> </select>
补充${}会导致SQL注入攻击,不建议使用。
//取值的时候用的是#{}。它具体的意思是告诉MyBatis创建一个预处理语句参数。 //使用JDBC,这样的一个参数在SQL中会由一个“?”来标识,并被传递到一个新的预处理语句中。 //一般情况下,我们采用#{}取值,产生预处理语句,但是有时我们可能不希望Mybatis来帮我们预处理。 //${columnName},这里MyBatis不会修改或转义字符串。而是直接拼接到SQL字符串后面。