kettle系列-4.kettle定制化开发工具类
要说的话这个工具类还是比较简单的,每个方法体都比较小,但用起来还是可以的,把开发中一些常用的步骤封装了下,不用去kettle源码中找相关操作的具体实现了。
算了废话不多了,直接上重点,代码如下:
import java.util.List; import org.apache.log4j.Logger; import org.pentaho.di.core.KettleEnvironment; import org.pentaho.di.core.database.DatabaseMeta; import org.pentaho.di.core.exception.KettleException; import org.pentaho.di.core.exception.KettleSecurityException; import org.pentaho.di.job.JobMeta; import org.pentaho.di.job.entries.job.JobEntryJob; import org.pentaho.di.job.entries.trans.JobEntryTrans; import org.pentaho.di.job.entry.JobEntryBase; import org.pentaho.di.job.entry.JobEntryCopy; import org.pentaho.di.repository.AbstractRepository; import org.pentaho.di.repository.LongObjectId; import org.pentaho.di.repository.RepositoryDirectoryInterface; import org.pentaho.di.repository.StringObjectId; import org.pentaho.di.repository.filerep.KettleFileRepository; import org.pentaho.di.repository.filerep.KettleFileRepositoryMeta; import org.pentaho.di.repository.kdr.KettleDatabaseRepository; import org.pentaho.di.repository.kdr.KettleDatabaseRepositoryMeta; import org.pentaho.di.trans.TransMeta; import org.pentaho.di.trans.TransPreviewFactory; import org.pentaho.di.trans.step.BaseStepMeta; import org.pentaho.di.trans.step.StepMeta; import org.pentaho.di.trans.step.StepMetaInterface; import org.pentaho.di.trans.steps.jobexecutor.JobExecutorMeta; import org.pentaho.di.trans.steps.transexecutor.TransExecutorMeta; /** * ClassName: KettleUtils <br/> * Function: kettle定制化开发工具集. <br/> * date: 2015年4月29日 上午8:56:24 <br/> * @author jingma * @version 0.0.1 * @since JDK 1.6 */ public class KettleUtils { /** * LOG:日志 */ public static Logger log = Logger.getLogger(KettleUtils.class); /** * repository:kettle资源库 */ private static AbstractRepository repository; /** * 转换模板 */ private static TransMeta transMetaTemplate; /** * 作业模板 */ private static JobMeta jobMetaTemplate; /** * getInstance:获取的单例资源库. <br/> * @author jingma * @return 已经初始化的资源库 * @throws KettleException 若没有初始化则抛出异常 * @since JDK 1.6 */ public static AbstractRepository getInstanceRep() throws KettleException{ if(repository!=null){ return repository; // }else if(1==1){ // //TODO jingma:这里以后添加读取配置初始化连接资源库 }else{ throw new KettleException("没有初始化资源库"); } } /** * createFileRep:创建文件资源库. <br/> * @author jingma * @param id 资源库id * @param name 资源库名称 * @param description 资源库描述 * @param baseDirectory 资源库目录 * @return 已经初始化的资源库 * @throws KettleException * @since JDK 1.6 */ public static AbstractRepository createFileRep(String id, String name, String description, String baseDirectory) throws KettleException{ destroy(); //初始化kettle环境 if(!KettleEnvironment.isInitialized()){ KettleEnvironment.init(); } repository = new KettleFileRepository(); KettleFileRepositoryMeta fileRepMeta = new KettleFileRepositoryMeta( id, name, description, baseDirectory); repository.init(fileRepMeta); log.info(repository.getName()+"资源库初始化成功"); return repository; } /** * createDBRep:创建数据库资源库. <br/> * @author jingma * @param name 数据库连接名称 * @param type 数据库类型 * @param access 访问类型 * @param host ip地址 * @param db 数据库名称 * @param port 端口 * @param user 数据库用户名 * @param pass 数据库密码 * @return 初始化的资源库 * @throws KettleException * @since JDK 1.6 */ public static AbstractRepository createDBRep(String name, String type, String access, String host, String db, String port, String user, String pass) throws KettleException{ return createDBRep( name, type, access, host, db, port, user, pass, "DBRep", "DBRep", "数据库资源库"); } /** * createDBRep:创建数据库资源库. <br/> * @author jingma * @param name 数据库连接名称 * @param type 数据库类型 * @param access 访问类型 * @param host ip地址 * @param db 数据库名称 * @param port 端口 * @param user 数据库用户名 * @param pass 数据库密码 * @param id 资源库id * @param repName 资源库名称 * @param description 资源库描述 * @return 已经初始化的资源库 * @throws KettleException * @since JDK 1.6 */ public static AbstractRepository createDBRep(String name, String type, String access, String host, String db, String port, String user, String pass,String id, String repName, String description) throws KettleException{ destroy(); //初始化kettle环境 if(!KettleEnvironment.isInitialized()){ KettleEnvironment.init(); } //创建资源库对象 repository = new KettleDatabaseRepository(); //创建资源库数据库对象,类似我们在spoon里面创建资源库 DatabaseMeta dataMeta = new DatabaseMeta(name, type, access, host, db, port, user, pass); //资源库元对象 KettleDatabaseRepositoryMeta kettleDatabaseMeta = new KettleDatabaseRepositoryMeta(id, repName, description, dataMeta); //给资源库赋值 repository.init(kettleDatabaseMeta); log.info(repository.getName()+"资源库初始化成功"); return repository; } /** * connect:连接资源库. <br/> * @author jingma * @return 连接后的资源库 * @throws KettleSecurityException * @throws KettleException * @since JDK 1.6 */ public static AbstractRepository connect() throws KettleSecurityException, KettleException{ return connect(null,null); } /** * connect:连接资源库. <br/> * @author jingma * @param username 资源库用户名 * @param password 资源库密码 * @return 连接后的资源库 * @throws KettleSecurityException * @throws KettleException * @since JDK 1.6 */ public static AbstractRepository connect(String username,String password) throws KettleSecurityException, KettleException{ repository.connect(username, password); log.info(repository.getName()+"资源库连接成功"); return repository; } /** * setRepository:设置资源库. <br/> * @author jingma * @param repository 外部注入资源库 * @since JDK 1.6 */ public static void setRepository(AbstractRepository repository){ KettleUtils.repository = repository; } /** * destroy:释放资源库. <br/> * @author jingma * @since JDK 1.6 */ public static void destroy(){ if(repository!=null){ repository.disconnect(); log.info(repository.getName()+"资源库释放成功"); } } /** * loadJob:通过id加载job. <br/> * @author jingma * @param jobId 数字型job的id,数据库资源库时用此方法 * @return job元数据 * @throws KettleException * @since JDK 1.6 */ public static JobMeta loadJob(long jobId) throws KettleException { return repository.loadJob(new LongObjectId(jobId), null); } /** * loadJob:通过id加载job. <br/> * @author jingma * @param jobId 字符串job的id,文件资源库时用此方法 * @return job元数据 * @throws KettleException * @since JDK 1.6 */ public static JobMeta loadJob(String jobId) throws KettleException { return repository.loadJob(new StringObjectId(jobId), null); } /** * loadTrans:加载作业. <br/> * @author jingma * @param jobname 作业名称 * @param directory 作业路径 * @return 作业元数据 * @since JDK 1.6 */ public static JobMeta loadJob(String jobname, String directory) { return loadJob(jobname, directory, repository); } /** * loadTrans:加载作业. <br/> * @author jingma * @param jobname 作业名称 * @param directory 作业路径 * @param repository 资源库 * @return 作业元数据 * @since JDK 1.6 */ public static JobMeta loadJob(String jobname, String directory,AbstractRepository repository) { try { RepositoryDirectoryInterface dir = repository.findDirectory(directory); return repository.loadJob(jobname,dir,null, null); } catch (KettleException e) { log.error("获取作业失败,jobname:"+jobname+",directory:"+directory, e); } return null; } /** * loadTrans:加载转换. <br/> * @author jingma * @param transname 转换名称 * @param directory 转换路径 * @return 转换元数据 * @since JDK 1.6 */ public static TransMeta loadTrans(String transname, String directory) { return loadTrans(transname, directory, repository); } /** * loadTrans:加载转换. <br/> * @author jingma * @param transname 转换名称 * @param directory 转换路径 * @param repository 资源库 * @return 转换元数据 * @since JDK 1.6 */ public static TransMeta loadTrans(String transname, String directory,AbstractRepository repository) { try { RepositoryDirectoryInterface dir = repository.findDirectory(directory); return repository.loadTransformation( transname, dir, null, true, null); } catch (KettleException e) { log.error("获取转换失败,transname:"+transname+",directory:"+directory, e); } return null; } /** * loadTrans:根据job元数据获取指定转换元数据. <br/> * @author jingma * @param jobMeta job元数据 * @param teansName 转换名称 * @return 转换元数据 * @since JDK 1.6 */ public static TransMeta loadTrans(JobMeta jobMeta, String teansName) { JobEntryTrans trans = (JobEntryTrans)(jobMeta.findJobEntry(teansName).getEntry()); TransMeta transMeta = KettleUtils.loadTrans(trans.getTransname(), trans.getDirectory()); return transMeta; } /** * 根据转换元数据和步骤名称获取具体的步骤元数据的复制. <br/> * 一般是不需要这用这个方法的,该方法获取的实体不属于该转换,相当于一个复制 ,修改了直接保存transMeta是没有保存到修改的。<br/> * 若需要修改转换,可以使用:(T)transMeta.findStep(stepName).getStepMetaInterface(), * 这个方法获取的步骤是属于该转换的,修改后,直接保存transMeta就能实现转换修改<br/> * @author jingma * @param transMeta 转换元数据 * @param stepName 步骤名称 * @param stepMeta 具体的步骤元数据对象 * @return 从资源库获取具体数据的步骤元数据 * @since JDK 1.6 */ public static <T extends BaseStepMeta> T loadStep(TransMeta transMeta, String stepName, T stepMeta) { StepMeta step = transMeta.findStep(stepName); try { stepMeta.readRep(KettleUtils.getInstanceRep(), null, step.getObjectId(), KettleUtils.getInstanceRep().readDatabases()); } catch (KettleException e) { log.error("获取步骤失败", e); } return stepMeta; } /** * 根据作业元数据和作业实体名称获取具体的作业实体元数据的复制。<br/> * 一般是不需要这用这个方法的,该方法获取的实体不属于该job了,相当于一个复制 ,修改了直接保存jobMeta是没有保存到修改的。<br/> * 若需要修改job,可以使用:(T)jobMeta.findJobEntry(jobEntryName).getEntry(), * 这个方法获取的实体是属于job,修改后,直接保存jobMeta就能实现job修改<br/> * @author jingma * @param jobMeta 作业元数据 * @param jobEntryName 作业实体名称 * @param jobEntryMeta 要获取的作业实体对象 * @return 加载了数据的作业实体对象 */ public static <T extends JobEntryBase> T loadJobEntry(JobMeta jobMeta, String jobEntryName, T jobEntryMeta) { try { jobEntryMeta.loadRep(KettleUtils.getInstanceRep(), null, jobMeta.findJobEntry(jobEntryName).getEntry().getObjectId(), KettleUtils.getInstanceRep().readDatabases(),null); } catch (KettleException e) { log.error("获取作业控件失败", e); } return jobEntryMeta; } /** * saveTrans:保存转换. <br/> * @author jingma * @param transMeta 转换元数据 * @throws KettleException * @since JDK 1.6 */ public static void saveTrans(TransMeta transMeta) throws KettleException { // repository.save(transMeta, null, new RepositoryImporter(repository), true ); repository.save(transMeta, null, null, true ); } /** * saveJob:保存job. <br/> * @author jingma * @param jobMeta job元数据 * @throws KettleException * @since JDK 1.6 */ public static void saveJob(JobMeta jobMeta) throws KettleException { // repository.save(jobMeta, null, new RepositoryImporter(repository), true ); repository.save(jobMeta, null, null, true ); } /** * isDirectoryExist:判断指定的job目录是否存在. <br/> * @author jingma * @param directoryName * @return * @since JDK 1.6 */ public static boolean isDirectoryExist(String directoryName) { try { RepositoryDirectoryInterface dir = repository.findDirectory(directoryName); if(dir==null){ return false; }else{ return true; } } catch (KettleException e) { log.error("判断job目录是否存在失败!",e); } return false; } /** * 将步骤smi设置到转换trans中<br/> * @author jingma * @param teans 转换元数据 * @param stepName 步骤名称 * @param smi 步骤 */ public static void setStepToTrans(TransMeta teans, String stepName, StepMetaInterface smi) { try { StepMeta step = teans.findStep(stepName); step.setStepMetaInterface(smi); } catch (Exception e) { log.error("将步骤smi设置到转换trans中-失败",e); } } /** * 将步骤smi设置到转换trans中并保存到资源库 <br/> * @author jingma * @param teans 转换元数据 * @param stepName 步骤名称 * @param smi 步骤 */ public static void setStepToTransAndSave(TransMeta teans, String stepName, StepMetaInterface smi) { setStepToTrans( teans, stepName, smi); try { KettleUtils.saveTrans(teans); } catch (KettleException e) { log.error("将步骤smi设置到转换trans中并保存到资源库-失败",e); } } /** * 步骤数据预览 <br/> * @author jingma * @param teans 转换 * @param testStep 步骤名称 * @param smi 步骤实体 * @param previewSize 预览的条数 * @return 预览结果 */ public static List<List<Object>> stepPreview(TransMeta teans, String testStep, StepMetaInterface smi, int previewSize) { TransMeta previewMeta = TransPreviewFactory.generatePreviewTransformation( teans, smi, testStep); TransPreviewUtil tpu = new TransPreviewUtil( previewMeta, new String[] { testStep }, new int[] { previewSize } ); tpu.doPreview(); return TransPreviewUtil.getData(tpu.getPreviewRowsMeta(testStep),tpu.getPreviewRows(testStep)); } /** * 将指定job复制到KettleUtils中的资源库 <br/> * @author jingma * @param jobName job名称 * @param jobPath job路径 * @param repository 来源资源库 * @throws KettleException */ public static void jobCopy(String jobName,String jobPath,AbstractRepository repository) throws KettleException { JobMeta jobMeta = KettleUtils.loadJob(jobName,jobPath,repository); for(JobEntryCopy jec:jobMeta.getJobCopies()){ if(jec.isTransformation()){ JobEntryTrans jet = (JobEntryTrans)jec.getEntry(); transCopy(jet.getObjectName(), jet.getDirectory(),repository); }else if(jec.isJob()){ JobEntryJob jej = (JobEntryJob)jec.getEntry(); jobCopy(jej.getObjectName(),jej.getDirectory(),repository); } } jobMeta.setRepository(KettleUtils.getInstanceRep()); jobMeta.setMetaStore(KettleUtils.getInstanceRep().getMetaStore()); if(!isDirectoryExist(jobPath)){ //所在目录不存在则创建 KettleUtils.repository.createRepositoryDirectory(KettleUtils.repository.findDirectory("/"), jobPath); } KettleUtils.saveJob(jobMeta); } /** * 将指定转换复制到KettleUtils中的资源库 <br/> * @author jingma * @param jobName 转换名称 * @param jobPath 转换路径 * @param repository 来源资源库 * @throws KettleException */ public static void transCopy(String transName,String transPath,AbstractRepository repository) throws KettleException { TransMeta tm = KettleUtils.loadTrans(transName, transPath, repository); for(StepMeta sm:tm.getSteps()){ if(sm.isJobExecutor()){ JobExecutorMeta jem = (JobExecutorMeta)sm.getStepMetaInterface(); jobCopy(jem.getJobName(),jem.getDirectoryPath(),repository); } else if(sm.getStepMetaInterface() instanceof TransExecutorMeta){ TransExecutorMeta te = (TransExecutorMeta)sm.getStepMetaInterface(); transCopy(te.getTransName(), te.getDirectoryPath(),repository); } } if(!isDirectoryExist(transPath)){ //所在目录不存在则创建 KettleUtils.repository.createRepositoryDirectory(KettleUtils.repository.findDirectory("/"), transPath); } tm.setRepository(KettleUtils.getInstanceRep()); tm.setMetaStore(KettleUtils.getInstanceRep().getMetaStore()); KettleUtils.saveTrans(tm); } /** * @return transMetaTemplate */ public static TransMeta getTransMetaTemplate() { // if(transMetaTemplate==null){ // setTransMetaTemplate(KettleUtils.loadTrans(SysCode.TRANS_TEMPLATE_NAME, SysCode.TEMPLATE_DIR)); // } return transMetaTemplate; } /** * @param transMetaTemplate the transMetaTemplate to set */ public static void setTransMetaTemplate(TransMeta transMetaTemplate) { KettleUtils.transMetaTemplate = transMetaTemplate; } /** * @return jobMetaTemplate */ public static JobMeta getJobMetaTemplate() { // if(jobMetaTemplate==null){ // setJobMetaTemplate(KettleUtils.loadJob(SysCode.JOB_TEMPLATE_NAME, SysCode.TEMPLATE_DIR)); // } return jobMetaTemplate; } /** * @param jobMetaTemplate the jobMetaTemplate to set */ public static void setJobMetaTemplate(JobMeta jobMetaTemplate) { KettleUtils.jobMetaTemplate = jobMetaTemplate; } }
上面就是我定制化开发中编写的工具类,基本是一个独立可用的类,里面的模版jobMetaTemplate、transMetaTemplate可用删除,也可以根据需要使用。其中的步骤预览功能需要依赖另外一个类,这个类是我从kettleUI层提取出来并做了修改的,这里贴出代码吧:
/*! ****************************************************************************** * * Pentaho Data Integration * * Copyright (C) 2002-2013 by Pentaho : http://www.pentaho.com * ******************************************************************************* * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ******************************************************************************/ package com.iflytek.kettle.utils; import java.util.ArrayList; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.pentaho.di.core.Const; import org.pentaho.di.core.exception.KettleException; import org.pentaho.di.core.exception.KettleValueException; import org.pentaho.di.core.logging.KettleLogStore; import org.pentaho.di.core.row.RowMetaInterface; import org.pentaho.di.core.row.ValueMetaInterface; import org.pentaho.di.trans.Trans; import org.pentaho.di.trans.TransMeta; import org.pentaho.di.trans.debug.BreakPointListener; import org.pentaho.di.trans.debug.StepDebugMeta; import org.pentaho.di.trans.debug.TransDebugMeta; import org.pentaho.di.trans.step.StepMeta; /** * Takes care of displaying a dialog that will handle the wait while previewing a transformation... * * @author Matt * @since 13-jan-2006 */ public class TransPreviewUtil { public static final int MAX_BINARY_STRING_PREVIEW_SIZE = 1000000; private static Log log = LogFactory.getLog(TransPreviewUtil.class); private TransMeta transMeta; private String[] previewStepNames; private int[] previewSize; private Trans trans; private boolean cancelled; private String loggingText; private TransDebugMeta transDebugMeta; /** * Creates a new dialog that will handle the wait while previewing a transformation... */ public TransPreviewUtil( TransMeta transMeta, String[] previewStepNames, int[] previewSize ) { this.transMeta = transMeta; this.previewStepNames = previewStepNames; this.previewSize = previewSize; cancelled = false; } public void doPreview() { // This transformation is ready to run in preview! trans = new Trans( transMeta ); // Prepare the execution... // try { trans.prepareExecution( null ); } catch ( final KettleException e ) { log.error("", e); return; } // Add the preview / debugging information... // transDebugMeta = new TransDebugMeta( transMeta ); for ( int i = 0; i < previewStepNames.length; i++ ) { StepMeta stepMeta = transMeta.findStep( previewStepNames[i] ); StepDebugMeta stepDebugMeta = new StepDebugMeta( stepMeta ); stepDebugMeta.setReadingFirstRows( true ); stepDebugMeta.setRowCount( previewSize[i] ); transDebugMeta.getStepDebugMetaMap().put( stepMeta, stepDebugMeta ); } // set the appropriate listeners on the transformation... // transDebugMeta.addRowListenersToTransformation( trans ); // Fire off the step threads... start running! // try { trans.startThreads(); } catch ( final KettleException e ) { log.error("", e); // It makes no sense to continue, so just stop running... // return; } final List<String> previewComplete = new ArrayList<String>(); while ( previewComplete.size() < previewStepNames.length && !trans.isFinished() ) { // We add a break-point that is called every time we have a step with a full preview row buffer // That makes it easy and fast to see if we have all the rows we need // transDebugMeta.addBreakPointListers( new BreakPointListener() { public void breakPointHit( TransDebugMeta transDebugMeta, StepDebugMeta stepDebugMeta, RowMetaInterface rowBufferMeta, List<Object[]> rowBuffer ) { String stepName = stepDebugMeta.getStepMeta().getName(); previewComplete.add( stepName ); } } ); // Change the percentage... try { Thread.sleep( 500 ); } catch ( InterruptedException e ) { log.error("", e); // Ignore errors } } trans.stopAll(); // Capture preview activity to a String: loggingText = KettleLogStore.getAppender().getBuffer( trans.getLogChannel().getLogChannelId(), true ).toString(); } /** * @param stepname * the name of the step to get the preview rows for * @return A list of rows as the result of the preview run. */ public List<Object[]> getPreviewRows( String stepname ) { if ( transDebugMeta == null ) { return null; } for ( StepMeta stepMeta : transDebugMeta.getStepDebugMetaMap().keySet() ) { if ( stepMeta.getName().equals( stepname ) ) { StepDebugMeta stepDebugMeta = transDebugMeta.getStepDebugMetaMap().get( stepMeta ); return stepDebugMeta.getRowBuffer(); } } return null; } /** * @param stepname * the name of the step to get the preview rows for * @return A description of the row (metadata) */ public RowMetaInterface getPreviewRowsMeta( String stepname ) { if ( transDebugMeta == null ) { return null; } for ( StepMeta stepMeta : transDebugMeta.getStepDebugMetaMap().keySet() ) { if ( stepMeta.getName().equals( stepname ) ) { StepDebugMeta stepDebugMeta = transDebugMeta.getStepDebugMetaMap().get( stepMeta ); return stepDebugMeta.getRowBufferMeta(); } } return null; } /** * @return true is the preview was canceled by the user */ public boolean isCancelled() { return cancelled; } /** * @return The logging text from the latest preview run */ public String getLoggingText() { return loggingText; } /** * * @return The transformation object that executed the preview TransMeta */ public Trans getTrans() { return trans; } /** * @return the transDebugMeta */ public TransDebugMeta getTransDebugMeta() { return transDebugMeta; } /** * Copy information from the meta-data input to the dialog fields. * @param rowMetaInterface */ public static List<List<Object>> getData(RowMetaInterface rowMeta, List<Object[]> buffer) { List<List<Object>> result = new ArrayList<List<Object>>(); List<Object> row1 = new ArrayList<Object>(); for ( int i = 0; i < buffer.size(); i++ ) { row1 = new ArrayList<Object>(); Object[] row = buffer.get( i ); getDataForRow( rowMeta, row1, row ); result.add(row1); } return result; } public static int getDataForRow( RowMetaInterface rowMeta, List<Object> row1, Object[] row ) { int nrErrors = 0; // Display the correct line item... // for ( int c = 0; c < rowMeta.size(); c++ ) { ValueMetaInterface v = rowMeta.getValueMeta( c ); String show; try { show = v.getString( row[c] ); if ( v.isBinary() && show != null && show.length() > MAX_BINARY_STRING_PREVIEW_SIZE ) { // We want to limit the size of the strings during preview to keep all SWT widgets happy. // show = show.substring( 0, MAX_BINARY_STRING_PREVIEW_SIZE ); } } catch ( KettleValueException e ) { nrErrors++; if ( nrErrors < 25 ) { log.error( Const.getStackTracker( e ) ); } show = null; } catch ( ArrayIndexOutOfBoundsException e ) { nrErrors++; if ( nrErrors < 25 ) { log.error( Const.getStackTracker( e ) ); } show = null; } if ( show != null ) { row1.add(show); } else { // Set null value row1.add("<null>"); } } return nrErrors; } }
以上两个类复制到自己项目中就可以用了,这个工具类是根据我自己需求编写的,根据使用场景不同,你也可以添加其他方法,丰富该类的功能。
关于使用示例,本来也想贴出来的,但相关代码依赖较多,同时工具类本身注释很详细,方法体很小,自己看一遍就能了解个大概,所以这里就不提供使用示例代码,希望对有需要的人有所帮助。