Функциональное руководство по созданию проекта фотоальбома.

Сегодня я покажу вам, как это сделать, для этого вам понадобятся:

  1. Промежуточные знания со Spring Framework
  2. Аккаунт AWS
  3. Java 11+ и Maven
  4. MongoDB

Мне нравится начинать с Backend

Проще сделать это из бэкэнда, для этого нам нужно создать Bucket на AWS S3. Buckets - это простая абстракция, позволяющая нам знать «где мои файлы?»

Подготовка AWS S3

В консоли AWS перейдите в Storage Session и нажмите S3:

Нажмите «Создать сегмент»:

Теперь вам нужно сообщить основную информацию о своем сегменте, чтобы вы могли идентифицировать его для дальнейшего обслуживания (имя сегмента уникально для всех Amazon S3):

Хорошо, теперь у вас есть корзина на AWS S3, теперь нам нужно создать «ключ доступа» и «секретный ключ» для доступа к вашей корзине в AWS Java SDK . Вернитесь в консоль AWS и выполните поиск «IAM в группе Безопасность, идентификация и соответствие»:

Не страшно с этой панелью, нам не нужно здесь ничего менять, перейдите в меню «Пользователи» и нажмите «Добавить пользователя»:

Следуйте шагам:

Теперь, на втором шаге, вам нужно выбрать «AmazonS3FullAccess», потому что этот пользователь будет добавлять / удалять изображения из вашей корзины.

Шаг 3, «Добавить теги» необязателен, поэтому перейдите к шагу 4, «Просмотр» и «Создание пользователя».

Прямо сейчас, на шаге 5, AWS покажет нам ваш «идентификатор ключа доступа» и «секретный ключ доступа», скопируйте и не забудьте это!

Создание проекта Spring

Предположим, у вас есть промежуточные знания о Spring Framework, перейдите в Spring Initializr и создайте свой проект.

С этими зависимостями:

Теперь нажмите «Создать».

Разархивируйте свой проект и откройте в своей любимой среде IDE, в моем случае IntelliJ.

Во-первых, нам нужно импортировать AWS Java SDK в проект, перейти к вашему «pom.xml» и добавить зависимость:

<!-- Maker, to create the API more faster -->
<dependency>
   <groupId>com.github.gustavovitor</groupId>
   <artifactId>maker-mongo</artifactId>
   <version>${maker-mongo.version}</version>
</dependency>
<!-- Amazon S3 -->
<dependency>
   <groupId>com.amazonaws</groupId>
   <artifactId>aws-java-sdk</artifactId>
   <version>${amazon-sdk.version}</version>
</dependency>

<!-- Apache Commons for FileUtils and misc. -->
<dependency>
   <groupId>commons-io</groupId>
   <artifactId>commons-io</artifactId>
   <version>${commons-io.version}</version>
</dependency>

Характеристики:

<properties>
    <java.version>11</java.version>
    <maker-mongo.version>0.0.6</maker-mongo.version>
    <amazon-sdk.version>1.11.728</amazon-sdk.version>
    <commons-io.version>2.6</commons-io.version>
</properties>

Хорошо, в основном AWS Java SDK работает как API, вам нужно сделать один вызов AWS API и отправить свои файлы в корзину.

Добавьте эти свойства в application.properties:

amazon.s3.bucket-name=awesome-project-java-flutter
amazon.s3.endpoint=https://awesome-project-java-flutter.s3-sa-east-1.amazonaws.com/
amazon.s3.access-key=${ACCESS_KEY}
amazon.s3.secret-key=${SECRET_KEY}

Для большей безопасности создайте на своем компьютере две переменные среды, ACCESS_KEY и SECRET_KEY, и заполните их информацией IAM.

Теперь создайте AmazonClientService:

package com.github.gustavovitor.photos.service.amazon;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;

@Service
public class AmazonClientService {

    // AmazonS3 Client, in this object you have all AWS API calls about S3.
    private AmazonS3 amazonS3;

    // Your bucket URL, this URL is https://{bucket-name}.s3-{region}.amazonaws.com/
    // If you don't know if your URL is ok, send one file to your bucket using AWS and
    // click on them, the file URL contains your bucket URL.
    @Value("${amazon.s3.endpoint}")
    private String url;

    // Your bucket name.
    @Value("${amazon.s3.bucket-name}")
    private String bucketName;

    // The IAM access key.
    @Value("${amazon.s3.access-key}")
    private String accessKey;

    // The IAM secret key.
    @Value("${amazon.s3.secret-key}")
    private String secretKey;

    // Getters for parents.
    protected AmazonS3 getClient() {
        return amazonS3;
    }

