CateoLJ3はSpring Bootと連携することで簡単にブログ用記事管理REST APIを構築するためのライブラリです。
記事管理部分はCateogoLJ3に任せて、ビューの部分を好きなように自分で作れるので、オレオレブログの構築が簡単です。
特徴としては
- 記事はMarkdownで書いてGit(GitHubなど)で管理
- 検索はElasticsearchを利用
な点です。このブログ自体もCategoLJ3で作られています。
本稿で、CategoLJ3によるREST APIサーバーの作り方を紹介します。
プロジェクトの作成
APIサーバーは基本的にSpring Bootプロジェクトです。
Spring Initializrで「Search for dependencies」に「Web」を入れて「Generate Project」をクリックし、プロジェクトの雛形をダウンロードします。
ダウンロードしたdemo.zipを展開し、MavenプロジェクトをIDEにインポートしてください。 IntelliJ IDEの場合はpom.xmlをIDEで開くだけでOKです。
pom.xml
にCategoLJ3を使うための依存関係am.ik.categolj3:categolj3-api
を追加します。執筆段階で、バージョンは1.0.0.M5
です。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
+ <dependency>
+ <groupId>am.ik.categolj3</groupId>
+ <artifactId>categolj3-api</artifactId>
+ <version>1.0.0.M5</version>
+ </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Gitレポジトリの準備
次に記事を管理するためのGitレポジトリを作成します。任意のレポジトリを使用しても良いですが、ここでは https://github.com/categolj/categolj3-article-base をForkします。
application.properties
にGitレポジトリの情報を設定します。
# 記事を管理するGitのリポジトリのURL(Fork先のURL)
git.uri=https://github.com/making/categolj3-article-base.git
# リポジトリ中のMarkdownファイルを管理するフォルダ名
git.content-dir=content
# git cloneをするディレクトリ(アプリケーションが自動でcloneする)
git.base-dir=/tmp/article
Gitリポジトリのアクセスに認証が必要な場合はgit.username
とgit.password
に認証情報を設定してください。
Gitの設定は以上です。
Elasticsearchの準備
次にElasticesearchの設定を行います。
ここでは簡単に使うためにDockerを使用しますが、ローカルにインストールしたElasticesearchを使っても構いません。
$ docker run -p 9200:9200 -p 9300:9300 --rm elasticsearch
Docker Machineをつかっている場合はデフォルトで、http://192.168.99.100:9200 にアクセスできるはず。
application.properties
にこのURLを設定します。
jest.connection-url=http://192.168.99.100:9200
Elasticesearchの設定は以上です。
アプリケーションの設定
最後にCateoLJ3を使うためのアプリケーションの設定を行います。
設定は非常に簡単で、DemoApplication.java
に@EnableCategoLJ3ApiServer
をつけるだけです。
package com.example;
+ import am.ik.categolj3.api.EnableCategoLJ3ApiServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
+ @EnableCategoLJ3ApiServer
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
DemoApplication.java
のmain
メソッドを実行しましょう。
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.3.1.RELEASE)
2015-12-29 02:02:24.025 INFO 11473 --- [ main] com.example.DemoApplication : Starting DemoApplication on Toshiaki-no-iMac.local with PID 11473 (/Users/maki/Downloads/demo/target/classes started by maki in /Users/maki/Downloads/demo)
2015-12-29 02:02:24.027 INFO 11473 --- [ main] com.example.DemoApplication : No active profile set, falling back to default profiles: default
2015-12-29 02:02:24.081 INFO 11473 --- [ main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@3439f68d: startup date [Tue Dec 29 02:02:24 JST 2015]; root of context hierarchy
2015-12-29 02:02:24.976 INFO 11473 --- [ main] o.s.b.f.s.DefaultListableBeanFactory : Overriding bean definition for bean 'beanNameViewResolver' with a different definition: replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2015-12-29 02:02:25.182 INFO 11473 --- [ main] o.s.s.a.AsyncAnnotationBeanPostProcessor : No TaskExecutor bean found for async annotation processing.
2015-12-29 02:02:25.213 INFO 11473 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.cache.annotation.ProxyCachingConfiguration' of type [class org.springframework.cache.annotation.ProxyCachingConfiguration$$EnhancerBySpringCGLIB$$72e54ff7] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-12-29 02:02:25.227 INFO 11473 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration' of type [class org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration$$EnhancerBySpringCGLIB$$ad0d501b] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-12-29 02:02:25.321 INFO 11473 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'spring.cache.CONFIGURATION_PROPERTIES' of type [class org.springframework.boot.autoconfigure.cache.CacheProperties] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-12-29 02:02:25.329 INFO 11473 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.boot.autoconfigure.cache.GuavaCacheConfiguration' of type [class org.springframework.boot.autoconfigure.cache.GuavaCacheConfiguration$$EnhancerBySpringCGLIB$$7cb3b918] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-12-29 02:02:25.340 INFO 11473 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'cacheManager' of type [class org.springframework.cache.guava.GuavaCacheManager] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-12-29 02:02:25.341 INFO 11473 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'cacheAutoConfigurationValidator' of type [class org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration$CacheManagerValidator] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-12-29 02:02:25.550 INFO 11473 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2015-12-29 02:02:25.559 INFO 11473 --- [ main] o.apache.catalina.core.StandardService : Starting service Tomcat
2015-12-29 02:02:25.560 INFO 11473 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.0.30
2015-12-29 02:02:25.619 INFO 11473 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2015-12-29 02:02:25.620 INFO 11473 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1543 ms
2015-12-29 02:02:25.764 INFO 11473 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/]
2015-12-29 02:02:25.767 INFO 11473 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
2015-12-29 02:02:25.767 INFO 11473 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2015-12-29 02:02:25.767 INFO 11473 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2015-12-29 02:02:25.768 INFO 11473 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*]
2015-12-29 02:02:26.017 INFO 11473 --- [cTaskExecutor-1] a.i.c.api.git.GitStore$GitPullTask : git pull https://github.com/making/categolj3-article-base.git
2015-12-29 02:02:26.195 INFO 11473 --- [ main] io.searchbox.client.JestClientFactory : Node Discovery Disabled...
2015-12-29 02:02:26.504 INFO 11473 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@3439f68d: startup date [Tue Dec 29 02:02:24 JST 2015]; root of context hierarchy
2015-12-29 02:02:26.545 INFO 11473 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/new]}" onto org.springframework.http.ResponseEntity<org.springframework.core.io.Resource> am.ik.categolj3.api.entry.EntryFileDownloadController.download()
2015-12-29 02:02:26.548 INFO 11473 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/entries],methods=[GET]}" onto org.springframework.data.domain.Page<am.ik.categolj3.api.entry.Entry> am.ik.categolj3.api.entry.EntryRestController.getEntries(org.springframework.data.domain.Pageable,boolean)
2015-12-29 02:02:26.548 INFO 11473 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/entries/{entryId}],methods=[GET]}" onto am.ik.categolj3.api.entry.Entry am.ik.categolj3.api.entry.EntryRestController.getEntry(java.lang.Long)
2015-12-29 02:02:26.549 INFO 11473 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/entries],methods=[GET],params=[q]}" onto org.springframework.data.domain.Page<am.ik.categolj3.api.entry.Entry> am.ik.categolj3.api.entry.EntryRestController.searchEntries(org.springframework.data.domain.Pageable,java.lang.String,boolean)
2015-12-29 02:02:26.549 INFO 11473 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/users/{createdBy}/entries],methods=[GET]}" onto org.springframework.data.domain.Page<am.ik.categolj3.api.entry.Entry> am.ik.categolj3.api.entry.EntryRestController.getEntriesByCreatedBy(org.springframework.data.domain.Pageable,java.lang.String,boolean)
2015-12-29 02:02:26.549 INFO 11473 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/users/{updatedBy}/entries],methods=[GET],params=[updated]}" onto org.springframework.data.domain.Page<am.ik.categolj3.api.entry.Entry> am.ik.categolj3.api.entry.EntryRestController.getEntriesByUpdatedBy(org.springframework.data.domain.Pageable,java.lang.String,boolean)
2015-12-29 02:02:26.549 INFO 11473 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/tags/{tag}/entries],methods=[GET]}" onto org.springframework.data.domain.Page<am.ik.categolj3.api.entry.Entry> am.ik.categolj3.api.entry.EntryRestController.getEntriesByTag(org.springframework.data.domain.Pageable,java.lang.String,boolean)
2015-12-29 02:02:26.549 INFO 11473 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/categories/{categories}/entries],methods=[GET]}" onto org.springframework.data.domain.Page<am.ik.categolj3.api.entry.Entry> am.ik.categolj3.api.entry.EntryRestController.getEntriesByCategories(org.springframework.data.domain.Pageable,java.lang.String,boolean)
2015-12-29 02:02:26.550 INFO 11473 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/tags],methods=[GET]}" onto java.util.List<java.lang.String> am.ik.categolj3.api.tag.TagRestController.list()
2015-12-29 02:02:26.551 INFO 11473 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/git/pull]}" onto java.util.concurrent.CompletableFuture<java.lang.String> am.ik.categolj3.api.git.GitRestController.pull()
2015-12-29 02:02:26.551 INFO 11473 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/jest/reindex]}" onto void am.ik.categolj3.api.jest.JestRestController.reindex()
2015-12-29 02:02:26.551 INFO 11473 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/categories],methods=[GET]}" onto java.util.List<java.util.List<java.lang.String>> am.ik.categolj3.api.category.CategoryRestController.list()
2015-12-29 02:02:26.553 INFO 11473 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2015-12-29 02:02:26.553 INFO 11473 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2015-12-29 02:02:26.569 INFO 11473 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-12-29 02:02:26.569 INFO 11473 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-12-29 02:02:26.595 INFO 11473 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-12-29 02:02:26.687 INFO 11473 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2015-12-29 02:02:26.746 INFO 11473 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2015-12-29 02:02:26.750 INFO 11473 --- [ main] com.example.DemoApplication : Started DemoApplication in 3.179 seconds (JVM running for 3.561)
2015-12-29 02:02:27.078 INFO 11473 --- [cTaskExecutor-1] am.ik.categolj3.api.git.GitStore : Syncing HEAD...
2015-12-29 02:02:31.707 INFO 11473 --- [pool-3-thread-1] am.ik.categolj3.api.event.EventManager : Initialized
api/...
へのリクエストマッピング情報が表示されていれば、CategoLJ3のREST APIサーバー起動しています。
記事を書く
記事は先ほど設定したGitリポジトリにpushすれば良いです。
GitHub上で直接ファイルを作成しても良いですが、ここではGitリポジトリをcloneしてコマンドラインで記事をpushします。
$ git clone https://github.com/making/categolj3-article-base.git
$ cd categolj3-article-base/content
記事用Markdownファイルのtemplateは/new
でダウンロードできます。
$ curl localhost:8080/new > 00001.md
ファイル名は連番([0-9]+
).mdにする必要があります。0埋めはしてもしなくてもいいですが、0埋めしておいた方がソートできて良いです。ちなみに記事IDは先頭の0を削ったものになります。この場合は1が記事IDです。
00001.md
を以下のように書きます。
---
title: First article
tags: ["Demo"]
categories: ["Demo", "Hello"]
---
This is my first article using CategoLJ3!
デフォルトではgitの最初のコミット時刻が作成時刻、最後のコミット時刻が更新時刻になります。
これらを明示的に指定したい場合は、date
とupdated
を使えます。
---
title: First article
tags: ["Demo"]
categories: ["Demo", "Hello"]
date: 2015-11-15T23:59:32+09:00
updated: 2015-11-15T23:59:32+09:00
---
このファイルをadd & commitしてpushしましょう。
$ git add -A
$ git commit -m "Add 00001.md"
$ git push origin master
この段階では、アプリ(REST APIサーバー)には記事は反映されません。定期的に毎時0分と30分にアプリがgit pullを行い、記事の反映を行います。
手動で反映する場合は/api/git/pull
にアクセスすれば良いです。
$ curl localhost:8080/api/git/pull
org.eclipse.jgit.transport.FetchResult@2229323a
Merge of revisions 2962b707734c0c75a933e446a30e82b6f2e28562, 6c3d1509fb47e0e762f9cde29d1f7c5830b06d25 with base 6c3d1509fb47e0e762f9cde29d1f7c5830b06d25 using strategy recursive resulted in: Fast-forward.
アプリケーションのログに、以下のようなログが出力されるでしょう。
2015-12-29 02:17:17.808 INFO 11473 --- [cTaskExecutor-3] a.i.c.api.git.GitStore$GitPullTask : git pull https://github.com/making/categolj3-article-base.git
2015-12-29 02:17:19.256 INFO 11473 --- [cTaskExecutor-3] am.ik.categolj3.api.git.GitStore : Syncing HEAD...
2015-12-29 02:17:19.257 INFO 11473 --- [cTaskExecutor-3] am.ik.categolj3.api.git.GitStore : [ADD] new=content/00001.md old=/dev/null
2015-12-29 02:17:19.310 INFO 11473 --- [cTaskExecutor-3] am.ik.categolj3.api.git.GitStore : put Entry(1)
2015-12-29 02:17:21.700 INFO 11473 --- [pool-3-thread-1] am.ik.categolj3.api.event.EventManager : publish bulk put event
2015-12-29 02:17:21.702 INFO 11473 --- [pool-3-thread-1] am.ik.categolj3.api.jest.JestSync : Bulk update (1)
2015-12-29 02:17:21.915 INFO 11473 --- [pool-3-thread-1] a.i.c.api.tag.InMemoryTagService : bulk put (1)
2015-12-29 02:17:21.918 INFO 11473 --- [pool-3-thread-1] a.i.c.a.c.InMemoryCategoryService : bulk put (1)
これで記事が反映されました。運用時はGitHubなどのwebhookにこのエンドポイントを設定しておけばpushのタイミングで記事が反映されます。
反映された記事は/api/entries
にアクセスすればJSON形式で取得できます。
$ curl localhost:8080/api/entries | jq .
{
"content": [
{
"entryId": 1,
"content": "This is my first article using CategoLJ3!",
"created": {
"name": "Toshiaki Maki",
"date": "2015-12-29T02:16:23+09:00"
},
"updated": {
"name": "Toshiaki Maki",
"date": "2015-12-29T02:16:23+09:00"
},
"frontMatter": {
"title": "First article",
"tags": [
"Demo"
],
"categories": [
"Demo",
"Hello"
]
}
}
],
"last": true,
"totalPages": 1,
"totalElements": 1,
"size": 10,
"number": 0,
"sort": null,
"first": true,
"numberOfElements": 1
}
ページング用として、クエリパラメータにpage
(ページ数、0始まり)とsize
(1ページあたりの件数)を指定できます。
特定の記事のみアクセスしたい場合は/api/entries/<記事ID>
にアクセスすれば良いです。
$ curl localhost:8080/api/entries/1 | jq .
{
"entryId": 1,
"content": "This is my first article using CategoLJ3!",
"created": {
"name": "Toshiaki Maki",
"date": "2015-12-29T02:16:23+09:00"
},
"updated": {
"name": "Toshiaki Maki",
"date": "2015-12-29T02:16:23+09:00"
},
"frontMatter": {
"title": "First article",
"tags": [
"Demo"
],
"categories": [
"Demo",
"Hello"
]
}
}
その他、APIガイド(準備中)は http://localhost:8080/docs/api-guide.html で確認できます。
CategoLJ3を使えば簡単にブログ用のREST APIを構築できることを紹介しました。 このREST APIをつかってオレオレブログを作ってみませんか?ブログに限らず、逆引き辞典を作るプラットフォームにもいかがでしょうか。
CategoLJ3はCloud FoundryやHerokuといったPaaSにも対応しているので、PaaSで運用する方法は別に紹介します。