どんなアプリのAPIか。
- チャット、通話機能があるSNSアプリのAPIです。
- ユーザー登録、プッシュ通知、ブロック・通報機能などのsnsアプリに必要な一通りのAPIを実装しています。
システムの構成
- フレームワーク:Spring Boot
- 言語:Kotlin
- API:Protocol Buffersで定義、gRPCで実装。
- DB:MySQL
コード
コメント
- gRPC, Protocol Buffersがどんなものかは他に沢山情報があるので省略します。
- 画像ファイルのアップロードはこのサンプルに含みません。
- iOSプッシュ通知の用のp8ファイルや、その他の認証用の文字列は無効なものになっています。
APIの一覧
アカウント
- ユーザー登録
- プロフィール編集
- 退会
ユーザー情報:
- ユーザーのリスト取得
- 指定したidのユーザーを取得
通話
- 通話を開始。(電話を掛ける)
- 通話を受ける。
- 通話を切る。
- 通話が継続していることを記録。
- 通話のステータス受信(開始、通話中、終了)
チャット
- メッセージ送信
- メッセージ受信
- すべてのメッセージを取得
プッシュ通知
- デバイストークン_iOS / 登録, 更新
- ON/OFF更新
その他
- ユーザーをブロック
- ユーザーを通報
Protocol BuffersによるAPI仕様定義
- account.proto
- block.proto
- calling.proto
- chat.proto
- commons.proto
- push_notify.proto
- report.proto
- user.proto
API(gRPC)のコントローラークラス
- AccountGrpcService
- AccountRegisterGrpcService
- BlockGrpcService
- CallingGrpcService
- ChatGrpcService
- PushNotifyGrpcService
- ReportGrpcService
- UserGrpcService
build.gradle: protoファイルから、APIのソースを生成
group 'com.talking'
// gRPCのコード生成コマンド: ./gradlew generateProto
// ドキュメント: gRPCの実装
// https://github.com/LogNet/grpc-spring-boot-starter
buildscript {
ext.kotlin_version = '1.3.61' // Required for Kotlin integration
ext.spring_boot_version = '2.2.0.RELEASE'
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // Required for Kotlin integration
classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version" // See https://kotlinlang.org/docs/reference/compiler-plugins.html#spring-support
classpath "org.springframework.boot:spring-boot-gradle-plugin:$spring_boot_version"
classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.5"
}
}
apply plugin: 'kotlin' // Required for Kotlin integration
apply plugin: "kotlin-spring" // https://kotlinlang.org/docs/reference/compiler-plugins.html#spring-support
apply plugin: 'org.springframework.boot'
apply plugin: 'com.google.protobuf'
def grpcVersion = '1.25.0'
repositories {
mavenCentral()
}
sourceSets {
main.kotlin.srcDirs += 'src/main/kotlin'
main.java.srcDirs += 'src/main/java'
main.java.srcDirs += 'src/main/generated-proto'
}
dependencies {
compile("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version")
compile("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version")
// testCompile('org.springframework.boot:spring-boot-starter-test')
// for web
compile("org.springframework.boot:spring-boot-starter-web:$spring_boot_version")
compile("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.8")
// for db
compile("org.springframework.boot:spring-boot-starter-data-jpa:$spring_boot_version")
runtime("mysql:mysql-connector-java:8.0.15")
compile("org.springframework.boot:spring-boot-starter-security:$spring_boot_version")
// grpc
compile("io.github.lognet:grpc-spring-boot-starter:3.5.0")
compile "io.grpc:grpc-api:${grpcVersion}"
compile "io.netty:netty-codec-http2:4.1.42.Final"
compile "io.grpc:grpc-core:${grpcVersion}"
compile "io.grpc:grpc-protobuf:${grpcVersion}"
compile "io.grpc:grpc-stub:${grpcVersion}"
// cache
compile(group: 'com.github.ben-manes.caffeine', name: 'caffeine', version: '2.8.1')
// iOSプッシュ通知
compile(group: 'com.eatthepath', name: 'pushy', version: '0.13.11')
}
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.5.0"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
}
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
outputSubDir = 'generated-proto'
}
}
task.plugins {
grpc {
outputSubDir = 'generated-proto'
}
}
}
}
generatedFilesBaseDir = "$projectDir/src/"
}
ddl
テーブル
- ユーザー
- プロフィール画像
- チャット
- 通話履歴
- 通報
- ブロック
- プッシュ通知デバイストークン_iOS
工夫したところ
ログインしているユーザーしか利用できないAPIの認証処理を共通化
AuthInterceptor内でrequestのheader内のtoken,userIdで認証。
@Component
class AuthInterceptor : ServerInterceptor {
@Autowired
private lateinit var userService: UserService
// 参考ソース:
// https://github.com/saturnism/grpc-by-example-java/blob/master/metadata-context-example/src/main/java/com/example/grpc/server/JwtServerInterceptor.java
override fun <ReqT : Any?, RespT : Any?> interceptCall(
call: ServerCall<ReqT, RespT>,
headers: Metadata,
next: ServerCallHandler<ReqT, RespT>
): ServerCall.Listener<ReqT> {
val userId = headers.get(GrpcConstant.USER_ID_METADATA_KEY)?.toLong() ?:
throw StatusRuntimeException(Status.UNAUTHENTICATED, headers)
val apiToken = headers.get(GrpcConstant.API_TOKEN_METADATA_KEY) ?:
throw StatusRuntimeException(Status.UNAUTHENTICATED, headers)
val user = userService.findByIdAndToken(userId, apiToken) ?:
throw StatusRuntimeException(Status.UNAUTHENTICATED, headers)
val ctx = Context.current().withValue(GrpcConstant.AUTH_USER_CONTEXT_KEY, user)
return Contexts.interceptCall(ctx, call, headers, next)
}
}
認証処理を利用したいAPIのクラスにAuthInterceptorを設定
https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/src/main/kotlin/com/talking/api/grpc/AccountGrpcService.kt
@GRpcService(interceptors = [AuthInterceptor::class])
class AccountGrpcService: AccountServiceGrpc.AccountServiceImplBase() {