CRUD操作を行うREST APIを作成する

とりあえずバリデーションはなしでCURD操作に注力する

build.gradle

plugins {
    id 'org.springframework.boot' version '2.5.0'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    compileOnly 'org.projectlombok:lombok'
    implementation 'org.slf4j:slf4j-api'
    implementation 'ch.qos.logback:logback-core'
    implementation 'ch.qos.logback:logback-classic'
    implementation 'com.google.guava:guava:30.1.1-jre'
    implementation 'org.apache.commons:commons-lang3'
    implementation 'commons-beanutils:commons-beanutils:1.9.2'
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.4'
    runtimeOnly 'mysql:mysql-connector-java' 
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}

/src/main/resources/application.yml

server:
  servlet:
    context-path: /sample
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:33060/sample
    username: mysql-app
    password: mysql-app
mybatis:
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    org:
      springframework: WARN
    com:
      example:
        demo:
          domain:
            mapper: DEBUG

/src/main/java/com/example/demo/DemoApplication.java

package com.example.demo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

// https://spring.pleiades.io/spring-boot/docs/current/reference/html/features.html
@SpringBootApplication
@MapperScan("com.example.demo.domain.mapper")
public class DemoApplication {

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

}

/src/main/java/com/example/demo/web/controller/UsersController.java

package com.example.demo.web.controller;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.service.UsersService;
import com.example.demo.service.dto.UserDto;
import com.example.demo.web.controller.helper.UsersHelper;
import com.example.demo.web.dto.request.UsersCreateRequest;
import com.example.demo.web.dto.request.UsersUpdateRequest;
import com.example.demo.web.dto.response.UsersCreateResponse;
import com.example.demo.web.dto.response.UsersGetListResponse;
import com.example.demo.web.dto.response.UsersGetResponse;

import lombok.RequiredArgsConstructor;

@CrossOrigin
@RestController
@RequestMapping(value = "/users", produces = "application/json;charset=UTF-8")
@RequiredArgsConstructor
public class UsersController {

    private static final Logger LOGGER = LoggerFactory.getLogger(UsersController.class);

    private final UsersService usersService;

    @RequestMapping(method = RequestMethod.GET)
    public UsersGetListResponse getList() {
        List<UserDto> resultList = usersService.findAll();
        return UsersHelper.convertToUsersGetListResponse(resultList);
    }

    @RequestMapping(method = RequestMethod.GET, value = "{id}")
    public UsersGetResponse get(@PathVariable("id") Long id) {
        LOGGER.info("#id : {}", id);
        UserDto result = usersService.findById(id);
        return UsersHelper.convertToUsersGetResponse(result);
    }

    @RequestMapping(method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.CREATED)
    public UsersCreateResponse create(@Validated @RequestBody UsersCreateRequest request) {
        LOGGER.info("#UsersCreateRequest : {}", request);
        UserDto userDto = UsersHelper.convertFromUsersCreateRequest(request);
        UserDto result = usersService.create(userDto);
        return UsersHelper.convertToUsersCreateResponse(result);
    }

    @RequestMapping(method = RequestMethod.PUT, value = "{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void update(@PathVariable("id") Long id, @Validated @RequestBody UsersUpdateRequest request) {
        LOGGER.info("#id : {}", id);
        LOGGER.info("#UsersUpdateRequest : {}", request);
        UserDto userDto = UsersHelper.convertFromUsersUpdateRequest(id, request);
        usersService.update(userDto);
    }

    @RequestMapping(method = RequestMethod.DELETE, value = "{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable("id") Long id) {
        LOGGER.info("#id : {}", id);
        usersService.delete(id);
    }

}

/src/main/java/com/example/demo/web/controller/helper/UsersHelper.java

package com.example.demo.web.controller.helper;

import java.util.List;
import java.util.stream.Collectors;

import com.example.demo.common.utils.ConvertUtils;
import com.example.demo.service.dto.UserDto;
import com.example.demo.web.dto.request.UsersCreateRequest;
import com.example.demo.web.dto.request.UsersUpdateRequest;
import com.example.demo.web.dto.response.UsersCreateResponse;
import com.example.demo.web.dto.response.UsersGetListResponse;
import com.example.demo.web.dto.response.UsersGetListResponse.User;
import com.example.demo.web.dto.response.UsersGetResponse;

public class UsersHelper {

