2018年7月25日 星期三

[Java] Swagger3 + JAX-RS Jersey

首先加入依賴包 swagger-jersey2-jaxrs

// build.gradle
dependencies {
    compile 'io.swagger:swagger-jersey2-jaxrs:1.5.20'
}

然後註冊 swagger,

// src/main/java/demo/WebApplication.java
package demo;

import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.server.ResourceConfig;

@ApplicationPath("/")
public class WebApplication extends ResourceConfig {
    public WebApplication() {
        packages("<MY-PACKAGE>"); // "demo"

        register(io.swagger.jaxrs.listing.ApiListingResource.class);
        register(io.swagger.jaxrs.listing.SwaggerSerializers.class);
    }
}

接著寫 swagger annotation,

// src/main/java/demo/Hello.java
package demo;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Response;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;

@Api(value="Hello World API")
@Path("/hello")
public class Hello {
    @ApiOperation(value = "say hello", response = String.class)
    @GET()
    public String hello() {
        return "hello";
    }
}

然後打開 url: http://localhost:8080/<PROJECT>/hello/swagger.json 確認是否正常。

沒問題之後,下載 Swagger UI dist 檔案到 src/main/webapp/static/, 將 index.html 裡面的 url https://petstore.swagger.io/v2/swagger.json 修改成剛剛的 http://localhost:8080/<PROJECT>/hello/swagger.json。

並且修改 web.xml 使可以讀取 Swagger UI,

