Java/Scala杂记之三


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的配置文件即可。

<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即可。

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的包名即可。

assemblyShadeRules in assembly := Seq(
  ShadeRule.rename("com.google.**" -> "shadeio.@1").inAll
)

合并策略

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

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

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

entity

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

@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去封装了一下,简单例子如下。

@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上,少一些商业的东西,让社区环境更好。


文章作者: Odin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Odin !
  目录