    protected String getUrl() {
        return url;
    }

    protected String getBucketName() {
        return bucketName;
    }

    // This method are called after Spring starts AmazonClientService into your container.
    @PostConstruct
    private void init() {

        // Init your AmazonS3 credentials using BasicAWSCredentials.
        BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

        // Start the client using AmazonS3ClientBuilder, here we goes to make a standard cliente, in the
        // region SA_EAST_1, and the basic credentials.
        this.amazonS3 = AmazonS3ClientBuilder.standard()
                .withRegion(Regions.SA_EAST_1)
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .build();
    }

}

Хорошо, с помощью этого клиента вы можете расширить их, чтобы использовать онлайн-клиент.

Теперь нам нужно сохранить основную информацию об образах AWS, для этого мы переходим к созданию простого домена / документа:

package com.github.gustavovitor.photos.domain.amazon;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

import javax.validation.constraints.NotNull;

@Data
@Document
public class AmazonImage {

    @Id
    private String amazonUserImageId;

    @NotNull
    private String imageUrl;

}

Вслед за Spring создайте для него репозиторий:

package com.github.gustavovitor.photos.repository.amazon;

import com.github.gustavovitor.photos.domain.amazon.AmazonImage;
import org.springframework.data.mongodb.repository.MongoRepository;

public interface AmazonImageRepository extends MongoRepository<AmazonImage, String> {
}

Spring предоставляет нам, в частности, данные формы под названием MultipartFile, но вы не можете загружать этот тип в свою корзину, вместо нее требуется java.io.File. Итак, для этого преобразования нам нужно создать такой класс Util:

package com.github.gustavovitor.photos.util;

import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Date;
import java.util.Objects;

public class FileUtils {

    public static File convertMultipartToFile(MultipartFile file) throws IOException {
        File convertedFile = new File(Objects.requireNonNull(file.getOriginalFilename()));
        FileOutputStream fileOutputStream = new FileOutputStream(convertedFile);
        fileOutputStream.write(file.getBytes());
        fileOutputStream.close();
        return convertedFile;
    }

    public static String generateFileName(MultipartFile multipartFile) {
        return new Date().getTime() + "-" + Objects.requireNonNull(multipartFile.getOriginalFilename()).replace(" ", "_");
    }

}

Отлично, теперь мы можем сделать сервис для отправки изображений в корзину:

package com.github.gustavovitor.photos.service.amazon;

import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.DeleteObjectRequest;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.github.gustavovitor.photos.domain.amazon.AmazonImage;
import com.github.gustavovitor.photos.repository.amazon.AmazonImageRepository;
import com.github.gustavovitor.photos.service.amazon.except.FileConversionException;
import com.github.gustavovitor.photos.service.amazon.except.InvalidImageExtensionException;
import com.github.gustavovitor.photos.service.amazon.util.AmazonClientService;
import com.github.gustavovitor.photos.util.FileUtils;
import com.github.gustavovitor.util.MessageUtil;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.io.FilenameUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Log4j2
@Service
public class AmazonS3ImageService extends AmazonClientService {

    @Autowired
    private AmazonImageRepository amazonImageRepository;

    // Upload a List of Images to AWS S3.
    public List<AmazonImage> insertImages(List<MultipartFile> images) {
        List<AmazonImage> amazonImages = new ArrayList<>();
        images.forEach(image -> amazonImages.add(uploadImageToAmazon(image)));
        return amazonImages;
    }

    // Upload image to AWS S3.
    public AmazonImage uploadImageToAmazon(MultipartFile multipartFile) {

        // Valid extensions array, like jpeg/jpg and png.
        List<String> validExtensions = Arrays.asList("jpeg", "jpg", "png");

        // Get extension of MultipartFile
        String extension = FilenameUtils.getExtension(multipartFile.getOriginalFilename());
        if (!validExtensions.contains(extension)) {
            // If file have a invalid extension, call an Exception.
            log.warn(MessageUtil.getMessage("invalid.image.extesion"));
            throw new InvalidImageExtensionException(validExtensions);
        } else {

            // Upload file to Amazon.
            String url = uploadMultipartFile(multipartFile);

            // Save image information on MongoDB and return them.
            AmazonImage amazonImage = new AmazonImage();
            amazonImage.setImageUrl(url);

            return amazonImageRepository.insert(amazonImage);
        }

    }

    public void removeImageFromAmazon(AmazonImage amazonImage) {
        String fileName = amazonImage.getImageUrl().substring(amazonImage.getImageUrl().lastIndexOf("/") + 1);
        getClient().deleteObject(new DeleteObjectRequest(getBucketName(), fileName));
        amazonImageRepository.delete(amazonImage);
    }

