SpringBootを2.0.1.RELEASEから2.6.6にバージョンアップしてみた

数年前に他で作られたものを引き継いで担当しているSpringBootアプリがあるのですが、dependency-checkを実行するとライブラリに多数の脆弱性がある事が判明。

[ERROR] dom4j-1.6.1.jar: CVE-2020-10683(9.8)
[ERROR] hibernate-core-5.2.16.Final.jar: CVE-2019-14900(6.5), CVE-2020-25638(7.4)
[ERROR] hibernate-validator-6.0.9.Final.jar: CVE-2019-10219(6.1), CVE-2020-10693(5.3)
略

対応の時間が取れそうなので、spring-boot-starterを対応開始時点最新だった2.6.4へ一気に上げてみる事にしました。今回対象のアプリではごく一部だけコード変更が必要でしたが、それを書き直して実行してみると、、起動に失敗しているようです。

***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
   processorLookupService (field private jp.esoro.automation.application.impl.xxxx)
      ↓
   monitoringAlert (field private jp.esoro.automation.application.ApplicationEvents xxxx)
┌─────┐
|  applicationEventsImpl (field private jp.esoro.automation.application.NotificationLookupService xxxx)
↑     ↓
|  notificationLookupService (field jp.esoro.automation.application.impl.ChangeNotification xxxx)
↑     ↓
|  changeNotification (field private jp.esoro.automation.application.ApplicationEvents xxxx)
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.

