一、總結
- 在Webx的Velocity中獲取url中參數:$rundata.getRequest().getParameter('userId')
- 在Webx項目中,防止CSRF攻擊(Cross-site request forgery,跨站請求偽造),在form表單提交中要加入$!csrfToken.ajaxUniqueToken
- 在MyBatis的mapper層,使用標簽association實現對象的關聯,一個bean配多個association標簽。
二、Bug描述:Velocity從URL中獲取parameter參數
在項目IDCM中,使用webx容器進行項目的開發。前端的模板引擎采用了velocity,在項目中,當從列表頁跳到詳情頁的時候,通常我們的screen層是采用如下方式進行展現的:
public class EditRules extends BaseScreen { @Autowired private AutoAssignSupplierBo autoAssignSupplierBo; @Autowired private SupplierBo supplierBo; @Autowired private AddressBo addressBo; @Autowired private SiteBo siteBo; public void execute(@Param("id") String id, Context context) throws Exception { QueryAssignRulesrDo query = new QueryAssignRulesrDo(); if (StringUtils.isBlank(id)) { throw new ServiceException("id is empty "); } query.setRuleId(Long.parseLong(id)); BoResultDTO<List<AssignRulesVo>> result = autoAssignSupplierBo.selectByQuery(query); List<AssignRulesVo> list = result.getData(); if (CollectionUtils.isNotEmpty(list)) { // 當前規則的詳情 if (StringUtils.isNotBlank(list.get(0).getType())) { list.get(0).setTypeVal(list.get(0).getType()); list.get(0).setType(WorkOrderCst.RelocationType.getNameByStrValue(list.get(0).getType())); } // 如果有數量信息需要展示 if (list.get(0).getRuleContent().contains("數量")) { String[] numDes = list.get(0).getRuleContent().split(" "); for (String str : numDes) { if (str.contains("數量")) { String[] sz = str.split(":"); if (2 == sz.length) { list.get(0).setAssetNum(sz[1]); } } } } context.put("ruleInfos", JSON.toJSONString(list.get(0))); // 補全控件信息 Map<String, Object> map = fullInfo(list.get(0).getRuleJsonVal()); if (list.get(0).getRuleContent().contains("數量")) { map.put("assetNum", list.get(0).getAssetNum()); } context.put("ruleDes", JSONUtils.toJSONString(map)); } // 初始化類型信息 Map<String, Object> RelocationTypeList = WorkOrderCst.RelocationType.getRelocationTypeList(); context.put("RelocationTypeList", RelocationTypeList); // 物流供應商信息 context.put("logisticsSps", supplierBo.queryAllByType(WorkOrderCst.SpType.logistics.name())); // 傳入設備類型 this.setorderDeviceType(context); } }
上圖代碼是自動分配物流供應商從列表頁跳轉到詳情頁的時候,需要顯示調用screen層的跟*.vm同名的*.java方法,通過傳入參數id,即選擇了指定行,后台會將查詢到的數據封裝到對象中,在vm中可以直接使用,而不用再走ajax請求,提升了系統的反映速度。其中在BaseScreen.java中負責公共日志的輸出,當前權限的獲取,以及一些公共屬性的動態獲取。但是,有一些業務場景中,我們在進行跳轉的時候,只需要知道跳轉過來的當前id,並不需要后端來加載數據。這時候,如果能從vm中直接獲取跳轉過來的url的parameter參數,那么就可以省去跟*.vm同名的*.java中的execute方法。
// 唯一正確的用法: <input type="hidden" id="user_id" value=" $rundata.getRequest().getParameter('userId')"/> //以下幾種用法都無法獲取到參數的值 $!request.parameter.userId $!request.paarmater.getParameter('userId')
此外,Velocity更多使用細節參考英文官方文檔或前往此處。
三、Bug描述:$!csrfToken.hiddenField
CSRF(跨站請求偽造),它通過偽裝來自受信任用戶的請求來利用受信任的網站。在IDCM項目中,在*.vm頁面會有大量的<from>表單提交,在表單提交的時候,為了防止跨站請求偽造,要在form標簽之后緊跟$!csrfToken.ajaxUniqueToken。
<form name="xxx" action="xxx" method="post"> $!csrfToken.hiddenField </form>
上述代碼中,在VM文件的form表單中添加了token。該表單請求極有可能涉及數據增刪改,需要防范CSRF,請確認使用POST請求,增加token參數,並在服務端校驗token。
三、Bug描述:MyBatis中mapper層的association標簽使用
API接口調用,web層/openapi/rack代碼如下,RpcResult做為接口查詢返回的對象,包括Data、info、Success,所以該層合理的代碼應該包含try catch,正確的邏輯打印正確的信息。一旦接口調用錯誤,要返回合適的信息。
//根據房間和機櫃名稱,批量查詢機櫃信息 @ResourceMapping("batchQueryRackByRoomNameAndRackName") public RpcResult<Object> batchQueryRackByRoomNameAndRackName(@RequestParam(name = "queryParam") String queryParam) { RpcResult<Object> rpcResult = new RpcResult<Object>(); try { @SuppressWarnings("unchecked") List<RackAndRoom> list = (List<RackAndRoom>) JsonToBeanUtil.JsonToJavaBean(queryParam, RackAndRoom.class); List<Rack> rackList = new ArrayList<Rack>(); if (CollectionUtils.isNotEmpty(list)) { for (RackAndRoom rackAndRoom : list) { checkParameter(rackAndRoom); Rack rack = rackBo.batchQueryRackByRoomNameAndRackName(rackAndRoom.getRackName(), rackAndRoom.getRoomName()); rackList.add(rack); } rpcResult.setData(rackList); rpcResult.setInfo("查詢成功!"); rpcResult.setSuccess(true); } } catch (Exception e) { logger.error(" batchQueryRackByRoomNameAndRackName error:" + e.getMessage(), e); rpcResult.setSuccess(false); rpcResult.setInfo("查詢失敗,具體異常信息為:" + e.getMessage()); } return rpcResult; }
上述的代碼結構在接口查詢中,利用了分類的思想,針對接口查詢成功和失敗,分別對info、success、data進行賦值。並且如果失敗,會有日志記錄。異常的捕獲在RPC層(addError)、Bo層(事務回滾)、OpenAPI層要區別對待。對於RPC中的錯誤,因為它是通過瀏覽器跟用戶交互的,所以一般會將錯誤添加到Error對象中,將錯誤信息反饋給瀏覽器端的用戶,使得用戶可以修改自己的操作,達到預期的效果。
/** * 修改排序 */ @ResourceMapping("updateOrderIng") public RpcResult<Object> updateOrderIng(@RequestParams AssignRulesVo assignRulesVo, @RequestParam(name = "type") String type, ErrorContext error) { RpcResult<Object> result = new RpcResult<Object>(); result.setSuccess(true); try { if (null == assignRulesVo || null == assignRulesVo.getOrdering() || StringUtils.isBlank(type)) { throw new SerialException("缺少排序信息和規則信息,無法修改"); } autoAssignSupplierBo.updateOrderIng(assignRulesVo, type); result.setInfo("修改排序成功"); result.setData(true); } catch (Exception e) { result.setSuccess(false); logger.error("updateOrderIng err : ", e.getMessage(), e); addError(error, ErrorCode.Sys_Error.getCode(), e.getMessage(), result); } return result; }
對比可以看到RPC中,通常會傳入Error對象,使用addError方法,將錯誤信息反饋到前端。接着講openapi中的查詢接口,它通過調用bo層、boImpl層、Ext層的rackMapperExt.batchQueryRackByRoomNameAndRackName(map);來查詢。
<select id="batchQueryRackByRoomNameAndRackName" parameterType="java.util.Map" resultMap="BaseResultMap_Ext"> select <include refid="Base_Column_List_Ext" /> <include refid="Base_Column_List_Room" /> <include refid="Base_Column_List_Site" /> FROM idc_rack temp_rack INNER JOIN idc_room temp_room ON temp_rack.room_id = temp_room.room_id INNER JOIN idc_site temp_site ON temp_room.site_id = temp_site.site_id WHERE temp_rack.is_deleted = 'n' AND temp_room.is_deleted = 'n' AND temp_site.is_deleted = 'n' AND temp_rack.rack_name = #{rackName} AND temp_room.room_name = #{roomName} </select>
在代碼中,查詢到的參數采用了<include>標簽,將要查詢的room、rack、site等信息撈出來,最后映射到BaseResultMap_Ext中,如下:
<resultMap type="com.alibaba.tboss.dal.mysql.rack.Rack" id="BaseResultMap_Ext" extends="BaseResultMap"> <result column="rack_name" property="rackName" jdbcType="VARCHAR" /> <result column="room_name" property="roomName" jdbcType="VARCHAR" /> <result column="site_name" property="siteName" jdbcType="VARCHAR" /> <result column="siteType" property="siteType" jdbcType="VARCHAR" /> <result column="lock_num" property="lockNum" jdbcType="INTEGER" /> <result column="power_state" property="powerState" jdbcType="VARCHAR" /> <association property="room" javaType="com.alibaba.tboss.dal.mysql.room.Room"> <id column="room_id" property="roomId" jdbcType="INTEGER" /> <result column="site_id" property="siteId" jdbcType="INTEGER" /> <result column="creator" property="creator" jdbcType="INTEGER" /> <result column="gmt_create" property="gmtCreate" jdbcType="TIMESTAMP" /> <result column="modifier" property="modifier" jdbcType="INTEGER" /> <result column="gmt_modified" property="gmtModified" jdbcType="TIMESTAMP" /> <result column="is_deleted" property="isDeleted" jdbcType="CHAR" /> <result column="room_name" property="roomName" jdbcType="VARCHAR" /> <result column="room_full_name" jdbcType="VARCHAR" property="roomFullName" /> <result column="type" property="type" jdbcType="VARCHAR" /> <result column="cmdb_room_id" property="cmdbRoomId" jdbcType="INTEGER" /> </association> <association property="site" javaType="com.alibaba.tboss.dal.mysql.site.Site"> <id column="site_id" jdbcType="INTEGER" property="siteId" /> <result column="site_name" jdbcType="VARCHAR" property="siteName" /> <result column="country" jdbcType="VARCHAR" property="country" /> <result column="region" jdbcType="VARCHAR" property="region" /> <result column="province" jdbcType="VARCHAR" property="province" /> <result column="city" jdbcType="VARCHAR" property="city" /> <result column="address" jdbcType="VARCHAR" property="address" /> <result column="type" jdbcType="VARCHAR" property="type" /> <result column="site_full_name" jdbcType="VARCHAR" property="siteFullName" /> <result column="supplier" jdbcType="VARCHAR" property="supplier" /> <result column="repair_api" jdbcType="VARCHAR" property="repairApi" /> <result column="is_valid" jdbcType="BIT" property="isValid" /> <result column="creator" jdbcType="VARCHAR" property="creator" /> <result column="modifier" jdbcType="VARCHAR" property="modifier" /> <result column="gmt_create" jdbcType="TIMESTAMP" property="gmtCreate" /> <result column="gmt_modified" jdbcType="TIMESTAMP" property="gmtModified" /> <result column="is_deleted" jdbcType="CHAR" property="isDeleted" /> <result column="sign_company" jdbcType="VARCHAR" property="signCompany" /> <result column="first_user_id" jdbcType="BIGINT" property="firstUserId" /> <result column="second_user_id" jdbcType="BIGINT" property="secondUserId" /> <result column="third_user_id" jdbcType="BIGINT" property="thirdUserId" /> <result column="comments" jdbcType="VARCHAR" property="comments" /> <result column="cmdb_site_id" property="cmdbSiteId" jdbcType="INTEGER" /> </association> </resultMap>
通過標簽<association>實現了對象映射的時候的一對多的關聯,上述resultMap使用extends="BaseResultMap"擴展了對象映射,其中BaseResultMap來自於Mybatis自動生成的mapper中的映射關系。
四、批量查詢接口性能優化
ext層:
@Resource public interface RackMapperExt extends RackMapper { List<Rack> batchQueryRackByRoomNameAndRackName(List<RackVo> list); }
mapper層:
<select id="batchQueryRackByRoomNameAndRackName" parameterType="java.util.List" resultMap="BaseResultMap_Ext"> select <include refid="Base_Column_List_Ext" /> <include refid="Base_Column_List_Room" /> <include refid="Base_Column_List_Site" /> FROM idc_rack temp_rack INNER JOIN idc_room temp_room ON temp_rack.room_id = temp_room.room_id INNER JOIN idc_site temp_site ON temp_room.site_id = temp_site.site_id WHERE temp_rack.is_deleted = 'n' AND temp_room.is_deleted = 'n' AND temp_site.is_deleted = 'n' AND <foreach item="item" index="index" collection="list" open="(" separator="OR" close=")"> (temp_rack.rack_name = #{item.rackName} AND temp_room.room_name = #{item.roomName}) </foreach> </select>
五、MyBatis中<![CDATA[...]]>的使用
在xml文件中,處於CDATA部分中的所有內容都會被解析器忽略,避免由於>&等sql字符影響xml文檔結構,segmentfault問題:前往此處。
六、待補充:阿里巴巴緩存使用bacardi、tail等。
附錄:
阿里巴巴-基礎架構事業群業務,項目路徑:

基礎架構事業群業務。
