#author("2024-07-22T03:06:18+09:00","","") #mynavi(Spring Boot) #author("2024-07-22T03:06:47+09:00","","") * Spring BootでWebAPI作成 [#vc607534] #setlinebreak(on); [[Spring Boot]] 自体は以前に記載した記事がいくつかあるが、2024現在の状況も踏まえて改めて整理する。 尚、Spring Boot 自体に直接紐づいているわけではないが、Thymeleaf や O/Rマッパーについても併せて記載する。 * 目次 [#v2eefcf6] #contents - 関連 -- [[Spring BootでWebAPI作成]] -- [[Spring BootのセッションストアをRedisにする]] -- [[AWS EC2上で Spring Bootアプリ起動]] -- [[Spring BootからDynamoDBに接続する]] -- 関連 --- [[AWS EC2上で Spring Bootアプリ起動]] --- [[Spring BootからDynamoDBに接続する]] * JDKのインストール [#q2a22ea8] (Mac) https://jdk.java.net/archive/ から 対象の JDK をダウンロード 及び 解凍し /Library/Java/JavaVirtualMachines/ 配下にコピー。 ** Spring Tool Suite (STS) のダウンロード [#yc41b823] https://spring.io/tools/sts ** STSの日本語化 [#ke7e8aa4] http://mergedoc.osdn.jp/ 本体は不要なのでプラグインだけ落として readme を見て features と plugins を突っ込む。 * VsCode環境設定 [#ae67eaf0] ** テスト用DBの準備(MySQL) [#ied5d235] https://code.visualstudio.com/docs/java/java-spring-boot #include(テスト用DBの準備(MySQL),notitle); 以下の拡張機能をインストール ** Spring Boot プロジェクトの作成 [#g8510bdd] - Extension Pack for Java - Spring Boot Extension Pack - Gradle for Java *** [新規] → [Spring スタータープロジェクト] [#s851f196] #ref(create_project.png); * プロジェクトの作成 [#ce5619eb] *** 変えたい所がされば変更して「次へ」 [#r9e730af] #ref(create_project2.png); #ref(01_create_new_project.png) *** O/Rマッパー、テンプレートエンジンなどを選択 [#z2b0db88] ※build.gradle の dependencies に反映されるだけなので、後からでも変更可能。 #ref(create_project3.png); #ref(02_create_new_project.png) #ref(03_create_new_project.png) *** application.properties にDB接続情報を追加 [#r80432ad] #ref(04_create_new_project.png) src/main/resources/application.properties #ref(05_create_new_project.png) #mycode2(){{ spring.datasource.url=jdbc:mysql://localhost:3306/example_db spring.datasource.username=example_user spring.datasource.password=example_pass spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.tomcat.maxActive=30 spring.datasource.tomcat.maxIdle=20 spring.datasource.tomcat.minIdle=10 spring.datasource.tomcat.initialSize=5 }} #ref(06_create_new_project.png) ※これらの値はOS環境変数などで上書き可能なので、ここではローカル用の設定をそのまま記述する。 ※環境変数に定義する時の変数名は spring.datasource.url → SPRING_DATASOURCE_URL のように変換する。 #ref(07_create_new_project.png) *** テーブル定義の作成 [#h7bde0de] #ref(08_create_new_project.png) しれっと lombok 使って setter 、getter を動的生成してるので、 lombok プラグインのインストールと、gradle.build の dependencies に compile('org.projectlombok:lombok:1.16.10') 等を追加する事。 #ref(09_create_new_project.png) src/main/java/com/example/demo/model/Book.java #mycode2(){{ package com.example.demo.model; * application.properties の編集 [#ad8b03c2] import java.io.Serializable; とりあえず最初の起動に必要な分だけ記載。(DBは事前準備) import java.util.Date; #mycode(){{ spring.application.name=demo spring.datasource.url=jdbc:postgresql://localhost:5432/sample spring.datasource.username=sample spring.datasource.password=sample spring.datasource.driver-class-name=org.postgresql.Driver import lombok.Data; @Data public class Book implements Serializable { private static final long serialVersionUID = 1L; private int id; private String isbn; private String title; private int price; private int created_at; private Date createdAt; private Date updatedAt; } }} * TOPページ作成 [#oec13b8d] *** Daoの作成 [#a73d64c0] ** コントローラ [#gf0f8414] src/main/java/com/example/demo/dao/BookDao.java #mycode2(){{ package com.example.demo.dao; src/main/java/com/example/demo/controller/IndexController.java #mycode(){{ package com.example.demo.controller; import java.util.List; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import com.example.demo.model.Book; @Controller public class IndexController { @GetMapping("/") public String index() { return "index"; } public interface BookDao { List<Book> selectAll(); Book select(int id); void insert(Book book); int update(Book book); int delete(int id); } }} ** Thymeleafテンプレート [#xb6da4df] *** MyBatis用のDao定義を作成 [#q3a20b98] src/main/resources/templates/index.html src/main/resources/dao/BookDao.xml #myhtml2(){{ <!doctype html> <html lang="ja"> <head> <meta charset="utf-8" /> </head> <body> Index Page! </body> </html> <!--?xml version="1.0" encoding="UTF-8" ?--> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.example.demo.dao.BookDao"> <select id="selectAll" resultType="com.example.demo.model.Book"> select * from books </select> <select id="select" parameterType="int" resultType="com.example.demo.model.Book"> select * from books where id = #{id} </select> <insert id="insert" parameterType="com.example.demo.model.Book"> insert into books (isbn,title,price,created_at,updated_at) values(#{isbn},#{title},#{price},now(),now()) <!-- MySQLの場合、以下で自動採番(AUTO_INCREMENT)されたIDをオブジェクトにセットできる --> <selectKey resultType="int" keyProperty="id" order="AFTER"> select @@IDENTITY <!-- SELECT LAST_INSERT_ID() でも可 --> </selectKey> </insert> <update id="update" parameterType="com.example.demo.model.Book"> update books set isbn = #{isbn}, title = #{title}, price = #{price}, updated_at = now() where id = #{id} </update> <delete id="delete" parameterType="int"> delete from books where id = #{id} </delete> </mapper> }} ** テスト起動 [#n2dbc325] *** mybatis-config.xml を追加 [#iffc50a1] #ref(test_boot_run.png,nolink) 参照: http://www.mybatis.org/mybatis-3/ja/configuration.html#settings 起動したら http://localhost:8080/ にアクセスしてTOPページを表示してみる。 src/main/resources/mybatis-config.xml #mycode2(){{ <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <setting name="lazyLoadingEnabled" value="true" /> <setting name="useColumnLabel" value="true" /> <setting name="mapUnderscoreToCamelCase" value="true" /> </settings> </configuration> }} 以降は、諸々の解説を記載する * コントローラについて [#g2675d10] *** MyBatis用のConfigクラスを追加 [#p4d765d3] ** コントローラの基本 [#t3af42e7] src/main/java/com/example/demo/config/SqlMappingConfig.java #mycode2(){{ package com.example.demo.config; import java.io.IOException; - class に @Controller または @RestController アノテーションを付与する - @GetMapping、@PostMapping 等を使用してルーティング定義を行う。 ※ https://spring.io/guides/gs/rest-service - RestAPI用のコントローラの場合 -- 戻り値(Map やDTO)を返却するロジックを書く。 ※ httpヘッダの指定等が必要な場合は ResponseEntity を使用する。 - SSR(サーバサイドレンダリング)用のコントローラの場合 -- 戻り値としてテンプレートファイル名を返却する。 -- Model テンプレートに展開したいデータは Model を使用して設定する。 import javax.sql.DataSource; 例) RestAPI用のコントローラ import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternUtils; #mycode(){{ @Configuration @MapperScan("com.example.demo.dao") public class SqlMappingConfig { /** * SqlSessionFactoryBean格納クラス。 * @return SqlSessionFactoryBean。 */ @Bean public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) throws IOException { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); factory.setDataSource(dataSource); ResourcePatternResolver resolver = ResourcePatternUtils.getResourcePatternResolver(new DefaultResourceLoader()); factory.setConfigLocation(resolver.getResource("classpath:mybatis-config.xml")); factory.setMapperLocations(resolver.getResources("classpath:dao/**/*.xml")); return factory; } } }} *** コントローラの作成 [#m95af82c] src/main/java/com/example/demo/controller/BookController.java #mycode2(){{ package com.example.demo.controller; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import com.example.demo.dao.BookDao; import com.example.demo.model.Book; @RestController public class SampleRestController { @RequestMapping("/api") public class BookController { @GetMapping("api1") public Map<String, String> samplejson() { Map<String, String> res = new HashMap<String, String>(); res.put("var1", "test1"); return res; } @Autowired private BookDao bookMapper; @GetMapping("api2") public ResponseEntity<Map<String, String>> samplejson3() { Map<String, String> res = new HashMap<String, String>(); res.put("var1", "test1"); HttpHeaders headers = new HttpHeaders(); headers.setContentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8)); return new ResponseEntity<Map<String, String>>(res, headers, HttpStatus.BAD_REQUEST); } @RequestMapping(value = "/books", method = RequestMethod.GET) public List<Book> selectAll() { List<Book> books = bookMapper.selectAll(); for (Book book : books) { System.out.println(book.toString()); } return books; } @RequestMapping(value = "/book/{id}", method = RequestMethod.GET) public Book select(@PathVariable("id") Integer id) { Book books = bookMapper.select(id); return books; } @RequestMapping(value = "/book", method = RequestMethod.POST) public Book create(@ModelAttribute Book book) { bookMapper.insert(book); return book; } @RequestMapping(value = "/book/{id}", method = RequestMethod.PUT) public Map<String,String> update(@PathVariable("id") Integer id, @ModelAttribute Book book) { Map<String,String> results = new HashMap<String, String>(); book.setId(id); int count = bookMapper.update(book); results.put("result", count == 1 ? "OK" : "NG"); return results; } @RequestMapping(value = "/book/{id}", method = RequestMethod.DELETE) public Map<String,String> delete(@PathVariable("id") Integer id) { Map<String,String> results = new HashMap<String, String>(); int count = bookMapper.delete(id); results.put("result", count == 1 ? "OK" : "NG"); return results; } } }} 例) SSR(サーバサイドレンダリング)を行う場合 ** ビルド [#v6a471cd] #ref(gradle_build.png); #mycode(){{ package com.example.demo.controller; import org.springframework.stereotype.Controller; ** 起動 [#z200e678] #ref(gradle_boot_run.png); import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @Controller public class HelloController { @GetMapping("/") public String index(Model model) { model.addAttribute("var1", "Test1"); return "index"; } ** 動作確認 [#eef8c85a] #code(myterm2 nolinenums){{ @GetMapping("/hello") public String hello(Model model) { model.addAttribute("name", "山田"); return "hello"; } } # 一覧検索 curl -v http://localhost:8080/api/books # 一意検索 curl -v http://localhost:8080/api/book/1 # 登録 curl -v -XPOST --data "title=TEST&isbn=XXXXX&price=1234" http://localhost:8080/api/book/ # 更新 curl -v -XPUT --data "title=UPDATE&isbn=YYYY&price=5678" http://localhost:8080/api/book/4 # 削除 curl -v -XDELETE http://localhost:8080/api/book/4 }} ** リクエストデータの受け取り方法 [#h96d553b] ** おまけ(動作確認用のページ追加) [#l9e08707] #TODO @RequestParam @PathVariable @ModelAttribute src/main/java/com/example/demo/config/StaticResourceConfig.java #mycode2(){{ package com.example.demo.config; ** 描画データの設定(SSRの場合) [#c13e0d74] import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; #TODO Model.addAttribute によるデータセット @Configuration public class StaticResourceConfig extends WebMvcConfigurerAdapter { ** セッション情報の取得/設定 [#z5cfb32e] @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**") .addResourceLocations("classpath:/static/"); } } }} #TODO src/main/resources/static/book.html #myhtml2(){{ <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <style> #booklist { border-collapse: collapse; } #booklist th, #booklist td { padding: 4px 10px; border: 1px solid #333; } #booklist th { background: #ccc; } #booklist td { cursor: pointer; } </style> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> <script src="/js/ajax.js"></script> </head> <body> <form action="/api/books" method="get" > <input type="submit" value="一覧検索" data-method="get" data-callback="resultList" /> <table id="booklist"> <thead> <tr> <th>id</th> <th>isbn</th> <th>title</th> <th>price</th> </tr> </thead> <tbody> </tbody> </table> </form> <hr /> <form action="/api/book" method="get" > id : <input type="text" name=id value="1" /><br /> <input type="submit" value="一意検索" data-method="get" /> </form> <hr /> <form action="/api/book" method="post"> isbn : <input type="text" name="isbn" value="123-4567890123" /><br /> title : <input type="text" name="title" value="test1" /><br /> price : <input type="number" name="price" value="1000" /><br /> <input type="submit" value="登録" data-method="post" /> </form> <hr /> <form action="/api/book"> id : <input type="text" name="id" value="1" /><br /> isbn : <input type="text" name="isbn" value="123-4567890123" /><br /> title : <input type="text" name="title" value="test2" /><br /> price : <input type="number" name="price" value="1000" /><br /> <input type="submit" value="更新" data-method="put" /> </form> <hr /> <form action="/api/book"> id : <input type="text" name="id" value="1" /><br /> <input type="submit" value="削除" data-method="delete" /> </form> <hr /> 結果 <div id="result" style="border:1px solid #000000;padding:10px;"></div> * Thymeleaf [#a8e57995] <script> // 一覧検索結果の描画 window.resultList = function(result){ console.log("callback!"); $("#booklist tbody").empty(); if (result && result.length > 0) { for (var i in result) { var rowHTml = "<tr>" + "<td>"+result[i]["id"]+"</td>" + "<td>"+result[i]["isbn"]+"</td>" + "<td>"+result[i]["title"]+"</td>" + "<td>"+result[i]["price"]+"</td>" + "</tr>"; $("#booklist tbody").append(rowHTml); } } }; jQuery(function($){ // 一覧の任意の行を選択時 $(document).on("click", "#booklist tbody td", function(){ var id = $(this).parents("tr").find("td").eq(0).text(); var isbn = $(this).parents("tr").find("td").eq(1).text(); var title = $(this).parents("tr").find("td").eq(2).text(); var price = $(this).parents("tr").find("td").eq(3).text(); $(document).find("[name=id]").val(id); $(document).find("[name=isbn]").val(isbn); $(document).find("[name=title]").val(title); $(document).find("[name=price]").val(price); }); }); </script> </body> </html> }} #TODO src/main/resources/static/js/ajax.js #mycode2(){{ * セッションの利用 [#ib9c7d9f] jQuery(function($){ $("form").each(function(){ $(this).find("input[type=submit]").on("click", function(e){ $btn = $(this); $form = $(this).parents("form"); var method = $btn.data("method").toLowerCase(); var url = $form.attr("action"); #TODO var callbackFunc = null; var callback = $btn.data("callback"); if (callback && window[callback]) { callbackFunc = window[callback]; } * O/Rマッパーの利用 [#t5e1f566] var data = {}; $form.find("[name]").each(function(){ var name = $(this).attr("name"); var val = $(this).val(); if ($(this).attr("type") == "checkbox") { val = ($(this).prop("checked") ? $(this).val() : data[name]) || ""; } else if ($(this).attr("type") == "radio"){ val = ($(this).prop("checked") ? $(this).val() : data[name]) || ""; } data[name] = val; }); #TODO if ((method == "put" || method == "delete" || method == "get") && data["id"]) { url = url + "/" + data["id"]; delete(data["id"]); } * AOPの利用 [#s7eb8adc] console.log("url :" + url); console.log("method:" + method); console.log(data); #TODO $.ajax({ "url" : url ,"data" : data ,"type" : method ,"dataType" : "json" ,"success" : function(result){ console.log("success!"); console.log(result); var resultText = "url : " + url + "<br />" + "method : " + method + "<br />" + "result : " + JSON.stringify(result); $("#result").html(resultText); if (callbackFunc){ callbackFunc(result); } } ,"error" : function(a1){ console.log("error!"); console.log(a1); } }); return false; }); }); }); }} 以上のファイルを作成すれば http://localhost:8080/static/book.html から CRUD の確認ができる。