2012_11月总结分享

     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 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/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








  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值