    public static UsersGetListResponse convertToUsersGetListResponse(List<UserDto> list) {
        final UsersGetListResponse response = new UsersGetListResponse();
        List<User> users = list.stream().map((dto) -> {
            final UsersGetListResponse.User user = new UsersGetListResponse.User();
            ConvertUtils.copyProperties(dto, user);
            return user;
        }).collect(Collectors.toList());
        response.setUsers(users);
        return response;
    }

    public static UsersGetResponse convertToUsersGetResponse(UserDto dto) {
        final UsersGetResponse response = new UsersGetResponse();
        ConvertUtils.copyProperties(dto, response);
        return response;
    }

    public static UserDto convertFromUsersCreateRequest(UsersCreateRequest request) {
        final UserDto userDto = new UserDto();
        ConvertUtils.copyProperties(request, userDto);
        return userDto;
    }

    public static UsersCreateResponse convertToUsersCreateResponse(UserDto dto) {
        final UsersCreateResponse response = new UsersCreateResponse();
        ConvertUtils.copyProperties(dto, response);
        return response;
    }

    public static UserDto convertFromUsersUpdateRequest(Long id, UsersUpdateRequest request) {
        final UserDto userDto = new UserDto();
        ConvertUtils.copyProperties(request, userDto);
        userDto.setUserId(id);
        return userDto;
    }

}

/src/main/java/com/example/demo/web/dto/request/UsersCreateRequest.java

package com.example.demo.web.dto.request;

import lombok.Data;

@Data
public class UsersCreateRequest {

    private String userName;
    private String mailAddress;
    private String password;
    private String confirmPassword;

}

/src/main/java/com/example/demo/web/dto/request/UsersUpdateRequest.java

package com.example.demo.web.dto.request;

import lombok.Data;

@Data
public class UsersUpdateRequest {

    private String userName;
    private String mailAddress;
    private String password;
    private String confirmPassword;
    private String deleted;

}

/src/main/java/com/example/demo/web/dto/response/UsersGetListResponse.java

package com.example.demo.web.dto.response;

import java.util.List;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.google.common.collect.Lists;

import lombok.Data;

@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UsersGetListResponse {

    private List<User> users = Lists.newArrayList();

    @Data
    public static class User {
        private String userId;
        private String userName;
        private String mailAddress;
    }

}

/src/main/java/com/example/demo/web/dto/response/UsersGetResponse.java

package com.example.demo.web.dto.response;

import com.fasterxml.jackson.annotation.JsonInclude;

import lombok.Data;

@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UsersGetResponse {

    private String userId;
    private String userName;
    private String mailAddress;

}

/src/main/java/com/example/demo/web/dto/response/UsersCreateResponse.java

package com.example.demo.web.dto.response;

import com.fasterxml.jackson.annotation.JsonInclude;

import lombok.Data;

@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UsersCreateResponse {

    private String userId;
    private String userName;
    private String mailAddress;
    private String registTime;
    private String updateTime;

}

/src/main/java/com/example/demo/service/UsersService.java

package com.example.demo.service;