    // Make upload to Amazon.
    private String uploadMultipartFile(MultipartFile multipartFile) {
        String fileUrl;

        try {
            // Get the file from MultipartFile.
            File file = FileUtils.convertMultipartToFile(multipartFile);

            // Extract the file name.
            String fileName = FileUtils.generateFileName(multipartFile);

            // Upload file.
            uploadPublicFile(fileName, file);

            // Delete the file and get the File Url.
            file.delete();
            fileUrl = getUrl().concat(fileName);
        } catch (IOException e) {

            // If IOException on conversion or any file manipulation, call exception.
            log.warn(MessageUtil.getMessage("multipart.to.file.convert.except"), e);
            throw new FileConversionException();
        }

        return fileUrl;
    }

    // Send image to AmazonS3, if have any problems here, the image fragments are removed from amazon.
    // Font: https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3Client.html#putObject%28com.amazonaws.services.s3.model.PutObjectRequest%29
    private void uploadPublicFile(String fileName, File file) {
        getClient().putObject(new PutObjectRequest(getBucketName(), fileName, file)
                .withCannedAcl(CannedAccessControlList.PublicRead));
    }

}

Это мой проект прямо сейчас:

Если вы читаете этот пост для AWS S3, это все, что вам нужно знать, после этого я покажу вам коды для Maker Project и Flutter (Flutter в другом посте, этот пост слишком длинный).

Создание простого API фотоальбома

Этот API очень прост, нам нужен один маршрут API для загрузки фотографий и другой маршрут для получения информации об этих фотографиях, верно? Поэтому я хочу использовать Maker Project, чтобы сделать это за нас.

Maker Project - это абстракция для ускорения разработки RestAPI с использованием Spring Framework, кто?

Каждому снисходительному API требуется множество операций CRUD (создание, чтение, обновление, удаление), и эти CRUD могут стоить времени, а для меня времени слишком мало. Думая об этом, я создал для своих личных проектов Maker, с его помощью я могу создать CRUD API за 10 минут с применением всех бизнес-правил.

Используя Maker Project, вы можете сосредоточить все свое время на бизнес-правилах.

Итак, приступим к созданию объекта "Альбом":

package com.github.gustavovitor.photos.domain.album;

import com.github.gustavovitor.photos.domain.amazon.AmazonImage;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.List;

@Data
@Document
public class Album {

    @Id
    private String albumId;

    @Size(max = 64)
    @NotNull
    @NotBlank
    private String title;

    // A list of AmazonImage, for this simple project, the AmazonImage is one request
    // and AlgumPhoto is another request.
    private List<AmazonImage> images;

}

Хорошо, теперь нам нужно создать SpecificationObject для альбома (требования Maker):

package com.github.gustavovitor.photos.repository.album.spec;

import com.github.gustavovitor.maker.repository.MongoSpecificationBase;
import com.github.gustavovitor.photos.domain.album.Album;
import com.github.gustavovitor.photos.domain.album.QAlbum;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Predicate;

import javax.management.ReflectionException;

import static java.util.Objects.nonNull;

public class AlbumSpecification extends MongoSpecificationBase<Album> {
    public AlbumSpecification(Album object) throws ReflectionException {
        super(object);
    }

    @Override
    public Predicate toPredicate() {
        BooleanBuilder builder = new BooleanBuilder();
        if (nonNull(getObject().getTitle())) {
            builder.and(QAlbum.album.title.containsIgnoreCase(getObject().getTitle()));
        }
        return builder;
    }
}

Где находится QAlbum.class? QAlbum создается с использованием одного плагина для этого, добавьте в свой pom.xml внутри плагинов сборки это:

<!-- Code Generation QueryDsl -->
<plugin>
   <groupId>com.mysema.maven</groupId>
   <artifactId>apt-maven-plugin</artifactId>
   <version>1.1.3</version>
   <dependencies>
      <dependency>
         <groupId>com.querydsl</groupId>
         <artifactId>querydsl-apt</artifactId>
         <version>4.2.2</version>
      </dependency>
   </dependencies>
   <executions>
      <execution>
         <phase>generate-sources</phase>
         <goals>
            <goal>process</goal>
         </goals>
         <configuration>
            <outputDirectory>target/generated-sources/apt</outputDirectory>
            <processor>
               org.springframework.data.mongodb.repository.support.MongoAnnotationProcessor
            </processor>
            <logOnlyOnError>true</logOnlyOnError>
         </configuration>
      </execution>
   </executions>
</plugin>

