文章目录
  1. 1. Overview
  2. 2. spring-boot
    1. 2.1. 整合spring-boot和dubbo
    2. 2.2. spring-boot中配置Hikari-CP
  3. 3. sbt-assembly合并策略
    1. 3.1. 修改包名大法
    2. 3.2. 合并策略
  4. 4. finatra
    1. 4.1. module
  5. 5. slick
    1. 5.1. domain
    2. 5.2. entity
    3. 5.3. dao
  6. 6. future&promise
  7. 7. Conclusion

Overview

话说前段时间在用spring-boot的时候,还想专门写一篇学习笔记,不过后来嫌麻烦就弃坑了,所以挪到这里简单谈一下好了。最近做了一个服务,最开始用spring-boot,写起来还算简单,但是感觉spring各种约定俗成太多了,如果要想用好需要看的东西太多了,尽管并不耽误做出来,但不求甚解心里还是不踏实。

后来无聊看了一下twitter的finatra,发现比akka-http/spray简单的多,也更符合常规restful框架的结构,于是用finatra重写了一遍。虽然finatra也有很多不尽如人意的地方,但总体感觉还算满意,写的代码多了不少,但是总体来说有那种一切掌握在手中的感觉。

spring-boot

搞Java那还是在5年前上大学的时候,那个时候spring印象中还是2.x,那个时候用spring还要写一大堆的xml配置,简直蛋疼无比。用Java做web开发也是各种蛋疼,还有各种框架,除了spring还有structs/hibernate/ibatis等等。因为团队都是做Java的,而且前段时间也接手了几个SpringMVC的模块,才知道Spring已经进入了4.x的时代了,而且>也有了很大改变,更适合于现代互联网web开发了,也不用写什么xml了。所以准备找时间重新学习一下spring4,而且spring.io新推出了一个叫做spring-boot的“微框架”,更适合于做快速web开发。

总的来说spring-boot还是不错的,可以在写很少的代码的情况下完成很多功能,也有一套自己的模板,比如spring-data-cassandra用来读写cassandra都非常方便。

整合spring-boot和dubbo

之前的项目里用了dubbo,作为一个纯Java的RPC框架,dubbo用起来还是不错的,阿里开源的,侵入性也很小,基本上只要写spring的xml配置文件就好。不过dubbo已经很久没有更新过了,在整合到spring-boot中的时候,花了很多力气,主要也是因为自己对spring-boot的理解不到位。

之前一直用@SpringBootApplication的,没有写xml,但是dubbo是以xml的方式配的,而如果单加一个bean来启动dubbo的context又会无法注入其他bean,所以这里还是提供一个xml,让spring-boot在启动时读取并初始化dubbo。用@ImportResource({"classpath:META-INF/spring/application-context.xml"})即可指定,而在该配置文件中引入dubbo的配置文件即可。

1
<import resource="classpath:dubbo-provider.xml" />

spring-boot中配置Hikari-CP

spring-boot中默认用的是数据库连接池是tomcat-cp,但是tomcat-cp的性能真是渣,既然有号称’the ultimate connection pool’之称的Hikari-CP为啥不用呢,替换也比较简单,在配置文件里指定Hikari-CP的datasourceClass,然后再写一个BeanConfig即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package com.odinliu.dao;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@ComponentScan
public class DataSourceConfig {
@Value("${spring.datasource.username}")
private String user;
@Value("${spring.datasource.password}")
private String password;
@Value("${spring.datasource.url}")
private String dataSourceUrl;
@Value("${spring.datasource.dataSourceClassName}")
private String dataSourceClassName;
@Value("${spring.datasource.poolName}")
private String poolName;
@Value("${spring.datasource.connectionTimeout}")
private int connectionTimeout;
@Value("${spring.datasource.maxLifetime}")
private int maxLifetime;
@Value("${spring.datasource.maximumPoolSize}")
private int maximumPoolSize;
@Value("${spring.datasource.minimumIdle}")
private int minimumIdle;
@Value("${spring.datasource.idleTimeout}")
private int idleTimeout;
@Value("${spring.datasource.prepStmtCacheSize}")
private int prepStmtCacheSize;
@Value("${spring.datasource.prepStmtCacheSqlLimit}")
private int prepStmtCacheSqlLimit;
@Bean
public DataSource primaryDataSource() {
Properties dsProps = new Properties();
dsProps.put("url", dataSourceUrl);
dsProps.put("user", user);
dsProps.put("password", password);
dsProps.put("prepStmtCacheSize", prepStmtCacheSize);
dsProps.put("prepStmtCacheSqlLimit", prepStmtCacheSqlLimit);
dsProps.put("cachePrepStmts", Boolean.TRUE);
dsProps.put("useServerPrepStmts", Boolean.TRUE);
Properties configProps = new Properties();
configProps.put("dataSourceClassName", dataSourceClassName);
configProps.put("poolName", poolName);
configProps.put("maximumPoolSize", maximumPoolSize);
configProps.put("minimumIdle", minimumIdle);
configProps.put("minimumIdle", minimumIdle);
configProps.put("connectionTimeout", connectionTimeout);
configProps.put("idleTimeout", idleTimeout);
configProps.put("dataSourceProperties", dsProps);
HikariConfig hc = new HikariConfig(configProps);
HikariDataSource ds = new HikariDataSource(hc);
return ds;
}
}