import java.util.List;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.demo.domain.model.Users;
import com.example.demo.domain.repository.UsersRepository;
import com.example.demo.service.dto.UserDto;
import com.example.demo.utils.DateUtils;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class UsersService {

    private static final Logger LOGGER = LoggerFactory.getLogger(UsersService.class);
    
    private final UsersRepository usersRepository;

    @Transactional(readOnly = true)
    public List<UserDto> findAll() {
        return usersRepository.findAll().stream().map((users) -> {
            return convert(users);
        }).collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public UserDto findById(Long id) {
        Users users = usersRepository.findById(id);
        if (users == null) {
            return null;
        }
        return convert(users);
    }

    @Transactional(readOnly = false)
    public UserDto create(UserDto dto) {
        final Users users = Users.builder()
                .mailAddress(dto.getMailAddress())
                .userName(dto.getUserName())
                .password(dto.getPassword())
                .registTime(DateUtils.getThreadDateTime())
                .updateTime(DateUtils.getThreadDateTime())
                .deleted(0)
                .build();
        usersRepository.create(users);
        LOGGER.info("#users : {}", users);
        return convert(users);
    }

    @Transactional(readOnly = false)
    public void update(UserDto dto) {
        final Users users = Users.builder()
                .id(dto.getUserId())
                .mailAddress(dto.getMailAddress())
                .userName(dto.getUserName())
                .password(dto.getPassword())
                .updateTime(DateUtils.getThreadDateTime())
                .deleted(dto.getDeleted())
                .build();
        usersRepository.update(users);
    }

    @Transactional(readOnly = false)
    public void delete(Long id) {
        usersRepository.delete(id);
    }

    private UserDto convert(Users users) {
        UserDto dto = new UserDto();
        dto.setUserId(users.getId());
        dto.setMailAddress(users.getMailAddress());
        dto.setUserName(users.getUserName());
        dto.setRegistTime(users.getRegistTime());
        dto.setUpdateTime(users.getUpdateTime());
        dto.setDeleted(users.getDeleted());
        return dto;
    }

}

/src/main/java/com/example/demo/service/dto/UserDto.java

package com.example.demo.service.dto;

import java.util.Date;

import lombok.Data;

@Data
public class UserDto {

    private Long userId;
    private String userName;
    private String mailAddress;
    private String password;
    private Date registTime;
    private Date updateTime;
    private Integer deleted;

}

/src/main/java/com/example/demo/domain/repository/UsersRepository.java

package com.example.demo.domain.repository;

import java.util.List;

import org.springframework.stereotype.Repository;

import com.example.demo.domain.mapper.UsersMapper;
import com.example.demo.domain.model.Users;

import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class UsersRepository {

    private final UsersMapper usersMapper;

    public List<Users> findAll() {
        return usersMapper.findAll();
    }

    public Users findById(Long id) {
        return usersMapper.findById(id);
    }

    public int create(Users users) {
        return usersMapper.insert(users);
    }

    public int update(Users users) {
        return usersMapper.update(users);
    }

    public int delete(Long id) {
        return usersMapper.delete(id);
    }

}

/src/main/java/com/example/demo/domain/mapper/UsersMapper.java

package com.example.demo.domain.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import com.example.demo.domain.model.Users;

@Mapper
public interface UsersMapper {

    List<Users> findAll();

    Users findById(@Param("id") Long id);

    Integer insert(@Param("users") Users users);

    Integer update(@Param("users") Users users);

    Integer delete(@Param("id") Long id);

}

/src/main/resources/com/example/demo/domain/mapper/UsersMapper.xml

<?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.domain.mapper.UsersMapper">
    <select id="findAll" resultType="com.example.demo.domain.model.Users">
        SELECT
            ID,
            MAIL_ADDRESS,
            USER_NAME,
            PASSWORD,
            LAST_LOGIN_TIME,
            REGIST_TIME,
            UPDATE_TIME,
            DELETED
        FROM
            t_users
        WHERE
            DELETED = 0
        ORDER BY
            ID
    </select>
    <select id="findById" resultType="com.example.demo.domain.model.Users">
        SELECT
            ID,
            MAIL_ADDRESS,
            USER_NAME,
            PASSWORD,
            LAST_LOGIN_TIME,
            REGIST_TIME,
            UPDATE_TIME,
            DELETED
        FROM
            t_users
        WHERE
            ID = #{id}
    </select>
    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO t_users
        (
            MAIL_ADDRESS,
            USER_NAME,
            PASSWORD,
            LAST_LOGIN_TIME,
            REGIST_TIME,
            UPDATE_TIME,
            DELETED
        )
        VALUES
        (
            #{users.mailAddress},
            #{users.userName},
            SHA2(#{users.password}, 256),
            NULL,
            #{users.registTime},
            #{users.updateTime},
            0
        )
    </insert>
    <update id="update">
        UPDATE 
            t_users
        SET
            UPDATE_TIME = #{users.updateTime} 
        <if test="users.mailAddress != null">
            ,MAIL_ADDRESS = #{users.mailAddress} 
        </if>
        <if test="users.userName != null">
            ,USER_NAME = #{users.userName} 
        </if>
        <if test="users.password != null">
            ,PASSWORD = SHA2(#{users.password}, 256) 
        </if>
        <if test="users.deleted != null">
            ,DELETED = #{users.deleted} 
        </if>
        WHERE
            ID = #{users.id} 
    </update>
    <delete id="delete">
        DELETE FROM 
            t_users
        WHERE
            ID = #{id} 
    </delete>
</mapper>

/src/main/java/com/example/demo/common/utils/DateUtils.java

package com.example.demo.common.utils;

import java.util.Date;
import java.util.regex.Pattern;

import org.apache.commons.lang3.time.DateFormatUtils;

public class DateUtils {

    private static final String THREAD_LOCAL_KEY = "___THREAD_LOCAL_DATE___";

    // ISO8601拡張形式
    private static final Pattern DEFAULT_DATE_TIME_FORMAT_PATTERN = Pattern
            .compile("[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\+[0-9]{2}:[0-9]{2}");

    private DateUtils() {
    }

    public static void setThreadDateTime(final Date datetime) {
        ThreadLocalUtils.set(THREAD_LOCAL_KEY, datetime);
    }

    public static Date getThreadDateTime() {
        return ThreadLocalUtils.get(THREAD_LOCAL_KEY, Date.class);
    }

    public static String format(Date date) {
        if (date == null) {
            return null;
        }
        return DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(date);
    }

    public static Date parse(String strDate) {
        try {
            return DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse(strDate);
        } catch (Exception e) {
            return null;
        }
    }

    public static boolean isDefaultDateTimeFormat(String str) {
        if (str == null) {
            return false;
        }
        return DEFAULT_DATE_TIME_FORMAT_PATTERN.matcher(str).matches();
    }

}

/src/main/java/com/example/demo/common/utils/ThreadLocalUtils.java

package com.example.demo.common.utils;

import java.util.Map;

import com.google.common.collect.Maps;

public class ThreadLocalUtils {

    private ThreadLocalUtils() {
    }

    private static final ThreadLocal<Map<String, Object>> POOL = new ThreadLocal<Map<String, Object>>() {
        @Override
        protected Map<String, Object> initialValue() {
            return Maps.newHashMap();
        }
    };

    public static Object set(String key, Object value) {
        return POOL.get().put(key, value);
    }

    public static <T> T get(String key, Class<T> clazz) {
        return clazz.cast(POOL.get().get(key));
    }

    public static void clear() {
        POOL.get().clear();
    }
}

/src/main/java/com/example/demo/common/utils/ConvertUtils.java

package com.example.demo.common.utils;

import java.lang.reflect.InvocationTargetException;
import java.util.Date;

import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.ConvertUtilsBean;
import org.apache.commons.beanutils.Converter;
import org.apache.commons.beanutils.converters.DateTimeConverter;

public class ConvertUtils {

    private static final BeanUtilsBean BUB;
    static {
        BUB = new BeanUtilsBean(new ConvertUtilsBean(), BeanUtilsBean.getInstance().getPropertyUtils());
        BUB.getConvertUtils().register(new MyStringConverter(), String.class);
        final MyDateConverter myDateConverter = new MyDateConverter();
        myDateConverter.setPatterns(new String[] { "yyyyMMdd", "yyyyMMddHHmmss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss",
                "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss" });
        BUB.getConvertUtils().register(myDateConverter, Date.class);
    }

    static class MyStringConverter implements Converter {
        @Override
        public <T> T convert(Class<T> type, Object value) {
            if (value == null) {
                return null;
            } else {
                if (value instanceof java.util.Date) {
                    return type.cast(DateUtils.format((Date) value));
                }
                return type.cast(value.toString());
            }
        }
    }

    static class MyDateConverter extends DateTimeConverter {
        public MyDateConverter() {
            super();
        }

        public MyDateConverter(Object defaultValue) {
            super(defaultValue);
        }

        @Override
        protected Class<?> getDefaultType() {
            return Date.class;
        }

        @Override
        public <T> T convert(Class<T> type, Object value) {
            if (value != null && value instanceof java.lang.String
                    && DateUtils.isDefaultDateTimeFormat((String) value)) {
                return type.cast(DateUtils.parse((String) value));
            } else {
                return super.convert(type, value);
            }
        }
    }

    public static void copyProperties(Object src, Object dest) {
        try {
            BUB.copyProperties(dest, src);
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new IllegalStateException(e);
        }
    }

}

/src/main/java/com/example/demo/web/filter/ThreadLocalFilter.java

package com.example.demo.web.filter;

import java.io.IOException;
import java.util.Date;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.springframework.stereotype.Component;

import com.example.demo.common.utils.DateUtils;

@Component
public class ThreadLocalFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        DateUtils.setThreadDateTime(new Date());
        chain.doFilter(request, response);
    }

}