После этого запустите команду «mvn clean install -DskipTests», чтобы сгенерировать код.

Примечание. Вам необходимо указать в своей среде IDE, где находится сгенерированный исходный корень. В моем случае, используя IntelliJ, мне нужно щелкнуть правой кнопкой мыши на / target / generated-sources и пометить каталог как - ›Корень сгенерированных источников.

Теперь нам нужно создать репозиторий:

package com.github.gustavovitor.photos.repository.album;

import com.github.gustavovitor.maker.repository.MongoRepositoryMaker;
import com.github.gustavovitor.photos.domain.album.Album;

public interface AlbumRepository extends MongoRepositoryMaker<Album, String> {
}

Примечание: расширяет MongoRepositoryMaker, используя Maker Project для создания методов за нас.

Хорошо, а теперь создайте сервис:

package com.github.gustavovitor.photos.service.album;

import com.github.gustavovitor.maker.service.MongoServiceMaker;
import com.github.gustavovitor.photos.domain.album.Album;
import com.github.gustavovitor.photos.repository.album.AlbumRepository;
import com.github.gustavovitor.photos.repository.album.spec.AlbumSpecification;
import org.springframework.stereotype.Service;

@Service
public class AlbumService extends MongoServiceMaker<AlbumRepository, Album, String, Album, AlbumSpecification> {
    @Override
    public void beforeInsert(Album object) {
        // Business rules here.
    }

    @Override
    public void beforeUpdate(String objectId, Album object) {
        // Business rules here.
    }

    @Override
    public void beforePatch(Album object) {
        // Business rules here.
    }

    @Override
    public void beforeDelete(String objectId) {
        // Business rules here.
    }
}

Maker Project предлагает нам множество методов, внутри MongoServiceMaker вы можете увидеть эти методы.

Хорошо, теперь нам нужно создать ресурсы / конечные точки:

package com.github.gustavovitor.photos.resource.album;

import com.github.gustavovitor.maker.resource.MongoResourceMaker;
import com.github.gustavovitor.photos.domain.album.Album;
import com.github.gustavovitor.photos.service.album.AlbumService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/album")
public class AlbumResource extends MongoResourceMaker<AlbumService, Album, String, Album> {

}

Наконец, еще один ресурс / конечная точка для Amazon:

package com.github.gustavovitor.photos.resource.amazon;

import com.github.gustavovitor.photos.domain.amazon.AmazonImage;
import com.github.gustavovitor.photos.service.amazon.AmazonS3ImageService;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@Getter
@RestController
@RequestMapping("/amazon")
public class AmazonResource {

    @Autowired
    private AmazonS3ImageService amazonS3ImageService;

    @PostMapping("/images")
    public ResponseEntity<List<AmazonImage>> insertImages(@RequestPart(value = "images") List<MultipartFile> images) {
        return ResponseEntity.ok(getAmazonS3ImageService().insertImages(images));
    }

}

Пойдем, попробуем?

Сначала настройте доступ к MongoDB в application.properties следующим образом:

spring.data.mongodb.host=172.17.0.2
spring.data.mongodb.database=photo

И запустите приложение Spring.

Примечание: если вы получаете сообщение об ошибке SecurityAutoConfiguration, исключите этот класс в своем SpringBootApplication, эта ошибка вызвана тем, что проект Maker включает зависимости Spring Security.

package com.github.gustavovitor.photos;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;

@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
public class PhotosApplication {

   public static void main(String[] args) {
      SpringApplication.run(PhotosApplication.class, args);
   }

}

Во-первых, пойдем со мной и протестируем AmazonResource, откройте свой Postman или другое приложение, чтобы вызвать API и позвонить:

Отлично!

Проверьте свою корзину Amazon прямо сейчас!

Ладно, ладно, успокойся! Теперь вам нужно получить ответ на запрос на создание альбома:

Что ?! Почта? 201 Создан? Какие? Ага! Maker сделает это за нас, все, что нам нужно, чтобы сделать этот CRUD, Maker Project сделает за нас.

Вы можете сделать запрос для своего API, например:

Для получения дополнительной информации о Maker Project прочтите документацию на GitHub!

Видите? Отправлять изображения в AWS S3 просто и быстрее с Maker!

Если у вас есть вопросы по этому поводу, прокомментируйте или отправьте личное сообщение в моем LinkedIn.

Интегрируйте этот API с проектом Flutter, следуя еще одному уроку: https://medium.com/analytics-vidhya/creating-an-album-photo-application-using-flutter-java-and- aws-s3-1d421c432b0d

Код этого API: https://github.com/gustavovitor/photo-album