2012_11月总结分享

来源:互联网 发布:苹果手机恢复数据在哪 编辑:程序博客网 时间:2024/06/15 09:04

     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每一个方法,都有如下类似如下的处理:如果参数为空,会抛出IllegalArgumentException

 public 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 stubif(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/QuickStartWithJavaClient

List<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答疑的回复

墨合 (2012-11-29 11:50:03):
想咨询一下,是否可以不在外层catch Exception?
tair答疑 (13:16:33):
这个接口不抛异常,不需要try catch


最终我的想法是:

反正我们外面加上,保证世界和平


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








原创粉丝点击