浅谈Slick(4)- Slick301:我的Slick开发项目设置
前面几篇介绍里尝试了一些Slick的功能和使用方式,看来基本可以满足用scala语言进行数据库操作编程的要求,而且有些代码可以通过函数式编程模式来实现。我想,如果把Slick当作数据库操作编程主要方式的话,可能需要先制定一套比较规范的模式来应付日常开发(也要考虑团队开发)、测试和维护。首先从项目结构来说,我发现由Intellij-Idea IDE界面直接产生的SBT项目结构已经比较理想了。在src/main/resources是scala项目获取配置文件的默认目录、我们可以按照需要在src/main/scala下增加代码子目录(package)及在src/main/test下摆放测试代码。配置文件application.conf、logback.xml是放在src/main/resources下的。application.conf是Slick的配置文件,logback.xml是跟踪器logback(log4j)的配置文件。Slick把jdbc api集成到scala编程语言里,能够支持多种数据库。也就是说Slick提供了多种数据库的驱动api。Slick支持在配置文件application.conf里配置数据库功能模式,这样我们就可以在正式部署软件时才通过修订application.conf里的配置来决定具体的数据库种类和参数。当然前提是我们的程序代码不能依赖任何特别的数据库api。我们从表结构设定开始,先看看上篇Slick101里的例子:
package com.datatech.learn.slick101
import slick.driver.H2Driver.api._
object slick101 { /* ----- schema */
//表字段对应模版
case class AlbumModel (id: Long
,title: String
,year: Option[Int]
,artist: String
)
//表结构: 定义字段类型, * 代表结果集字段
class AlbumTable(tag: Tag) extends Table[AlbumModel](tag, "ALBUMS") {
def id = column[Long]("ID",O.AutoInc,O.PrimaryKey)
def title = column[String]("TITLE")
def year = column[Option[Int]]("YEAR")
def artist = column[String]("ARTIST",O.Default("Unknown"))
def * = (id,title,year,artist) <> (AlbumModel.tupled, AlbumModel.unapply)
}
//库表实例
val albums = TableQuery[AlbumTable]
我们可以看到这段代码依赖了slick.driver.H2Driver.api,是专门针对H2 Database的了。我们可以用依赖注入(dependency injection, IOC)来解决这个依赖问题。先试试用最传统的依赖注入方式:传入参数来注入这个数据库驱动依赖,把代码放在src/main/scala/model/TableDefs.scala里:
package com.bayakala.learn.slick301.model
import slick.driver.JdbcProfile
class TableDefs(val dbDriver: JdbcProfile) {
import dbDriver.api._
case class Supplier(id: Long
, name: String
, contact: Option[String]
, website: Option[String])
final class Suppliers(tag: Tag) extends Table[Supplier](tag,"SUPPLERS") {
def id = column[Long]("ID",O.AutoInc,O.PrimaryKey)
def name = column[String]("NAME")
def contact = column[Option[String]]("CONTACT")
def website = column[Option[String]]("WEBSITE")
def * = (id, name, contact, website) <> (Supplier.tupled,Supplier.unapply)
def nidx = index("NM_IDX",name,unique = true)
}
val suppliers = TableQuery[Suppliers] case class Coffee(id: Long
,name: String
,supid: Long
,price: Double
,sales: Int)
final class Coffees(tag: Tag) extends Table[Coffee](tag, "COFFEES") {
def id = column[Long]("ID",O.AutoInc,O.PrimaryKey)
def name = column[String]("NAME")
def supid = column[Long]("SUPID")
def price = column[Double]("PRICE",O.Default(0.0))
def sales = column[Int]("SALES",O.Default())
def * = (id,name,supid,price,sales) <> (Coffee.tupled, Coffee.unapply)
def fk_sup = foreignKey("FK_SUP",supid,suppliers)(_.id,onDelete = ForeignKeyAction.Restrict,onUpdate = ForeignKeyAction.Cascade)
def supidx = index("SUP_IDX",supid,unique = false)
def nidx = index("NM_IDX",name,unique = true)
}
val coffees = TableQuery[Coffees] }
注意我们是把JdbcProfile作为参数注入了class TableDefs里。如果TableDefs经常需要作为其它类的父类继承的话,设计成trait能更加灵活的进行类型混合(type mixing)。这样的需求可以用cake pattern方式进行依赖注入。我们在需要src/main/scala/config/AppConfig.scala里定义依赖界面trait DBConfig:
package com.bayakala.learn.slick301.config
import slick.driver.JdbcProfile
trait DBConfig {
val jdbcDriver: JdbcProfile
import jdbcDriver.api._
val db: Database
}
后面我们可以通过实现多种DBConfig实例方式来构建开发、测试、部署等数据库环境。为了方便示范,我们设计几个基本的Query Action,放在src/main/scala/access/DAOs.scala里,用cake pattern注入依赖DBConfig:
package com.bayakala.learn.slick301.access
import com.bayakala.learn.slick301.config
import com.bayakala.learn.slick301.config.DBConfig
import com.bayakala.learn.slick301.model.TableDefs
trait DAOs { dbconf: DBConfig =>
import jdbcDriver.api._
//注入依赖
val tables = new TableDefs(dbconf.jdbcDriver)
import tables._
//suppliers queries
val createSupplierTable = suppliers.schema.create
val allSuppliers = suppliers.result
def insertSupplier(id:Long,name:String,address:Option[String],website:Option[String])
= suppliers += Supplier(id,name,address,website)
def insertSupbyName(n: String) = suppliers.map(_.name) += n
//coffees queries
val createCoffeeTable = coffees.schema.create
val allCoffees = coffees.result
def insertCoffee(c: (Long,String,Long,Double,Int)) =
coffees += Coffee(id=c._1, name=c._2,supid=c._3,price=c._4,sales=c._5) }
dbconf: DBConfig => 的意思是在进行DAOs的实例化时必须混入(mixing)DBConfig类。
以上两个代码文件TableDefs.scala和DAOs.scala在注入依赖后都能够顺利通过编译了。
我们在src/main/scala/main/Main.scala里测试运算DAOs里的query action:
package com.bayakala.learn.slick301.main
import com.bayakala.learn.slick301.config.DBConfig
import com.bayakala.learn.slick301.access.DAOs import scala.concurrent.{Await, Future}
import scala.util.{Failure, Success}
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import slick.backend.DatabaseConfig
import slick.driver.{H2Driver, JdbcProfile}
object Main { object Actions extends DAOs with DBConfig {
override lazy val jdbcDriver: JdbcProfile = H2Driver
val dbConf: DatabaseConfig[H2Driver] = DatabaseConfig.forConfig("h2")
override val db = dbConf.db
}
import Actions._ def main(args: Array[String]) = {
val res = db.run(createSupplierTable).andThen {
case Success(_) => println("supplier table created")
case Failure(_) => println("unable to create supplier table")
}
Await.ready(res, seconds) val res2 = db.run(insertSupbyName("Acme Coffee Co."))
Await.ready(res2, seconds) Await.ready(db.run(allSuppliers), seconds).foreach(println) val res10 = db.run(createCoffeeTable).andThen {
case Success(_) => println("coffee table created")
case Failure(_) => println("unable to create coffee table")
}
Await.ready(res10, seconds) val res11 = db.run(insertCoffee((,"Columbia",,158.0,)))
Await.ready(res11, seconds) Await.ready(db.run(allCoffees), seconds).foreach(println) } }
Actions是DAOs的实例。我们看到必须把DBConfig混入(mixin)。但是我们构建的数据库又变成了专门针对H2的api了,这样的话每次变动数据库对象我们就必须重新编译Main.scala,不符合上面我们提到的要求。我们可以把目标数据库放到application.conf里,然后在Main.scala里用typesafe-config实时根据application.conf里的设置确定数据库参数。src/main/resources/application.conf内容如下:
app = {
dbconfig = h2
}
h2 {
driver = "slick.driver.H2Driver$"
db {
url = "jdbc:h2:~/slickdemo;mv_store=false"
driver = "org.h2.Driver"
connectionPool = HikariCP
numThreads =
maxConnections =
minConnections =
keepAliveConnection = true
}
}
h2mem = {
url = "jdbc:h2:mem:slickdemo"
driver = org.h2.Driver
connectionPool = disabled
keepAliveConnection = true
}
mysql {
driver = "slick.driver.MySQLDriver$"
db {
url = "jdbc:mysql://localhost/slickdemo"
driver = com.mysql.jdbc.Driver
keepAliveConnection = true
user="root"
password=""
numThreads=
maxConnections =
minConnections =
}
}
mysqldb = {
dataSourceClass = "com.mysql.jdbc.jdbc2.optional.MysqlDataSource"
properties {
user = "root"
password = ""
databaseName = "slickdemo"
serverName = "localhost"
}
numThreads =
maxConnections =
minConnections =
}
postgres {
driver = "slick.driver.PostgresDriver$"
db {
url = "jdbc:postgresql://127.0.0.1/slickdemo"
driver = "org.postgresql.Driver"
connectionPool = HikariCP
user = "slick"
password = ""
numThreads =
maxConnections =
minConnections =
}
}
postgressdb = {
dataSourceClass = "org.postgresql.ds.PGSimpleDataSource"
properties = {
databaseName = "slickdemo"
user = "slick"
password = ""
}
connectionPool = HikariCP
numThreads =
maxConnections =
minConnections =
}
mssql {
driver = "com.typesafe.slick.driver.ms.SQLServerDriver$"
db {
url = "jdbc:sqlserver://host:port"
driver = com.microsoft.sqlserver.jdbc.SQLServerDriver
connectionTimeout = second
connectionPool = HikariCP
user = "slick"
password = ""
numThreads =
maxConnections =
minConnections =
keepAliveConnection = true
}
}
tsql {
driver = "slick.driver.H2Driver$"
db = ${h2mem}
}
现在application.conf里除了数据库配置外又加了个app配置。我们在Main.scala里实例化DAOs时可以用typesafe-config读取app.dbconfig值后设定jdbcDriver和db:
object Actions extends DAOs with DBConfig {
import slick.util.ClassLoaderUtil
import scala.util.control.NonFatal
import com.typesafe.config.ConfigFactory
def getDbConfig: String =
ConfigFactory.load().getString("app.dbconfig")
def getDbDriver(path: String): JdbcProfile = {
val config = ConfigFactory.load()
val n = config.getString((if (path.isEmpty) "" else path + ".") + "driver")
val untypedP = try {
if (n.endsWith("$")) ClassLoaderUtil.defaultClassLoader.loadClass(n).getField("MODULE$").get(null)
else ClassLoaderUtil.defaultClassLoader.loadClass(n).newInstance()
} catch {
case NonFatal(ex) =>
throw new SlickException(s"""Error getting instance of Slick driver "$n"""", ex)
}
untypedP.asInstanceOf[JdbcProfile]
}
override lazy val jdbcDriver: JdbcProfile = getDbDriver(getDbConfig)
val dbConf: DatabaseConfig[JdbcProfile] = DatabaseConfig.forConfig(getDbConfig)
override val db = dbConf.db
}
现在我们只需要改变application.conf里的app.dbconfig就可以转换目标数据库参数了。实际上,除了数据库配置,我们还可以在application.conf里进行其它类型的配置。然后用typesafe-config实时读取。如果不想在application.conf进行数据库之外的配置,可以把其它配置放在任何文件里,然后用ConfigFactory.load(path)来读取。
另外,在软件开发过程中跟踪除错也是很重要的。我们可以用logback来跟踪Slick、HikariCP等库的运行状态。logback配置在src/main/resources/logback.xml:
<?xml version="1.0" encoding="UTF-8"?> <configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{} - %msg%n</pattern>
</encoder>
</appender> <logger name="application" level="DEBUG"/>
<logger name="com.zaxxer.hikari" level="DEBUG"/>
<logger name="slick" level="DEBUG"/> <root level="DEBUG">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
DEBUG值可以显示最详细的状态信息。
好了,我把这次示范代码提供在下面:
build.sbt:
name := "learn-slick301" version := "1.0" scalaVersion := "2.11.8" libraryDependencies ++= Seq(
"com.typesafe.slick" %% "slick" % "3.1.1",
"com.h2database" % "h2" % "1.4.191",
"com.typesafe.slick" %% "slick-hikaricp" % "3.1.1",
"ch.qos.logback" % "logback-classic" % "1.1.7",
"org.typelevel" %% "cats" % "0.7.2" )
src/main/resources/
application.conf:
app = {
dbconfig = h2
}
h2 {
driver = "slick.driver.H2Driver$"
db {
url = "jdbc:h2:~/slickdemo;mv_store=false"
driver = "org.h2.Driver"
connectionPool = HikariCP
numThreads =
maxConnections =
minConnections =
keepAliveConnection = true
}
}
h2mem = {
url = "jdbc:h2:mem:slickdemo"
driver = org.h2.Driver
connectionPool = disabled
keepAliveConnection = true
}
mysql {
driver = "slick.driver.MySQLDriver$"
db {
url = "jdbc:mysql://localhost/slickdemo"
driver = com.mysql.jdbc.Driver
keepAliveConnection = true
user="root"
password=""
numThreads=
maxConnections =
minConnections =
}
}
mysqldb = {
dataSourceClass = "com.mysql.jdbc.jdbc2.optional.MysqlDataSource"
properties {
user = "root"
password = ""
databaseName = "slickdemo"
serverName = "localhost"
}
numThreads =
maxConnections =
minConnections =
}
postgres {
driver = "slick.driver.PostgresDriver$"
db {
url = "jdbc:postgresql://127.0.0.1/slickdemo"
driver = "org.postgresql.Driver"
connectionPool = HikariCP
user = "slick"
password = ""
numThreads =
maxConnections =
minConnections =
}
}
postgressdb = {
dataSourceClass = "org.postgresql.ds.PGSimpleDataSource"
properties = {
databaseName = "slickdemo"
user = "slick"
password = ""
}
connectionPool = HikariCP
numThreads =
maxConnections =
minConnections =
}
mssql {
driver = "com.typesafe.slick.driver.ms.SQLServerDriver$"
db {
url = "jdbc:sqlserver://host:port"
driver = com.microsoft.sqlserver.jdbc.SQLServerDriver
connectionTimeout = second
connectionPool = HikariCP
user = "slick"
password = ""
numThreads =
maxConnections =
minConnections =
keepAliveConnection = true
}
}
tsql {
driver = "slick.driver.H2Driver$"
db = ${h2mem}
}
logback.xml:
<?xml version="1.0" encoding="UTF-8"?> <configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{} - %msg%n</pattern>
</encoder>
</appender> <logger name="application" level="DEBUG"/>
<logger name="com.zaxxer.hikari" level="DEBUG"/>
<logger name="slick" level="DEBUG"/> <root level="DEBUG">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
src/main/scala/config/AppConfig.scala:
package com.bayakala.learn.slick301.config
import slick.driver.JdbcProfile
trait DBConfig {
val jdbcDriver: JdbcProfile
import jdbcDriver.api._
val db: Database
}
src/main/scala/model/TableDefs.scala:
package com.bayakala.learn.slick301.model
import slick.driver.JdbcProfile
class TableDefs(val dbDriver: JdbcProfile) {
import dbDriver.api._
case class Supplier(id: Long
, name: String
, contact: Option[String]
, website: Option[String])
final class Suppliers(tag: Tag) extends Table[Supplier](tag,"SUPPLERS") {
def id = column[Long]("ID",O.AutoInc,O.PrimaryKey)
def name = column[String]("NAME")
def contact = column[Option[String]]("CONTACT")
def website = column[Option[String]]("WEBSITE")
def * = (id, name, contact, website) <> (Supplier.tupled,Supplier.unapply)
def nidx = index("NM_IDX",name,unique = true)
}
val suppliers = TableQuery[Suppliers] case class Coffee(id: Long
,name: String
,supid: Long
,price: Double
,sales: Int)
final class Coffees(tag: Tag) extends Table[Coffee](tag, "COFFEES") {
def id = column[Long]("ID",O.AutoInc,O.PrimaryKey)
def name = column[String]("NAME")
def supid = column[Long]("SUPID")
def price = column[Double]("PRICE",O.Default(0.0))
def sales = column[Int]("SALES",O.Default())
def * = (id,name,supid,price,sales) <> (Coffee.tupled, Coffee.unapply)
def fk_sup = foreignKey("FK_SUP",supid,suppliers)(_.id,onDelete = ForeignKeyAction.Restrict,onUpdate = ForeignKeyAction.Cascade)
def supidx = index("SUP_IDX",supid,unique = false)
def nidx = index("NM_IDX",name,unique = true)
}
val coffees = TableQuery[Coffees] }
src/main/scala/access/DAOs.scala:
package com.bayakala.learn.slick301.access
import com.bayakala.learn.slick301.config
import com.bayakala.learn.slick301.config.DBConfig
import com.bayakala.learn.slick301.model.TableDefs
trait DAOs { dbconf: DBConfig =>
import jdbcDriver.api._
//注入依赖
val tables = new TableDefs(dbconf.jdbcDriver)
import tables._
//suppliers queries
val createSupplierTable = suppliers.schema.create
val allSuppliers = suppliers.result
def insertSupplier(id:Long,name:String,address:Option[String],website:Option[String])
= suppliers += Supplier(id,name,address,website)
def insertSupbyName(n: String) = suppliers.map(_.name) += n
//coffees queries
val createCoffeeTable = coffees.schema.create
val allCoffees = coffees.result
def insertCoffee(c: (Long,String,Long,Double,Int)) =
coffees += Coffee(id=c._1, name=c._2,supid=c._3,price=c._4,sales=c._5) }
src/main/scala/main/Main.scala:
package com.bayakala.learn.slick301.main
import com.bayakala.learn.slick301.config.DBConfig
import com.bayakala.learn.slick301.access.DAOs import scala.concurrent.Await
import scala.util.{Failure, Success}
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import slick.backend.DatabaseConfig
import slick.driver.JdbcProfile object Main { object Actions extends DAOs with DBConfig {
import slick.SlickException
import slick.util.ClassLoaderUtil
import scala.util.control.NonFatal
import com.typesafe.config.ConfigFactory def getDbConfig: String =
ConfigFactory.load().getString("app.dbconfig") def getDbDriver(path: String): JdbcProfile = {
val config = ConfigFactory.load()
val n = config.getString((if (path.isEmpty) "" else path + ".") + "driver")
val untypedP = try {
if (n.endsWith("$")) ClassLoaderUtil.defaultClassLoader.loadClass(n).getField("MODULE$").get(null)
else ClassLoaderUtil.defaultClassLoader.loadClass(n).newInstance()
} catch {
case NonFatal(ex) =>
throw new SlickException(s"""Error getting instance of Slick driver "$n"""", ex)
}
untypedP.asInstanceOf[JdbcProfile]
} override lazy val jdbcDriver: JdbcProfile = getDbDriver(getDbConfig)
val dbConf: DatabaseConfig[JdbcProfile] = DatabaseConfig.forConfig(getDbConfig)
override val db = dbConf.db
}
import Actions._ def main(args: Array[String]) = { val res = db.run(createSupplierTable).andThen {
case Success(_) => println("supplier table created")
case Failure(_) => println("unable to create supplier table")
}
Await.ready(res, seconds) val res2 = db.run(insertSupbyName("Acme Coffee Co."))
Await.ready(res2, seconds) Await.ready(db.run(allSuppliers), seconds).foreach(println) val res10 = db.run(createCoffeeTable).andThen {
case Success(_) => println("coffee table created")
case Failure(_) => println("unable to create coffee table")
}
Await.ready(res10, seconds) val res11 = db.run(insertCoffee((,"Columbia",,158.0,)))
Await.ready(res11, seconds) Await.ready(db.run(allCoffees), seconds).foreach(println) }
浅谈Slick(4)- Slick301:我的Slick开发项目设置的更多相关文章
- 【转载】浅谈TDD、BDD与ATDD软件开发
转载自(此处仅供学习):http://blog.csdn.net/zhenyu5211314/article/details/22033295 1. 首先了解一下这三个开发模式都是什么意思: TDD: ...
- 安卓开发_浅谈Android动画(四)
Property动画 概念:属性动画,即通过改变对象属性的动画. 特点:属性动画真正改变了一个UI控件,包括其事件触发焦点的位置 一.重要的动画类及属性值: 1. ValueAnimator 基本属 ...
- Android安全开发之启动私有组件漏洞浅谈
0x00 私有组件浅谈 android应用中,如果某个组件对外导出,那么这个组件就是一个攻击面.很有可能就存在很多问题,因为攻击者可以以各种方式对该组件进行测试攻击.但是开发者不一定所有的安全问题都能 ...
- 浅谈白鹭Egret
浅谈白鹭Egret 最近在做一个移动项目,技术选型的时候接触到了白鹭,简单了解了之后觉得挺合适的,最终就选择了这个引擎. 为什么会选择白鹭引擎呢? 我看上他主要有一下几点: 1 ...
- 浅谈Slick(3)- Slick201:从fp角度了解Slick
我在上期讨论里已经成功的创建了一个简单的Slick项目,然后又尝试使用了一些最基本的功能.Slick是一个FRM(Functional Relational Mapper),是为fp编程提供的scal ...
- 浅谈Slick(1)- 基本功能描述
Slick (Scala language-integrated connection kit)是scala的一个FRM(Functional Relational Mapper),即函数式的关系数据 ...
- 浅谈 Fragment 生命周期
版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 源码:AndroidDemo/Fragment 文中如有纰漏,欢迎大家留言指出. Fragment 是在 Android 3.0 中 ...
- 浅谈 LayoutInflater
浅谈 LayoutInflater 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 源码:AndroidDemo/View 文中如有纰漏,欢迎大家留言指出. 在 Android 的 ...
- 浅谈Java的throw与throws
转载:http://blog.csdn.net/luoweifu/article/details/10721543 我进行了一些加工,不是本人原创但比原博主要更完善~ 浅谈Java异常 以前虽然知道一 ...
随机推荐
- 在开源中国(oschina)git中新建标签(tags)
我今天提交代码到主干上面,本来想打个标签(tags)的. 因为我以前新建过标签(tags),但是我现在新建的时候不知道入库在哪了.怎么找也找不到了. 从网上找资料也没有,找客服没有人理我,看到一个交流 ...
- HTML5 程序设计 - 使用HTML5 Canvas API
请你跟着本篇示例代码实现每个示例,30分钟后,你会高喊:“HTML5 Canvas?!在哥面前,那都不是事儿!” 呵呵.不要被滚动条吓到,很多都是代码和图片.我没有分开写,不过上面给大家提供了目录,方 ...
- Linux设备文件简介(转载)
Linux 中的设备有2种类型:字符设备(无缓冲且只能顺序存取).块设备(有缓冲且可以随机存取).每个字符设备和块设备都必须有主.次设备号,主设备号相同的设 备是同类设备(使用同一个驱动程序).这些设 ...
- SpringMVC入门
Spring Web MVC是一种基于Java的实现了Web MVC设计模式的请求驱动类型的轻量级Web框架,即使用了MVC架构模式的思想,将web层进行职责解耦,基于请求驱动指的就是使用请求-响应模 ...
- angularJS(5)
angularJS(5) 一,数据循环:特别要注意作用域 使用ng-repeat指令. <div ng-app="myApp" ng-controller="myC ...
- [转载]MVVM模式原理分析及实践
没有找到很好的MVVM模式介绍文章,简单找了一篇,分享一下.MVVM实现了UI\UE设计师(Expression Blend 4设计界面)和软件工程师的合理分工,在SilverLight.WPF.Wi ...
- LeetCode All in One 题目讲解汇总(持续更新中...)
终于将LeetCode的免费题刷完了,真是漫长的第一遍啊,估计很多题都忘的差不多了,这次开个题目汇总贴,并附上每道题目的解题连接,方便之后查阅吧~ 477 Total Hamming Distance ...
- 判断一个对象是jQuery对象还是DOM对象
今天调试一段代码的时候,看到其中一个变量,想知道它到底是jquery对象还是dom对象. 虽然直接console出这个对象,看它的内部可以判断出来.但是我想有没有什么更方便的方法呢. 后来我想到了一个 ...
- 【腾讯Bugly干货分享】动态链接库加载原理及HotFix方案介绍
本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57bec216d81f2415515d3e9c 作者:陈昱全 引言 随着项目中动 ...
- 集成基于OAuth协议的单点登陆
在之前的一篇文章中,我们已经介绍了如何为一个应用添加对CAS协议的支持,进而使得我们的应用可以与所有基于CAS协议的单点登陆服务通讯.但是现在的单点登陆服务实际上并不全是通过实现CAS协议来完成的.例 ...