--- title: CategoLJ3でオレオレブログシステムを作ろう tags: ["CategoLJ3", "Spring Boot"] categories: ["Dev", "Blog", "CategoLJ3"] date: 2015-12-28T18:16:28Z updated: 2015-12-29T01:29:45Z --- [CateoLJ3](https://github.com/categolj/categolj3-api)はSpring Bootと連携することで簡単にブログ用記事管理REST APIを構築するためのライブラリです。 ![logo.png](https://qiita-image-store.s3.amazonaws.com/0/1852/3c7ea236-42d4-b0a0-6b44-79262d79adff.png) 記事管理部分はCateogoLJ3に任せて、ビューの部分を好きなように自分で作れるので、オレオレブログの構築が簡単です。 特徴としては * 記事はMarkdownで書いてGit(GitHubなど)で管理 * 検索はElasticsearchを利用 な点です。このブログ自体も[CategoLJ3で作られています](https://blog.ik.am/entries/362)。 本稿で、CategoLJ3によるREST APIサーバーの作り方を紹介します。 ### プロジェクトの作成 APIサーバーは基本的にSpring Bootプロジェクトです。 [Spring Initializr](http://start.spring.io)で「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`です。 ``` diff 4.0.0 com.example demo 0.0.1-SNAPSHOT jar demo Demo project for Spring Boot org.springframework.boot spring-boot-starter-parent 1.3.1.RELEASE UTF-8 1.8 org.springframework.boot spring-boot-starter-web + + am.ik.categolj3 + categolj3-api + 1.0.0.M5 + org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin ``` ### Gitレポジトリの準備 次に記事を管理するためのGitレポジトリを作成します。任意のレポジトリを使用しても良いですが、ここでは https://github.com/categolj/categolj3-article-base をForkします。 スクリーンショット 2015-12-29 1.17.57.png `application.properties`にGitレポジトリの情報を設定します。 ``` properties # 記事を管理する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を使っても構いません。 ``` console $ docker run -p 9200:9200 -p 9300:9300 --rm elasticsearch ``` Docker Machineをつかっている場合はデフォルトで、http://192.168.99.100:9200 にアクセスできるはず。 `application.properties`にこのURLを設定します。 ``` properties jest.connection-url=http://192.168.99.100:9200 ``` Elasticesearchの設定は以上です。 ### アプリケーションの設定 最後にCateoLJ3を使うためのアプリケーションの設定を行います。 設定は非常に簡単で、`DemoApplication.java`に`@EnableCategoLJ3ApiServer`をつけるだけです。 ``` diff 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`メソッドを実行しましょう。 ``` console . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: 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 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.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.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.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.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.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.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 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 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> 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> 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します。 ``` console $ git clone https://github.com/making/categolj3-article-base.git $ cd categolj3-article-base/content ``` [記事用Markdownファイルのtemplate](https://raw.githubusercontent.com/categolj/categolj3-article-base/master/content/template.md)は`/new`でダウンロードできます。 ``` console $ curl localhost:8080/new > 00001.md ``` ファイル名は連番(`[0-9]+`).mdにする必要があります。0埋めはしてもしなくてもいいですが、0埋めしておいた方がソートできて良いです。ちなみに記事IDは先頭の0を削ったものになります。この場合は1が記事IDです。 `00001.md`を以下のように書きます。 ``` md --- title: First article tags: ["Demo"] categories: ["Demo", "Hello"] --- This is my first article using CategoLJ3! ``` デフォルトではgitの最初のコミット時刻が作成時刻、最後のコミット時刻が更新時刻になります。 これらを明示的に指定したい場合は、`date`と`updated`を使えます。 ``` md --- 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しましょう。 ``` console $ git add -A $ git commit -m "Add 00001.md" $ git push origin master ``` この段階では、アプリ(REST APIサーバー)には記事は反映されません。定期的に毎時0分と30分にアプリがgit pullを行い、記事の反映を行います。 手動で反映する場合は`/api/git/pull`にアクセスすれば良いです。 ``` console $ 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. ``` アプリケーションのログに、以下のようなログが出力されるでしょう。 ``` console 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形式で取得できます。 ``` console $ 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>`にアクセスすれば良いです。 ``` console $ 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で運用する方法は別に紹介します。