sbt-assembly合并策略

Java里面最蛋疼的问题莫过于依赖冲突,引入各种包的冲突还可以用exclude解决,但是在打’fat-jar’时遇到冲突简直蛋疼菊紧。不过还在sbt可以自己写合并策略,这里记录几个常用的策略吧。

修改包名大法

之前在spark中访问cassandra时遇到过一个guava版本冲突的问题,CDH依赖的guava版本比较低,而cassandra需要调用新版本的接口,导致运行时异常,其实只要修改一下自己assembly的包名即可。

1
2
3
assemblyShadeRules in assembly := Seq(
ShadeRule.rename("com.google.**" -> "[email protected]").inAll
)

合并策略

netty是很多Java/Scala第三方库都会以来的一个库,但是这个包会蛋疼的带一个io.netty.versions.properties文件,而这个文件各种冲突,其实只要带一个就ok了,这里可以写一个策略。除此之外如果还有包的冲突,也可以指定合并策略,具体如下:

1
2
3
4
5
6
assemblyMergeStrategy in assembly := {
case "BUILD" => MergeStrategy.discard
case m if m.endsWith("io.netty.versions.properties") => MergeStrategy.last
case PathList("org", "aopalliance", xs @ _*) => MergeStrategy.first
case other => MergeStrategy.defaultMergeStrategy(other)
}

finatra

finatra是twitter开源的一个web框架,基于twitter的finagle和twitter-server进行开发,这个框架更像是“使用twitter开源库开发app”的demo,不清楚在twitter内部用的多不多。不过大概用了一下,感觉还可以,尽管很多功能都要自己写,但大体上还是能用的。finatra是模仿sinatra进行开发的,twitter之前是用的ruby,因此才会开发一套finatra吧。

finatra使用Google Guice作为IOC框架,比spring轻量许多。因为twitter-server自带admin监控台,所以对于服务可控性做的还是挺不错的。finatra中主要有三个概念,module/controller/filter。

module

可以看做是提供Guice依赖注入实例的对象,可以在module中管理相关的对象,而其他类中只要注入就好。

slick

在选用了finatra作为web框架之后,因为finatra只有web框架,并没有持久层框架,所以一番考虑之后还是选择了更Scala的slick作为持久层框架,一方面是slick就是用scala开发的,更idiomastic一些。另外一方面,slick是lightbend(typesafe)支持的框架,更官方一些。slick可以通过sql生成定义类,当然也可以手写,这里来个手写的例子。

domain

1
case class TagDetail(id: Long, name: String, privilege: Option[Int], source: Option[String], tagType: Option[Int])

entity

1
2
3
4
5
6
7
8
class TagTable(tag: Tag) extends Table[TagDetail](tag, "uds_tag") {
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
def privilege = column[Option[Int]]("privilege", O.Default[Option[Int]](Some(0)))
def source = column[Option[String]]("source", O.Default[Option[String]](Some("小麦公社")))
def tagType = column[Option[Int]]("type", O.Default[Option[Int]](Some(1)))
def * = (id, name, privilege, source, tagType) <> (TagDetail.tupled, TagDetail.unapply)
}

