Spring Boot で Object Storage を使う

今回は Spring Boot で Object Storage を使ってみようと思います.

Object Storage の説明はこちらを参照ください.
(簡単に言うと、S3 と同じイメージです)

Object Storage の Java 用 SDK が用意されているので、こちらを使います.

build.gradle

build.gradle は以下のようになります.

buildscript {
	ext {
		springBootVersion = '2.0.4.RELEASE'
	}
	repositories {
		mavenCentral()
	}
	dependencies {
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
	}
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'com.ibm.jp.gbs'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
	mavenCentral()
}


dependencies {
	compile('org.springframework.boot:spring-boot-starter-web:latest.release')
	compile('org.projectlombok:lombok:latest.release')
	compile('com.ibm.cos:ibm-cos-java-sdk:latest.release')
}

 

Repository

まずは Interface です.
画像を取得する getImage を宣言しました.

package com.ibm.jp.gbs.ibmwaydayapp.domain.repository;

import java.util.Optional;

public interface IObjectStorageRepository {

    public Optional getImage(String bucketName, String imageName);
}

 

IObjectStorageRepository の実装です.

最終的には S3ObjectInputStream を byte[] に変換して返しています.
画像が取得できなかった場合は、Optional.empty が返されます.

package com.ibm.jp.gbs.ibmwaydayapp.infrastructure;

import com.ibm.cloud.objectstorage.AmazonServiceException;
import com.ibm.cloud.objectstorage.ClientConfiguration;
import com.ibm.cloud.objectstorage.SDKGlobalConfiguration;
import com.ibm.cloud.objectstorage.SdkClientException;
import com.ibm.cloud.objectstorage.auth.AWSCredentials;
import com.ibm.cloud.objectstorage.auth.AWSStaticCredentialsProvider;
import com.ibm.cloud.objectstorage.auth.BasicAWSCredentials;
import com.ibm.cloud.objectstorage.client.builder.AwsClientBuilder;
import com.ibm.cloud.objectstorage.oauth.BasicIBMOAuthCredentials;
import com.ibm.cloud.objectstorage.services.s3.AmazonS3;
import com.ibm.cloud.objectstorage.services.s3.AmazonS3ClientBuilder;
import com.ibm.cloud.objectstorage.services.s3.model.S3ObjectInputStream;
import com.ibm.jp.gbs.ibmwaydayapp.application.config.ConfigReader;
import com.ibm.jp.gbs.ibmwaydayapp.domain.repository.IObjectStorageRepository;
import org.springframework.stereotype.Repository;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Optional;

@Repository
public class ObjectStorageRepository implements IObjectStorageRepository {

    private static AmazonS3 client;

    public ObjectStorageRepository(){

        SDKGlobalConfiguration.IAM_ENDPOINT = "https://iam.bluemix.net/oidc/token";
        
        String apiKey = "XXXAu8zD_hfm66ycnwHxkAb0smjJBcGm7kUPCJmLxE5";
        String resourceInstanceId = "XXX:v1:bluemix:public:cloud-object-storage:global:a/XXX::";
        String endpointUrl = "https://s3.ap-geo.objectstorage.softlayer.net";
        String location = "ap-geo";

        client = createClient(apiKey, resourceInstanceId, endpointUrl, location);
    }

    private static AmazonS3 createClient(String apiKey,
                                         String serviceInstanceId,
                                         String endpointUrl,
                                         String location)
    {
        AWSCredentials credentials;

        if (endpointUrl.contains("objectstorage.softlayer.net")) {
            credentials = new BasicIBMOAuthCredentials(apiKey, serviceInstanceId);
        } else {
            credentials = new BasicAWSCredentials(apiKey, serviceInstanceId);
        }

        ClientConfiguration clientConfig = new ClientConfiguration().withRequestTimeout(5000);
        clientConfig.setUseTcpKeepAlive(true);

        AmazonS3 client = AmazonS3ClientBuilder.standard()
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endpointUrl, location))
                .withPathStyleAccessEnabled(true)
                .withClientConfiguration(clientConfig).build();

        return client;
    }

    @Override
    public Optional getImage(String bucketName, String imageName) {

        S3ObjectInputStream content;

        try {
            content = client.getObject(bucketName, imageName).getObjectContent();

        } catch (AmazonServiceException amazonServiceException){
            System.out.printf("Error reading file %s:%s:%s\n",
                    bucketName,
                    imageName,
                    amazonServiceException.getMessage());
            return Optional.empty();
        }

        try {
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            byte [] buffer = new byte[1024];

            while(true) {

                int length = content.read(buffer);
                if(length < 0) {
                    break;
                }
                stream.write(buffer, 0, length);
            }

            return Optional.of(stream.toByteArray());

        } catch (IOException ioException){
            System.out.printf("Error reading file %s:%s:%s\n", bucketName, imageName, ioException.getMessage());
            return Optional.empty();
        }
    }
}

 

Service

Service になります.
Optinal 型の中身が Null だった場合に、no_image.png を設定する処理を記述しています.

package com.ibm.jp.gbs.ibmwaydayapp.domain.service;

import com.ibm.cloud.objectstorage.util.IOUtils;
import com.ibm.jp.gbs.ibmwaydayapp.domain.repository.IObjectStorageRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;

@Service
public class ImageService {

    @Autowired
    private IObjectStorageRepository repository;

    @Autowired
    private ResourceLoader resourceLoader;

    public byte[] getImage(String bucketName, String imageName) throws IOException {

        Optional optional = repository.getImage(bucketName, imageName);

        Resource resource = resourceLoader.getResource("classpath:" + "static/image/no_image.png");
        InputStream stream = resource.getInputStream();

        return optional.orElse(IOUtils.toByteArray(stream));
    }
}

 

Controller

最後に Controller です.
画像を返すエンドポイント image/backet_name/image_name を実装しました.

package com.ibm.jp.gbs.ibmwaydayapp.application.controller;

import com.ibm.jp.gbs.ibmwaydayapp.domain.service.ImageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.IOException;

@Controller
public class ImageController {

    @Autowired
    private ImageService service;

    @ResponseBody
    @GetMapping("image/{bucketName}/{imageName}")
    public ResponseEntity getImage(@PathVariable("bucketName") String bucketName,
                                                  @PathVariable("imageName") String imageName) throws IOException {

        byte[] image = service.getImage(bucketName, imageName);

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.IMAGE_JPEG);
        headers.setContentLength(image.length);
        return ResponseEntity.ok()
                .headers(headers)
                .body(image);
    }
}

 

SDK があるので、とてもシンプルに書けますね.
Object Storage 自体は AWS S3 API と互換なので、使い勝手も良いかと思います.

 

以上です.