11月份下旬,我在技术上主要看了看spring的IoC容器实现相关的内容。但是这次来不及写了(这是一个很长的故事),就分享了一下11月份遇到的值得记录的东西吧,中间也穿插2篇文章分享,无缝对接。总结如下:
(1) 代码规范问题
(2) Tair批量读取使用一个问题及其相关
(3) ttm配置文件的一个问题
1. 代码规范问题
最近两周遇到的代码规范问题,主要是DAO、Manager、Service的规范和日志处理规范。
(1) DAO、Manager、Service规范
之前@南八分享了一下这个规范,我还有ppt。我觉得挺好的。如下:
a. DAO
DAO实现读取数据库基本操作,CURD;封装BaseDAO,添加DBRoute支持,增强扩展性 ;配置添加TDDL支持;基于淘宝的common-dao;统一抛出DAOException;入口参数无需验证;尽量原子操作,避免复杂逻辑。
我代码不规范之处为红色部分。在IbatisBrandDAO的updateMyBrands等方法中,不规范如下:
1.写了大量的业务逻辑;
2.读取了Tair;
3.没有统一抛出DAOException;
后面我会改正,大家现在还可以上主干去看看,当反面教材看。
b. Manager
统一异常抛出ManagerException;注解注入DAO,Hsf Consumer Service;如果单个操作执行多条sql,采用Spring事务进行处理;入口参数进行验证;
c. Service
返回结果明确泛型类型;入口参数要求进行验证;不要抛出异常;错误要打印日志;
我代码犯错如下:
1. BrandService接口规范:publicList<BrandDO> getAllBrandWithLogo() throws BrandException
2. 抛出了异常BrandException
3. 结果没有封装
后续我把这个getAllBrandWithLogo会改造。
接下来,我自己对于Deptcenter-common包中对外接口规范要求初步如下:
a.brandService所有的方法均不抛出异常,客户端不用catch异常(只要你相信我,后面我会专门谈到这个问题),只需要查看结果码即可。
b.brandService返回的结果使用result<T>来封装,T为结果类型,并携带结果码。每一种结果码对应一种情况,比如处理成功、参数不合法、数据库异常、错误、hsf服务调用超时等等。每一种错误对应的错误码也要对应起来。
BrandConstants对应了至少7种错误情况。
public final static int ERROR_PARAM_INVALID = -10; //user_id <= 0 or pageSisze <= 0 or brandId <= 0类似情况 public final static int ERROR_BRANDID_NOT_IN_EXTDB = -11; //brandId不在收藏品牌表中 public final static int ERROR_BRANDID_NOT_MANUAL = -12; //brandId在收藏表中,但是该品牌本身不能收藏 public final static int ERROR_ADD_BRAND = -13; //关注品牌逻辑错误:用户之前已经关注过该品牌 public final static int ERROR_DELETE_BRAND = -14; //用户取消关注品牌辑错误:用户之前并未关注过该品牌 public final static int ERROR_BRANDID_NOT_IN_INFODB = -15; //brandId不在品牌基本表中 public final static int ERROR_BRANDID_NOT_IN_TAIR = -16; //brandId对应的信息在Tair中没有
我看了看Tair client处理源码,和这个类似,提供大量的错误码供客户端判断。
以后至少deptcenter-common包会严格按照这个规范来做。
2.异常处理的问题
a.首先分享一下异常处理
异常处理的最佳实践http://blog.jobbole.com/18291/
记录异常日志的7条规则http://www.importnew.com/518.html
分别都提到了不要记录后又重新向外抛出或者为仅记录exception一次,理由是对同一个错误的栈回溯(stack trace)记录多次的话,会让程序员搞不清楚错误的原始来源。所以仅仅记录一次就够了。
代码不规范之处:
DefaultMyBrandsManager的isMyBrandExit方法:
public boolean isMyBrandExit(Long userId, Long brandId) throws MyBrandsServiceException { try { return this.myBrandsDAO.isMyBrandExit(userId, brandId); } catch (DAOException e) { this.logger.error("查询用户是否收藏品牌时出错,数据库错误,用户:"+ userId+" 品牌id"+brandId,e); throw new MyBrandsServiceException(e); } }
记录了日志,又抛出了ManagerException异常。
在上层DefaultMyBrandsAO.isMyBrandExit()方法再次catch了这个managerException,并打印同样错误地信息。
public Result isMyBrandExit(Long userId, Long brandId) { Result result = initResult(); try { boolean isExit = myBrandsManager.isMyBrandExit(userId, brandId); result.setDefaultModel(isExit); result.setSuccess(true); } catch (ManagerException e) { log.error("取得az页面详细信息失败,原因为:", e); result.setSuccess(false); } return result; }
而且错误的中文原因也写错了。这样在日志中会至少同时记录两处一样的错误堆栈信息。
另外在deptcenter-core-client包中的MyBrandsServiceClient每一个方法,都有如下类似如下的处理:如果参数为空,会抛出IllegalArgumentExceptionpublic boolean isMyBrandExit(Long userId, Long brandId) throws MyBrandsServiceException { if(null==userId || null == brandId){ throw new IllegalArgumentException("MyBrandsServiceException@ condition erro: userId or brandId is null"); } return myBrandsService.isMyBrandExit(userId, brandId); }
我认为这样不太好:
IllegalArgumentException继承了RuntimeException,是一种RuntimeException。这样就会导致调用MyBrandsServiceClient的客户端必须要在外层catch Exception。而事实上很多客户端使用完全忘记了这一点,相当危险!举例如下:
DefaultMyBrandsManager的isMyBrandExit()实现如下:
public boolean isMyBrandExit(Long userId, Long brandId) throws MyBrandsServiceException { try { return this.myBrandsDAO.isMyBrandExit(userId, brandId); } catch (DAOException e) { this.logger.error("查询用户是否收藏品牌时出错,数据库错误,用户:"+ userId+" 品牌id"+brandId,e); throw new MyBrandsServiceException(e); } }
可以看出来,并没有catch Exception,也就是说,如果userId或者brandId为空,isMyBrandExit会抛出IllegalArgumentException,但是代码并没有catch住,所以这个IllegalArgumentException会继续传播到上一层,到DefaultMyBrandsAO对于isMyBrandExit的处理如下:
public Result isMyBrandExit(Long userId, Long brandId) { Result result = initResult(); try { boolean isExit = myBrandsManager.isMyBrandExit(userId, brandId); result.setDefaultModel(isExit); result.setSuccess(true); } catch (ManagerException e) { log.error("取得az页面详细信息失败,原因为:", e); result.setSuccess(false); } return result; }
可以看出,这里还是没有catch Exception。可以想像,这个AO的该方法会有可能抛出RuntimeException,很危险。建议:要么在MyBrandsServiceClient别抛出IllegalArgumentException,不然上层客户端都要catchRuntimeException。
就在写这篇文章的时候的当天,我突然想起的对外提供的hsf服务犯了一个低级错误,这里写出来让大家看看,拉出来示众。
public BrandResult<Integer> isFavBrand(Long userId, Long brandId) { // TODO Auto-generated method stub if(userId <= 0 || brandId <= 0){ BrandResult<Integer> brandResult = new BrandResult<Integer>(); brandResult.setSuccess(false); brandResult.setResultCode(BrandConstants.Result.ERROR_PARAM_INVALID); brandResult.setErrorMsg("参数非法"); return brandResult; }else{ return brandService.isFavBrand(userId, brandId); } }
对外服务BrandService的每一个接口都没有申明抛出异常。但是在BrandServiceClient实现的时候,我仅对参数做了大小判断,没有做非空判断。如果此时客户端调用的时候,传过来一个null,那么这里就会抛出NullPointerException,客户端就会出错。修改后,增加了非空判断。
上述案例我都会放在品牌街wiki里面codereiew的记录
1. Tair批量读接口使用一个问题
问题:漏掉了潜在出现的部分成功的处理。
原处理代码:
DefaultBrandTairManager
批量读取Tair的时候,代码如下:
try { Result<List<DataEntry>> result = defaultMyBrandsTairManager.mget(namespace,getMKeys(brandIdList)); if (result != null) { if (result.isSuccess()) { if (result.getValue() != null) { for(DataEntry dataEntry: result.getValue()) { list.add((ForTagValueIndexDO)dataEntry.getValue()); } return list; } } else { log.warn("getBrandForTagValueIndexFromTair get error;" + result.getRc()+ ";key=" + brandIdList); } } } catch (Exception e) { log.error("getBrandForTagValueIndexFromTair", e); } return list;
其实Tair的QuickStart已经写好示例了:
http://baike.corp.taobao.com/index.php/QuickStartWithJavaClientList<Object> keys = new ArrayList<Object>(); keys.add(key1); keys.add(key2); // 更多key…… Result<List<DataEntry>> result = tm.mget(namespace, keys); if (result.isSuccess() || ResultCode.PARTSUCC.equals(result.getRc())) { // 部分成功时会返回成功的值 for (DataEntry de : result.getValue()) { // 返回的处理代码 } } else { // 你的出错处理代码 }
批量读取Tair的时候,应该加上部分成功的逻辑(只要业务能够接受)。即:
if(result.isSuccess() || ResultCode.PARTSUCC.equals(result.getRc()))
这个批量读取Tair的方法在品牌街的应用场景是在我的关注品牌页面:首先通过userId查询List<Long BrandId>,然后将这个brandIdList去批量查询Tair,得到品牌详情并展示。原先有个隐藏的bug:如果用户收藏了Nike和Adidas,且恰好Nike品牌详情不在Tair中,那么之前的代码没有加上部分成功的判断,直接返回空数据,用户在我的关注品牌页面啥都没有。其实业务逻辑应该是,就算nike不在,应该返回Adidas的品牌详情。所以,原来那样写是有问题的。
Tair使用还有一个坑,就是批量读取的顺序问题。比如用list<k1,k2,k3>去Tair去数据,返回结果不保证是list<V1,V2,V3>,顺序是乱序的。如果业务需要,那么要手动调整。
另外还有一个问题就是:到底要不要相信我们调用的服务的申明。比如TairManager方法申明中中,都没有抛出异常。
Tair使用示例:
http://baike.corp.taobao.com/index.php/QuickStartWithJavaClient
get方法并没有加上catch Exception。
我们读取Tair代码都是加了一层try catch,catch Exception
try { Result<List<DataEntry>> result = defaultMyBrandsTairManager.mget(namespace,getMKeys(brandIdList)); if (result != null) { if (result.isSuccess()) { if (result.getValue() != null) { for(DataEntry dataEntry: result.getValue()) { list.add((ForTagValueIndexDO)dataEntry.getValue()); } return list; } } else { log.warn("getBrandForTagValueIndexFromTair get error;" + result.getRc()+ ";key=" + brandIdList); } } } catch (Exception e) { log.error("getBrandForTagValueIndexFromTair", e); } return list;
这个问题我问了问@兆文和@战枫,答复是都一定要在外层catchException,因为我们不能完全相信Tair:他说不抛出异常,就不抛出异常。
之所以有这个疑问,是effective java的第57条说,只针对异常情况才使用异常,而Tair读取的各个方法都没有申明异常。。。这点大家怎么看?
还有对此相似的问题就是,在sqlMapDAO的类似executeQueryForList申明会抛出DAOException,我们代码中还有无必要在使用的时候,在catchDAOException的基础上,有必要catch Exception?
附:Tair答疑的回复
最终我的想法是:
反正我们外面加上,保证世界和平
3. SchedulerFactoryBean的autoStartup属性
定时程序配置文件ttm-biz-timetask.xml中的SchedulerFactoryBean类,有一个配置项如下:
<propertyname="autoStartup" value="false"/>
问题产生:舒俊在本地的自测定时程序的时候,定时程序一直跑不起来,发现autoStartup配置为false。
1. autoStartup如何起作用?
这个autoStartup属性我看了看,无论是线上、预发、日常,配置值都是false。这个属性是用来确定是否自动启动定时任务:即在Ioc容器初始化在生成这个SchedulerFactoryBean的时候,自动启动定时任务。看了看SchedulerFactoryBean这个类的实现,autoStartup这个boolean变量是这样起作用的:
SchedulerFactoryBean实现了InitializingBean这个接口,实现了afterPropertiesSet()方法。
在afterPropertiesSet()方法中,最后三行:
if (this.autoStartup) {
startScheduler(this.scheduler, this.startupDelay);
}
容器工厂会在SchedulerFactoryBean构造出来后,立即调用SchedulerFactoryBean实现的afterPropertiesSet()方法。如果autoStartup为true,那么开始执行startScheduler()方法。startScheduler方法会启动一个线程,调用scheduler.start(),开始执行任务。
2. 为什么autoStartup要配置为false的?
我对这个问题起先不是很清楚:为什么不让应用启动的时候,就去启动任务,还要通过ttm后台来人工指定机器再来运行。后来想到,我们deptadmin应用线上有两台机器,如果
autoStartup配置的是true,那么代码部署上去以后,两台机器都会自动运行定时程序,这样有可能会有问题,也有可能没必要。
3.ttm平台是如何启动任务的?
应该是手动调用了SchedulerFactoryBean的startScheduler方法。待研究。
问了问兆文,admin后台的两台机器是采用随机策略被选中执行的(如果两台机器都是好的话)。开始我认为应用部署成功之后,ttm后台会调用某一台机器的应用,调用startScheduler()方法,启动定时任务。但是我觉得应该不是这样做的,这样做的话,
deptadmin应用中的autoStartup为true,需要在ttm后台,配置好机器绑定后,定时任务才开始执行。应该是在配置机器后,ttm那边调用了startScheduler()方法,定时任务才开始执行,而不是部署代码成功之后(即IoC容器初始化之后)。从这边看来,SchedulerFactoryBean不能配置为lazy-init(不知道ttm那边怎么实现的)
这样看来,SchedulerFactoryBean绝对不能配置为non-singleton的,即scope="prototype"这个配置项目。
EOF