下記にも出てましたが、メッセージの図が示している通り循環参照をしているようです。
[blogcard url=”https://qiita.com/rhirabay/items/c3fe7cd7faee9f35ab2c”]

該当箇所のコードを読み、そのままで問題無い事を確認した上でメッセージの通りにapplication.ymlに設定を追加します。

  spring:
    main:
      allow-circular-references: true

稼働確認をしていくと数か所の動作に問題がありましたが、基本的にはライブラリ間のバージョン不一致によるものだったので、使用ライブラリの依存を整理してバージョンを調整。

対応が殆ど終わったと思ったら、Springの脆弱性「Spring4Shell」が見つかり、実行環境的にはJava1.8なので影響は無いのですが、最新の2.6.6にして再度稼働確認し一段落。

SpringBootでZabbixAPIを使ってみる

 Zabbixの作業を自動化したいという事で、ZabbixAPIを使ってみました。対象のZabbixは3.2とちょっと古めのようですが、API自体はあまりバージョンは気にしなくてよさそうでした。ドキュメントが充実しているので助かります。

認証後、2件のホストを指定してホスト情報を取ってくるテストを書いてみました。

	@Test
	public void testZabbixAPI() throws Exception {
		String zabbixUser = "zabbixuser";
		String zabbixPass = "****";
		String zabbixEndpoint = "http://zabbixserver/zabbix/api_jsonrpc.php";

		//認証
		JSONObject json = new JSONObject();
		json.put("user", zabbixUser);
		json.put("password", zabbixPass);
		json.put("userData", "false");
		
		String token = null;
		ResponseEntity<String> response = Request("user.login", json, zabbixEndpoint, token);
		
	    JsonNode root = new ObjectMapper().readTree(response.getBody());
	    
	    token = root.get("result").get("sessionid").asText();

		//ホスト情報取得
		JSONObject target = new JSONObject();
	    String[] chkTarget = {"server01","server02"};
		JSONArray targetArray = new JSONArray(chkTarget);
	    
    	JSONObject param = new JSONObject();
		param.put("output", new JSONArray(Arrays.asList("hostid","host","name")));
		target.put("host", targetArray);
		param.put("filter", target);

		response = Request("host.get", param, zabbixEndpoint, token);
		ObjectMapper mapper = new ObjectMapper();
		ArrayNode arrayHost = (ArrayNode) mapper.readTree(response.getBody()).path("result");
		
	    System.out.println("hosts: " + arrayHost);
    }
	
	private ResponseEntity<String> Request(String method, JSONObject param, String endPoint, String token) throws Exception {
		JSONObject json = new JSONObject();

		json.put("jsonrpc", "2.0");
        json.put("method", method);
        json.put("params", param);
        json.put("id", 1);
		if(token != null){
            json.put("auth", token);
        }
        HttpMethod httpMethod = HttpMethod.POST;
        
	    HttpEntity <String> entity ;
		HttpHeaders headers = new HttpHeaders();
	    headers.add("Content-Type", "application/json-rpc");
	    headers.add("Accept-Language", "ja");
	    
	    if(param == null){
	    	entity = new HttpEntity <String>(headers);
	    }
	    else{
	    	entity = new HttpEntity <String>(json.toString(), headers);
	    }
	    RestTemplate restTemplate = new RestTemplate();
	    restTemplate.getMessageConverters()
        .add(0, new StringHttpMessageConverter(Charset.forName("UTF-8")));
	    
	    ResponseEntity<String> response = restTemplate.exchange(endPoint, httpMethod, entity, String.class);
	    
		return response;
	}

で、こんな感じで戻ってきます。

hosts: [{“hostid”:”10387″,”host”:”server01″,”name”:”server01_DB”},{“hostid”:”10388″,”host”:”server02″,”name”:”server02_DB”}]

問題無く使えそうなので、要件を整理してから実装してみます。

Webixでイベントを実装する

 今作っているWebのUIに、前回使ったw2uiをまた使おうかと思いましたが、今回対象のUIが他のWebサイトから遷移する前提なので、w2uiでは遷移元のWebサイト画面との違和感が強く、別のJavascriptUIフレームワークを探してみたら、Webixを発見。
 
 すっきりした見た目で違和感なさそうなので、今回はWebixを使ってみることにします。
 
 こちらのサイトに概要が書かれているのでとても参考になりました。

[blogcard url=”http://kumapanda.jp/?p=129″]

 で、UIの形は出来てきたので、イベントの実装をどうやるのかとドキュメントを見ていくと、ボタンのようなUI Controlでは、下記のようにidを指定してイベントを実装するようです。

webix.ready(function(){

   webix.i18n.setLocale("ja-JP");
   webix.ui({
      type: 'wide',
      cols: [
         { view: "button", id: "exec_button", label:"実行", width:300, align:"center"},
      ]
    });

   $$("exec_button").attachEvent("onItemClick", function(ids, some){
       webix.confirm({
          title:"",
          ok:"はい", 
          cancel:"いいえ",
          text: "実行してよろしいですか?"
       }).then(function(result){
          //OK時
       });
   });
});

ドキュメントが充実しているので、とても助かります。

SpringBoot ファイルアップロードでエラーが出る

 前にSpringBootで作ったWebアプリで、たまにファイルアップロードエラーが出ているとの事でログを確認。

 こんなログが吐かれています。

2020-08-03 16:31:28.826 ERROR org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/xx].[dispatcherServlet] Servlet.service() for servlet [dispatcherServlet] in context with path [/xx] threw exception [Request processing failed; nested exception is org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request; nested exception is java.io.IOException: The temporary upload location [/tmp/tomcat.1401623156062740234.8080/work/Tomcat/xx/xx] is not valid] with root cause
java.io.IOException: The temporary upload location [/tmp/tomcat.1401623156062740234.8080/work/Tomcat/xx/xx] is not valid

 同事象のコチラを参考にテンポラリの場所を変えてみたのですが、開発端末のWindowsだと、どうも設定した値はデフォルトのテンポラリディレクトリの中に指定したディレクトリに出力しようとしてやっぱりエラーになるような感じでした。

 で、次にコチラを参考にspring.http.multipart.locationとして設定したところ、エラーは解消したようなのですが、非推奨になっています。。

 最後に実行環境のEC2にデプロイしたところ、spring.servlet.multipart.locationで指定してちゃんと動きました。アップロード後にtmpファイルは自動的に浄化されています。

 OSで違うなんて微妙な感じです。。

Springboot Securityのログイン画面がいい感じになっていた

 Springbootで作ったアプリを、そろそろユーザーに渡そうかというタイミングになりましたが、Springbootのバージョンが結構更新されているので、今更ながら最新に入れ替えてみました。
 それまでフレームワークは、spring-boot-starter-parentの2.0.1.RELEASEを指定していましたが、今見ると2.1系の次に2.2系が既に出ています。
 コードに影響があれば、そこまで最新にしなくてもいいかととりあえず2.2.4を試したところ、特に問題が無かったのでチェンジ。
 今回のアプリはsecurityのデフォルトログイン画面を使っていたのですが、更新したところ画面が変わりました。どうやら2.1から既に変わっていたようです。

2.0系はこれ

2.1系overはこれ

2.0系の見ためがしょぼいのでカスタマイズしようか考えましたが、中央寄せのいい感じのUIに変わってました。これならそのままでも違和感無いですね。

アプリの動作確認は終了、最後に、バージョンアップでこんな警告が出るようになったみたいなので、
2020-02-27 14:09:12 WARN JpaBaseConfiguration$JpaWebConfiguration spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning

出ないようにapplication.ymlに追加

spring:
   jpa:
     open-in-view: false

受け渡し準備OKです。
でもjarのファイルサイズはどんどん肥大化してます。。

w2uiでテーブルメンテナンス画面を作ってみる

SpringBootで作っているアプリに、テーブルメンテナンス画面が必要になったので、w2uiで作ってみる事にしました。
[blogcard url=”http://w2ui.com/web”]
テーブルメンテナンス画面といえば、一覧表示があって、選択したら登録フォームが表示されて、とか基本的な構成はどのテーブルも似たようなものですよね。w2uiのグリッドとフォームを使い、使いまわし出来るようjavascriptに機能を纏め、html側にテーブル固有の個別設定を寄せる事で、いろんなテーブルに対応出来るような形にしていきます。
w2uiのformは、urlを指定するとthis.save()でフォームをPOSTしてくれるようなのですが、ちょっと扱いにくいのでsaveを使わずにjsonでPOSTするようにしました。

eclipse上のリソース構成はこんな感じです。

javascriptです。ちょっと変更後のUIの動きがアバウトではありますが。。

//編集表示
function showEditForm(isEdit){
   $('#data-form').show(); 
   $('#data-grid').hide(); 
	w2ui['data_form'].clear();
	w2ui['data_form'].header = uiName;
	
	if(isEdit){
		var sel = w2ui['data_grid'].getSelection();
		if (sel.length == 1) {
        	var record = w2ui['data_grid'].get(sel[0]);
        	w2ui['data_form'].record = record;
		}
	}
    w2ui['data_form'].refresh();
}

//一覧表示
function showGrid(){
   $('#data-grid').show(); 
   $('#data-form').hide(); 
   $.ajax({
      headers:{ "Accept":"text/html,application/xhtml+xml,application/xml,application/json",},
      url:gridURL})
      .fail((jqXHR, textStatus, errorThrown) => {
         if( jqXHR.status == 404 ){
            w2alert('該当するデータはありません'+ jqXHR.responseText, uiName);
         }else{
            w2alert('データが取得出来ませんでした。 ' + jqXHR.status + jqXHR.responseText, uiName);
         }
      })
      .done((data, textStatus, jqXHR) => {
         if(data == ""){
            w2alert('該当する情報はありません', uiName);
            return;
         }
         var json=JSON.parse(data);
         var idx = 0;
         if(!Array.isArray(json)){
            json = JSON.parse('[' + JSON.stringify(json) + ']');
         }
         for(var item of json){
            idx++;
            item['recid']=idx;
         }
         w2ui['data_grid'].clear();
         w2ui['data_grid'].records = json;
         w2ui['data_grid'].refresh();
       });
}

w2utils.locale({
    "phrases" : {
        "Save": "閉じる",
        "Hide": "隠す",
        "Add New": "追加",
        "Edit": "編集",
        "Delete": "削除",
        "Confirmation": "確認",
        "Yes": "はい",
        "No": "いいえ",
        "Reload data in the list": "一覧を再表示します",
        "Edit selected record": "選択した設定を編集します",
        "Required field": "入力必須項目です",
        "Add new record": "新規に設定を追加します",
        "Delete selected records": "選択した設定を削除します",
        "Are you sure you want to delete selected records?": "選択した設定を削除します。よろしいですか?"
    }
});

//更新要求
function ajaxRequest(url, method, JSON, msg){
	var deferred = new $.Deferred();
 	w2confirm( msg +'します。よろしいですか?')
	.yes(function () { 
		$.ajax({
	        type: method,
	        url: url,
	        data: JSON,
	        contentType: 'application/JSON',
	        dataType : 'JSON',
	        scriptCharset: 'utf-8',
	        })
	        .fail((jqXHR, textStatus, errorThrown) => {
	           w2alert(msg + 'に失敗しました' + jqXHR.status + jqXHR.responseText, uiName).done(function () {
		        	deferred.resolve();
	           });
	        })
	        .done((data, textStatus, jqXHR) => {
	           w2alert(msg + 'しました', uiName).done(function () {
		        	deferred.resolve();
	           });
	        });
	});
 	return deferred;
}

$('#data-grid').w2grid({
    header: uiName,
    show: { header: true, toolbar: true, toolbarEdit: true, toolbarAdd: true, toolbarDelete: true, toolbarReload: true, toolbarColumns: false, toolbarSearch: false },
    name: 'data_grid',
    multiSelect : false,
    onReload: function(event) {
    	showGrid();
    },
    onAdd: function (event) {
    	showEditForm(false);
    },
    onEdit: function (event) {
    	showEditForm(true);
    },
    onDelete: function (event) {
    	event.preventDefault();
       	var record = this.get(this.getSelection());
    	var deleteJSON = JSON.stringify(record[0]);
    	var deferred = ajaxRequest(formURL, 'delete', deleteJSON, '削除');
    	deferred.done(function(){
    		setTimeout(function(){showGrid()},500);
    	});
    },
    columns: gridColumns,
});

$('#data-form').w2form({ 
    name  : 'data_form',
    header: uiName,
//    url   : formURL,
    fields: formColumns,
    actions: {
        '更新': function (event) {
        	if( this.validate(true).length == 0 ){
      	    	var postData = this.record;
      	    	var postJSON = JSON.stringify(postData);
      	    	ajaxRequest(formURL, 'post', postJSON, '更新');
        	}
        },
        'キャンセル': function (event) {
            showGrid();
            this.clear();
        }
    }
});

htmlにはjsで定義済の要素とか変数を載せます。メンテ対象のテーブルが増えた場合を考慮しツールバーも入れてます。なお、項目の設定はw2uiのドキュメントを見ながら調整していきますが、結構融通が利きますね。

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Customer API Tool</title>
<script src="javascript/jquery-3.4.1.min.js"></script>
<script type="text/javascript" src="javascript/w2ui-1.5.rc1.min.js"></script>
<link rel="stylesheet" type="text/css" href="css/w2ui-1.5.rc1.min.css" />
</head>
<body>
<div id="toolbar" style="padding: 4px; border: 1px solid #dfdfdf; border-radius: 3px"></div>
<div id="data-grid" style="width: 100%; height: 500px;"></div>
<div id="data-form" style="width: 100%; height: 350px;"></div>
<script type="text/javascript">
//w2ui-tablemaintenance.js グリッド設定
var uiName = "顧客設定";  // 画面表示名
var gridURL = location.href + 'customers';  //グリッド表示データ取得URL
var gridColumns = [  // グリッド項目
                   { field: 'customerid', caption: '顧客ID', size: '35%' ,sortable: true, info: true},
                   { field: 'customerCode', caption: '顧客契約CD', size: '35%' ,sortable: true},
                   { field: 'customerName', caption: '顧客名 ', size: '130%' ,sortable: true},
                   { field: 'apiUrl', caption: 'API URL', size: '100%' ,sortable: true},
                   { field: 'user', caption: 'ユーザーID', size: '50%' ,sortable: true},
                   { field: 'password', caption: 'パスワード', size: '40%', render: 'password', sortable: true},
                   { field: 'mailadr', caption: '通知先メールアドレス', size: '100%' ,sortable: true},
                   { field: 'insdate', caption: '作成日時', size: '60%' ,sortable: true},
                   { field: 'updddate', caption: '更新日時', size: '60%' ,sortable: true},
                   ];  

//w2ui-tablemaintenance.js フォーム設定
var formURL = location.href + 'customers';  //フォーム更新URL
var formColumns = [ //フォーム項目
                   { field: 'customerid', type: 'text', required: true, html:{caption: '顧客ID', attr: 'size=5 maxlength=4'}},
                   { field: 'customerCode', type: 'text', required: true, html:{caption: '顧客契約CD', attr: 'size=10 maxlengh=8'}},
                   { field: 'customerName', type: 'text', required: true, html:{caption: '顧客名', attr: 'size=60 maxlengh=60' }},
                   { field: 'apiUrl', type: 'text', required: true, html:{caption: 'API URL', attr: 'size=60 maxlengh=60' }},
                   { field: 'user', caption: 'login ID', type: 'text', required: true, html:{caption: 'login ID', attr: 'size=20 maxlengh=20' }},
                   { field: 'password', caption: 'password', type: 'password', required: true, html:{caption: 'password', attr: 'size=20 maxlengh=20' }},
                   { field: 'mailadr', caption: '通知先メールアドレス', size: '100%' ,sortable: true, html:{caption: '通知先メールアドレス', attr: 'size=40 maxlengh=40' }},
                   ];

//ツールバー
$('#toolbar').w2toolbar({
    name: 'toolbar',
    tooltip: 'right',
    items: [{ type: 'break' },{ type: 'html', html: '<strong><font color="red">Customer API Tool</font></strong>'},
        { type: 'button', id: 'customerBT', group: '1',text: '顧客設定', icon: 'fa-wrench', tooltip: '顧客設定を参照・編集します',
        	onClick: function (event) { showGrid();}},
        { type: 'break' },
    ]
});

$(document).ready(function(){
	showGrid();
});

</script>
<script type="text/javascript" src="javascript/w2ui-tablemaintenance.js"></script>
</body>
</html>

最後にというか、本来なら最初にですがSpringBoot側は、w2uiの項目名と一致するエンティティクラス(ここではCustomer)と、CrudRepositoryを継承したインターフェースCustomerRepositoryを用意し、RESTコントローラーからアクセスさせるようにします。

@CrossOrigin
@RestController
public class CustomerController {
	private CustomerRepository repository;
	
	@Autowired
    public CustomerController(CustomerRepository repository) {
        this.repository = repository;
    }
	/**
	 * 顧客情報取得
	 * */
	@RequestMapping(value="/customers",method = RequestMethod.GET)
    private String getCustomers() throws Exception{
		
		ObjectMapper jsonMapper = new ObjectMapper();
		List<Customer> list = new ArrayList<>();
		StreamSupport.stream(Spliterators.spliteratorUnknownSize(repository.findAll().iterator(), 0),false).forEach(o -> list.add(o));
		return jsonMapper.writeValueAsString(list);
	}
	/**
	 * 顧客情報取得
	 * @param id customer id
	 * */
	@RequestMapping(value="/customers/{id}",method = RequestMethod.GET)
    private String getCustomers(@PathVariable(name = "id", required = false) String id
    		) throws Exception{
		ObjectMapper jsonMapper = new ObjectMapper();
		
		return jsonMapper.writeValueAsString(repository.findById(id));
	}
	/**
	 * 顧客情報更新
	 * */
	@RequestMapping(value="/customers",method = RequestMethod.POST)
	private Customer setCustomers(@RequestBody Customer customer) throws Exception{
		if(customer.getInsdate()== null){
			customer.setInsdate(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
		}
		customer.setUpdddate(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
		return repository.save(customer);
	}
	/**
	 * 顧客情報削除
	 * */
	@RequestMapping(value="/customers",method = RequestMethod.DELETE)
    private String deleteCustomers(@RequestBody Customer customer) throws Exception{
		repository.deleteById(customer.getCustomerid());
		return customer.getCustomerid();
	}
}

で、出来た画面です。

なお、今回セキュリティ要件は考慮していません。。

springboot AOP でリクエストパラメータをログに出力する

かなり前から稼働しているWebサービスをspringbootで再構築をしているところですが、リクエストとレスポンスをログに出力するという既存仕様の踏襲にAOPを使ってみました。
 対象のWebサービスは、昔ながらのフォームのポストでリクエストを受け付ける仕様なので、application/x-www-form-urlencodedに対応する形になります。
AOPについては、こちらを参考にさせて頂きました。
リクエストとレスポンスなので、@Aroundでメソッドの前後にログ出力をします。

Controllerのメソッド引数にHttpServletRequestとHttpServletResponseを入れる事で、Advicer側でパラメータが取得出来ました。

@Controller
public class XXXContoller {
	
    @RequestMapping(path={ "/online/XXX.do"}, method = RequestMethod.POST)
    public void doPost(HttpServletRequest request, HttpServletResponse response) {
		//サービス層で処理
    }
}

Adviser側です。

@Aspect
@Component
public class ControllerAdviser {
    private static final Logger logger= LogManager.getLogger();

    /**
     * Controllerメソッドの前後処理
     * */
    @Around("execution(* jp.esoro.application.controller..*.*(..)) && args(request,response,..)")
    public void Around(ProceedingJoinPoint joinPoint, HttpServletRequest request, HttpServletResponse response){
        try {
            String logMessage = request.getRemoteAddr() + ":" +request.getRequestURI() + ":";
            Map<String, String[]> map = request.getParameterMap();
            if(map != null) {
                boolean firstFlg = true;
                for (Iterator<Map.Entry<String, String[]>> it = map.entrySet().iterator(); it.hasNext();) {
                    Map.Entry<String, String[]> entry = it.next();
                    String[] value = entry.getValue();
                    if(value.length > 0){
                        if (firstFlg) {
                            logMessage = logMessage + entry.getKey() + "=" +value[0];
                            firstFlg = false;
                        }
                        else{
                            logMessage = logMessage +"&" + entry.getKey() + "=" +value[0];
                        }
                    }
                }
            }
            logger.info(logMessage);

            //実処理の実行
            joinPoint.proceed();

            //レスポンスログ(未実装)

        } catch (Throwable e) {
            logger.warn(e.getMessage());
        }
    }
}

下記のようにログに出力されました。
2019-05-21 11-38-39.002:0:0:0:0:0:0:0:1:/online/XXXX.do:Id=9999999&Password=ggggg&Action=regist

 でも、今回の要件にはこれ以外にログ出力要件があり、Controllerの引数ではちょっと無理っぽい。。
 他にも、同じクラスから呼ばれたメソッドには適用されないようなので、ビジネスロジック側の影響は避けられないようでした。
 また、戻りがあるメソッドに適用するにはjoinPoint.proceed()の戻りを返す必要があったりと、色々ハマりどころがあるようです。

AWS CodePipelineでspringbootアプリをECSへデプロイしてみた

オンプレサーバで稼働しているシステムをAWSへ移行したいという話があり、AWS環境でCI/CDがどう出来るか確認してみました。
構成としては、開発用端末のIDE(IntelliJ IDEA)でコミット、CodeCommitのリポジトリにプッシュしたら、Mavenビルドを開始してECRへコンテナをプッシュ、ECSへデプロイという流れです。

下記AWS環境を用意します。
・ECSの設定(クラスタ、サービス、タスク定義)
・ECRにリポジトリを作成、springbootのアプリなのでopenjdkのイメージをプッシュ
・CodeCommitにリポジトリ作成
・CodePipelineを設定

ググれば、いろいろAWSに関する情報はあるのですが、なかなかうまく出来ません。
で、いろいろやってみた結果、ビルド用にbuildspec.yml、デプロイ用にDockerfileをリポジトリの直下に置くとデプロイが成功しました。

version: 0.2

phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws --version
      - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)
      - REPOSITORY_URI={AWS上のECRリポジトリ}
      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
      - IMAGE_TAG=${COMMIT_HASH:=latest}
  build:
    commands:
      - echo Build started on `date`
      - mvn clean
      - mvn install
      - echo Building the Docker image...          
      - docker build -t $REPOSITORY_URI:latest .
      - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
  post_build:
    commands:
      - echo Build completed on `date`
      - mvn package
      - echo Pushing the Docker images...
      - docker push $REPOSITORY_URI:latest
      - docker push $REPOSITORY_URI:$IMAGE_TAG
      - echo Writing image definitions file...
      - printf '[{"name":"sample-app","imageUri":"%s"}]' $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json      
artifacts:
  files:
    - imagedefinitions.json
FROM openjdk:latest
VOLUME /tmp
ADD target/springtest-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 5000
ENV JAVA_OPTS=""
ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ] 

すいません、情報の参照元が解らなくなってしまいました。。
mvn install 結構時間掛かりますね。。

AWSは数年前にアカウントを登録してちょこっとだけ遊んでみただけだったので、ちゃんとAWS環境を理解した上で整理してからじゃないと実利用には遠い感じです。。

Swagger + SpringBoot で APIサイトを作ってみる

既存システムのDBへアクセスしてデータを参照するようなAPIを、って事で、
SwaggerでAPIを定義し、Swaggerから自動生成したソースを使って実装してみました。
なお、今回対象としているのは参照系がメインでRESTって雰囲気ではありません。
対象が既存システムのDBなので。。

とりあえず、ざっくりとした要求と応答の仕様を整理し、Swagger Editorにyamlを書いてみます。

なお、Swagger Editorは、別件で使っていた端末内のxampp/htdocsにGitHubからダウンロードしたZIPを解凍して使ってます。
ある程度、定義を書いた後、Swagger EditorからGenerate ServerでSpringのサーバソースを出力してみます。

自動生成されたJavaソースはこんな構成です。

Controllerとレスポンスエンティティのモデルが揃ってます。全体的なソース構成は、コントローラ層、サービス層、データアクセス層という3構成になりますかね。
コントローラ層は概ねSwaggerで生成したので、データアクセス層も自動生成しようと、Doma2を使ってみます。
doma-gen-build.xmlに接続先DB情報と出力先ソースの設定等を記述し、こちらもGenerate!

結果、ソースを手書きする必要があるのは、

・サービス層全体

・Swaggerで自動生成されたSpringBootApplicationクラスのComponentScanにサービス層のパッケージを追記
  @ComponentScan(basePackages = { “io.swagger”, “io.swagger.api”,”jp.esoro.api” })

・Swaggerで自動生成されたControllerにServiceをコールする箇所を追記

・DAOインターフェースに下記アノテーションを追記
@ConfigAutowireable
@Repository

・自動生成不可能なSQL文の作成し、それに合わせてDAOにメソッド追加

といったところで、とりあえずの動作確認が出来ました。

ただ、実用するには認証やページング等も必要なので、こんな単純な話にはなりませんし、APIの仕様をちゃんと整理してからSwaggerでソースを生成して着手しないと後が面倒ですね。

SpringBootでWebアプリが動かない

 作りかけのSpringBootのRestアプリを別端末の開発環境(Windows+EclipseSTS+Maven)へ移行して動かしてみたところ、ブラウザからURLを打ち込んでも、RestControllerがうんともすんともいいません。元々の環境では普通に動作していたのですが。。
 困ったな・・と別環境への移行を諦めようかと思っていたところ、よく見るとSpring実行時のログ出力に下記を発見


[ERROR] C:\Users\admin\.m2\repository\org\apache\tomcat\embed\tomcat-embed-core\8.5.31\tomcat-embed-core-8.5.31.jarの読込みエラーです。invalid LOC header (bad signature)

 とりあえず該当のC:\Users\admin\.m2\repository\org\apache\tomcat\embed\tomcat-embed-core\8.5.31 内のファイルを全て消して再度、プロジェクト→実行→Maven install実施。
すると何事も無く動き始めました。。


2018-07-30 15:18:55.806 INFO 14844 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8090 (http) with context path ''

こういった問題はあまり深く考えても詮無き事。。
なお、デフォルトの8080ポートは他アプリと被ってたので、application.ymlに

server:
port: 8090

と書いています。