// src/main/webapp/WEB-INF/web.xml
<web-app ...>
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/static/*</url-pattern>
    </servlet-mapping>
</web-app>
然後就可以使用 http://localhost:8080/<PROJECT>/static/index.html。

參考資料:

2019/07/12 更新:
附註:StackOverflow: What exactly is the ResourceConfig class in Jersey 2?

2018年7月24日 星期二

[Java] JAX-RS on AWS Lambda

AWS Lambda 有提供 proxy framework 掛上 jersey。 首先加上依賴包,

// build.gradle
dependencies {
    compile (
        'com.amazonaws.serverless:aws-serverless-java-container-jersey:1.1.3',
        'org.glassfish.jersey.media:jersey-media-json-jackson:2.27',
        'org.glassfish.jersey.inject:jersey-hk2:2.27'
    )
}

然後接口要換掉 (RequestHandler -> RequestStreamHandler),

package example;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;

import com.amazonaws.serverless.proxy.jersey.JerseyLambdaContainerHandler;
import com.amazonaws.serverless.proxy.model.AwsProxyRequest;
import com.amazonaws.serverless.proxy.model.AwsProxyResponse;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;

public final class ProxyWithStream implements RequestStreamHandler {

    private static final ResourceConfig jerseyApplication = new ResourceConfig()
            .packages("<PACKAGE-NAME>")
            .register(JacksonFeature.class);

    private static final JerseyLambdaContainerHandler handler = JerseyLambdaContainerHandler
            .getAwsProxyHandler(jerseyApplication);

    @Override
    public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException {
        handler.stripBasePath("<API-GATEWAY-PATH>");
        handler.proxyStream(inputStream, outputStream, context);
        outputStream.close();
    }

}
其中 <PACKAGE-NAME> 要輸入 resource 路徑, <API-GATEWAY-PATH> 則是端視在 API Gateway 的設定。 假設 API Gateway 設定 "/v1/foo/{proxy+}", JAX-RS 設定 APIs 為 "/bar/{id}", <API-GATEWAY-PATH> 就要設定為 "/v1/foo"。

接下來 Lambda 測試的 payload 需要更改成 API Gateway 傳給 Lambda 的格式,然後就可以測試了。

{
  "body": "",  // POST/PUT/PATCH body 的來源
  "resource": "/{proxy+}",
  "requestContext": {
    "resourceId": "123456",
    "apiId": "1234567890",
    "resourcePath": "/{proxy+}",
    "httpMethod": "GET",
    "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
    "accountId": "123456789012",
    "identity": {
      "apiKey": null,
      "userArn": null,
      "cognitoAuthenticationType": null,
      "caller": null,
      "userAgent": "Custom User Agent String",
      "user": null,
      "cognitoIdentityPoolId": null,
      "cognitoIdentityId": null,
      "cognitoAuthenticationProvider": null,
      "sourceIp": "127.0.0.1",
      "accountId": null
    },
    "stage": "prod"
  },
  "queryStringParameters": {
    "fields": "name"  // query parameters 的來源
  },
  "headers": {
    "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
    "Accept-Language": "en-US,en;q=0.8",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    "User-Agent": "Custom User Agent String",
    "Accept-Encoding": "gzip, deflate, sdch"
  },
  "pathParameters": {
    "proxy": "bar/12345"
  },
  "httpMethod": "GET",  // method 的來源
  "stageVariables": {},
  "path": "/v1/foo/bar/12345"  // url mapping 的來源
}

參考資料:

系列:

2018年7月22日 星期日

AWS Lambda + API Gateway

承上文 [Java] AWS Lambda 入門, 這次要在前面掛上 API Gateway。

首先設定好 API Gateway 串到之前的 Lambda,測試的時候會出現 Status 502 的狀況,

Execution failed due to configuration error: Malformed Lambda proxy response
主要原因是 Lambda 回傳給 API Gateway 有特定格式,以下用 Node 當作範例:
exports.handler = (event, context, callback) => {
    var response = {
        "statusCode": 200, //可略,默認為200
        "headers": { //可略
            "my_header": "my_value"
        },
        "body": JSON.stringify(event),
        "isBase64Encoded": false //可略,默認為非
    };
    callback(null, response);    
};
也就是說需要回傳 Json 物件,同時把資料放在 "body" 裡面,API Gateway 才會顯示正常。 在 Lambda 上測試的結果要像這樣:
{
  "statusCode": 200,
  "headers": {
    "my_header": "my_value"
  },
  "body": "{\"key3\":\"value3\",\"key2\":\"value2\",\"key1\":\"value1\"}",
  "isBase64Encoded": false
}
Java 的回傳格式也必須相同。

參考資料:

系列:

[Java] AWS Lambda + DynamoDB

承上文 [Java] AWS Lambda 入門, 這次添加存取 DynamoDB。

首先建立一個 DynamoDB 表單 "Employee",同時建立一筆資料。

接下來進入 IAM 建立一個 Role,這裡直接選擇 "AWS service", 在 "Choose the service that will use this role" 選擇 "Lambda", 然後就可以點擊按鈕 "Next: Permissions";
在 "Attach permissions policies" 裡面, 勾選 "AmazonDynamoDBFullAccess","AWSLambdaDynamoDBExecutionRole", 然後點擊按鈕 "Next: Review";
輸入 Role name,就可以建立一個 Role 了。

接下來回到之前建立的 Lambda,往下拉到 "Execution role",選取剛剛建立的 Role 就成功了。

可以參考官方文件用 node 簡單測試一下,

// Load the AWS SDK for Node.js
var AWS = require('aws-sdk');
// Set the region 
AWS.config.update({region: 'ap-northeast-1'});

exports.handler = (event, context, callback) => {
  // Create the DynamoDB service object
  ddb = new AWS.DynamoDB({apiVersion: '2012-10-08'});

  var params = {
    TableName: 'Employee',
    Key: {
      'EmpId' : {S: '1000'}
    }
    //, ProjectionExpression: 'EmpId'
  };

  // Call DynamoDB to read the item from the table
  ddb.getItem(params, function(err, data) {
    if (err) {
      console.log("Error", err);
    } else {
      console.log("Success", data.Item);
      callback(null, data);
    }
  });
};

回到之前的 Java Project,修改一下:

package example;

import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.amazonaws.services.dynamodbv2.document.Item;
import com.amazonaws.services.dynamodbv2.document.Table;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

public class Hello implements RequestHandler {
    @Override
    public String handleRequest(Integer input, Context context) {
        AmazonDynamoDB client = AmazonDynamoDBClientBuilder.defaultClient();
        DynamoDB dynamoDB = new DynamoDB(client);
        Table table = dynamoDB.getTable("Employee");
        Item item = table.getItem("EmpId", "10000");
        System.out.println(item.toJSONPretty());
        return item.toJSON();
    }
}

另外 Lambda 也提供直接轉換物件的方式,我們不用再去處理 Json 映射物件的問題。

package example;

import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

import lombok.Data;

public class Hello implements RequestHandler {
    @Override
    public String handleRequest(Integer input, Context context) {
        AmazonDynamoDB client = AmazonDynamoDBClientBuilder.defaultClient();
        DynamoDBMapper dynamodbMapper = new DynamoDBMapper(client);
        Employee result = dynamodbMapper.load(Employee.class, "10000");
        System.out.println(result);
        return result.toString();
    }

    @Data
    @DynamoDBTable(tableName = "Employee")
    class Employee {
        @DynamoDBHashKey(attributeName = "EmpId")
        private String id;
        @DynamoDBAttribute(attributeName = "Name")
        private String name;
    }
}

系列:

[Java] AWS Lambda 入門

首先修改 build.gradle。

// build.gradle
apply plugin: 'java'
repositories {
    mavenCentral()
}
dependencies {
    compile (
        'com.amazonaws:aws-lambda-java-core:1.1.0',
        'com.amazonaws:aws-lambda-java-events:1.1.0'
    )
}
task buildZip(type: Zip) {
    from compileJava
    from processResources
    into('lib') {
        from configurations.compile.Classpath
    }
}
build.dependsOn buildZip
如果打包有發生錯誤,可以刪除 .Classpath 再重新打包嘗試。

接下來寫一隻簡單的範例。

package example;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

public class Hello implements RequestHandler<Integer, String> {
    public String myHandler(int myCount, Context context) {
        return String.valueOf(myCount);
    }
    @Override
    public String handleRequest(Integer input, Context context) {
        return "Default " + input.toString();
    }
}

打包後上傳 .jar,"Handler" 改成 "example.Hello::myHandler", 然後在 "Configure test events" 裡面將輸入值改成數字(目前預設是 Json),就可以測試了。

"Handler" 的格式是 "包.類::函數";所以上述可以改成 "example.Hello:: handleRequest" 再測試一次。 另外如果輸入格式是 Json,可以將 Integer 改成 Map<String,String>

備註:Java 預設必須調整記憶體大於 768MB 與 Timeout 5秒,否則都會出現 "Task timed out after 3.00 seconds"。 因為實際執行的日誌顯示最大使用記憶體只達 34MB,推測啟動服務加上載入 .jar 需要這麼多才可以。

參考資料:

系列:

2018年7月9日 星期一

[Eclipse] 使用 Lombok

承上文 [Android Studio] 使用 LombokLombok 是在 POJO 寫些注解,然後在編譯時期幫忙產生冗余代碼。

在 build.gradle 設置:

dependencies {
    ...
    compile 'org.projectlombok:lombok:1.18.+'
}

下載 lombok.jar 放置在 /Applications/Eclipse.app/Contents/MacOS, 在 /Applications/Eclipse.app/Contents/Eclipse/eclipse.ini 後面增加一行:
-javaagent:lombok.jar

參考資料:https://projectlombok.org/setup/android
參考資料:Install Lombok for Eclipse on Mac

Eclipse IDE for Java Developers

Version: Oxygen Release (4.7.0)
Build id: 20170620-1800

[Java] Invalid HTTP method: PATCH

最近系統需要使用 Netty4,所以把衝突的 Netty3 拆掉,然後就出現了例外。 pom.xml <dependency> <groupId>com.ning</groupId> <artifactId>as...