0.背景
为什么会想到把这三个整合在一起? 当然是工作中遇到不舒服的地方。
最近数据的需求特别多,有时候自己定位问题也经常要跑数据,通常就是spark+scala的常规画风。虽然是提同一个jar包,但执行的每个包的路径都不一样,这就导致我要不断的去改脚本,很不舒服。提交spark job的画风通常是这样子的:
spark-submit --cluster hadoop-spark2.0 \
--class com.acceml.hit.User.PvCount \
"xxx.jar" ${params1} ${params2} ${params3}
spark-submit --cluster hadoop-spark2.0 \
--class com.acceml.hit.ShanghaiUser.UvCount \
"xxx.jar" ${params1} ${params2} ${params3}
spark-submit --cluster hadoop-spark2.0 \
--class com.acceml.hit.User.xxx.View \
"xxx.jar" ${params1} ${params2} ${params3}
用spring整合了一下,提交一个job只要指定它执行的类名即可。如下,三条命令分别解析pv、uv、曝光...
sh log_parser.sh PvCount 20180412
sh log_parser.sh UvCount 20180412
sh log_parser.sh View 20180412
1.实现
1.1 思路
说白了,这上面就是不需要指定包路径,想让程序根据类名执行相应的逻辑,利用控制反转在spring中简直再简单不过了。java代码如下:
@Service
public class TaskEngine {
//定义task名字到Task的一个映射
private final Map<String, Task> name2Task = new HashMap<>();
//自动注入所有Task的子类,这里task只是一个interface.
@Autowired
public TaskEngine(List<Task> tasks) {
tasks.forEach(task -> name2Task.put(task.getClass().getSimpleName(), task));
}
public static void main(String[] args) throws Exception {
//spring注入
ApplicationContext appContext = new ClassPathXmlApplicationContext("applicationContext.xml");
TaskEngine taskEngine = (TaskEngine) appContext.getBean("taskEngine");
//根据指定参数跑job.
taskEngine.name2Task.get(args[0]).runTask();
}
由于spark开发中我不喜欢用java,写起来太冗长,虽然java8支持lambda表达式,但是java版本和spark兼容的问题可能又是一个坑,用scala写job提交也可以借鉴自动注入,然后根据参数(类名)去选择提交哪个job,就此开始了如下尝试。
1.2 scala+srping实现依赖注入的功能
1、定义一个scala的trait:
trait LogParser extends Serializable {
def name(): String = {
this.getClass.getSimpleName
}
def run(params: ParseParams, sparkSession: SparkSession)
}
2、所有要执行的业务逻辑程序都实现它:
@Component
class DemoParser extends LogParser {
override def run(params: ParseParams, sparkSession: SparkSession): Unit = {
//业务逻辑.
}
}
3、定义一个LogparserFactory的Bean用spring的自动注入把LogParser所有子类注入进来。
@Service
class LogParserFactory {
private var logParsers: java.util.List[LogParser] = _
//自动注入所有子类.
@Autowired
def this(list: java.util.List[LogParser]) {
this()
logParsers = list
}
}
4、spark主程序加载所有Bean,并选择所需要的逻辑去执行.
//spring注入
val appContext = new ClassPathXmlApplicationContext("applicationContext.xml")
//获取得到相应的Bean
val logParserFactory = appContext.getBean("logParserFactory").asInstanceOf[LogParserFactory]
logParserFactory.getLogParsers()
.filter(e => e.name().equals(className))
.foreach(e => e.run(params, sparkSession))
5、新来了一个需求就啥都不用改,再写一个类就可以了.
@Component
class View extends LogParser {
override def run(params: ParseParams, sparkSession: SparkSession): Unit = {
//业务逻辑.
}
}
2.遇到的坑
2.1 spring版本不统一.
其他的依赖包里面经常会依赖不同版本的spring,导致程序运行时报错NoClassDefFoundError.这个时候用mvn dependency:tree查看一下。exclude掉其他version的spring就好了。
2.2 spring的xsi规则配置
由于spring是在spark集群中跑,xsi有可能定义为http形式可能获取不到,所以指定到classpath的路径下本地获取:
xsi:schemaLocation="http://www.springframework.org/schema/beans
classpath:/org/springframework/beans/factory/xml/spring-beans-4.1.xsd
http://www.springframework.org/schema/context
classpath:/org/springframework/context/config/spring-context-4.1.xsd">
3.源码
Acceml/offline_job_babelgithub.com
4.约束
- 类名不能重复,不然会报错,这是spring的性质决定的.