dao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Singleton
class TagDao @Inject() (db: MySQLDriver.backend.DatabaseDef, @DBExecutionContext dbec: ExecutionContext) extends Logging {
implicit val ec = dbec
val tableQuery = TableQuery[TagTable]
val pagedQuery = Compiled((d: ConstColumn[Long], t: ConstColumn[Long]) => tableQuery.drop(d).take(t))
def getTagById(tid: Long) = {
val q = tableQuery.filter(_.id === tid)
db.run(q.result)
}
def insertTag(tag: TagDetail) = {
//val q = tableQuery.map(x => (x.name, x.privilege, x.source, x.tagType)) += (tag.name, tag.privilege, tag.source, tag.tagType)
val q = tableQuery returning tableQuery.map(_.id) into ((ret, id) => ret.copy(id = id)) += tag
db.run(q)
}
def getAllTags = {
db.run(tableQuery.result)
}
def getTagsByPage(drop: Long, take: Long) = {
db.run(pagedQuery(drop, take).result)
}
def getCount = {
db.run(tableQuery.length.result)
}
def deleteTagById(id: Long) = {
db.run(tableQuery.filter(_.id === id).delete)
}
def updatePrivilegeById(id: Long, privilege: Int) = {
val q = for { t <- tableQuery if t.id === id } yield t.privilege
db.run(q.update(Some(privilege)))
}
def updateNameById(id: Long, name: String) = {
val q = for { t <- tableQuery if t.id === id } yield t.name
db.run(q.update(name))
}
def updateTagById(tag: TagDetail) = {
val q = for { t <- tableQuery if t.id === tag.id } yield (t.name, t.privilege, t.source, t.tagType)
db.run(q.update((tag.name, tag.privilege, tag.source, tag.tagType)))
}
}

future&promise

最早scala的并发范式是actor,后来akka发展太好了,最新版本的scala已经把actor从标准库移除了。不过scala仍然提供新的并发范式future。future基本上就是异步非阻塞回调的范式,说实话我不太喜欢异步回调,简直反人类。在scala项目中用cassandra,其实是没有官方库的,因此用的java的driver,但为了和slick的异步保持风格统一,因此用future/promise去封装了一下,简单例子如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Singleton
class UserIdDao @Inject()(accessor: UserIdAccessor,
@CassandraExecutionContext cassandraExecutionContext: ExecutionContext)
extends Logging {
implicit val ec = cassandraExecutionContext
def getRowkey(key: String): Future[Option[String]] = {
val p = Promise[Option[String]]
Future {
try {
val user = accessor.getRowkeyByKey(key)
if (user == null) {
p.success(None)
} else {
p.success(Some(user.getRowkey))
}
} catch {
case e: Exception =>
warn(s"get [$key] rowkey failed, $e")
p.failure(e)
}
}
p.future
}
}

future本来是期货的意思,这里其实挺贴近的,表示将来可以拿到的结果。另外一个option,是期权的意思,其实也是表示这个结果可能拿得到,可能拿不到。用这俩单词来表达对应的概念简直233333.

Conclusion

总得来说Scala还是一门很有表现力的语言的,特别是twitter的一些服务从ruby转向scala之后,给scala社区带来了很多工业界的想法,也让这门语言更贴近于生产。尽管最近看到linkedin从scala转向java8,因为这样那样的理由,但不得不说scala作为后来者没有那么多历史包袱,更容易成为一门具有生产力的语言。当然sbt确实慢的不行行了,还有二进制包版本不兼容之类的问题也是很麻烦。

还是希望lightbend/typesafe多投入一些精力在scala/sbt上,少一些商业的东西,让社区环境更好。

文章目录
  1. 1. Overview
  2. 2. spring-boot
    1. 2.1. 整合spring-boot和dubbo
    2. 2.2. spring-boot中配置Hikari-CP
  3. 3. sbt-assembly合并策略
    1. 3.1. 修改包名大法
    2. 3.2. 合并策略
  4. 4. finatra
    1. 4.1. module
  5. 5. slick
    1. 5.1. domain
    2. 5.2. entity
    3. 5.3. dao
  6. 6. future&promise
  7. 7. Conclusion