RESTful Demo

数据库

  • 数据库创建脚本, create.sql
-- Create database
CREATE DATABASE `drift_bottle`;

-- Create new user add grant privileges
GRANT INSERT, SELECT, UPDATE ON `drift_bottle`.* TO `dft_bt`@`127.0.0.1` IDENTIFIED BY 'QGUHuLhFy';
FLUSH PRIVILEGES;

-- Change database
USE `drift_bottle`;

-- Create tables
CREATE TABLE `user_info` (
    `row_id` CHAR(36) NOT NULL COMMENT "UUID to distinguish a user",
    `user_name` VARCHAR(36) NOT NULL COMMENT "User name display to user",
    `create_date` DATETIME NOT NULL COMMENT "Row create date",
    PRIMARY KEY (`row_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  • 数据库删除脚本, destory.sql
-- Drop database
DROP DATABASE IF EXISTS `drift_bottle`;

-- Drop user
DROP USER IF EXISTS `dft_bt`@`127.0.0.1`;

数据库连接池

  • 下载 Mariadb 的 JDBC 驱动, 将 JAR 包复制进 /usr/share/tomcat8/lib/
  • 定义 Tomcat 内建连接池, 此处采用的是应用内部定义, 当然也可以直接更改 /etc/tomcat8/context.xml 添加连接池, web/META-INF/context.xml
<?xml version="1.0" encoding="UTF-8"?>
<Context>
    <Resource
        name="jdbc/DriftBottle"
        type="javax.sql.DataSource"
        maxTotal="20"
        maxIdle="5"
        maxWaitMillis="10000"
        username="dft_bt"
        password="QGUHuLhFy"
        driverClassName="org.mariadb.jdbc.Driver"
        defaultTransactionIsolation="READ_COMMITTED"
        url="jdbc:mariadb://127.0.0.1:3306/drift_bottle" />
</Context>

依赖

  • pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.seliote</groupId>
    <artifactId>drift-bottle-endpoint</artifactId>
    <version>1.0</version>
    <packaging>war</packaging>

    <properties>
        <!-- source file encoding -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <!-- if not add two line will warning that version not support -->
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.targer>1.8</maven.compiler.targer>
        <!-- Spring version -->
        <spring.version>5.1.9.RELEASE</spring.version>
        <!-- Log4j version -->
        <log4j.version>2.12.1</log4j.version>
        <!-- Jackson version -->
        <jackson.version>2.9.9</jackson.version>
    </properties>

    <build>
        <!-- Compile source file root -->
        <sourceDirectory>src/main/java</sourceDirectory>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
        <!-- Test source file root -->
        <testSourceDirectory>src/test/java</testSourceDirectory>
        <testResources>
            <testResource>
                <directory>src/test/resources</directory>
            </testResource>
        </testResources>
        <!-- Plugins for build -->
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.2.3</version>
                <configuration>
                    <warSourceDirectory>web</warSourceDirectory>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <!-- Servlet -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
        <!-- Spring web MVC -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
            <scope>compile</scope>
        </dependency>
        <!-- Spring OXM use for Object/XML mapper -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-oxm</artifactId>
            <version>${spring.version}</version>
            <scope>compile</scope>
        </dependency>
        <!-- Websocket for spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
            <version>${spring.version}</version>
            <scope>compile</scope>
        </dependency>
        <!-- Spring Data JPA dependency -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
            <version>2.1.10.RELEASE</version>
            <scope>compile</scope>
        </dependency>
        <!-- Hibernate dependency scope is runtime to avoid use hibernate api -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>5.4.4.Final</version>
            <scope>runtime</scope>
        </dependency>
        <!-- Spring Test -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>
        <!--  JUnit Test, version 4.12 had some error -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13-beta-3</version>
            <scope>test</scope>
        </dependency>
        <!-- Maven repository does not has JPA dependency, use eclipse JPA instead of it -->
        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>javax.persistence</artifactId>
            <version>2.2.1</version>
            <scope>compile</scope>
        </dependency>
        <!-- Validation api -->
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
            <scope>compile</scope>
        </dependency>
        <!-- Validation api implement -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.17.Final</version>
            <scope>runtime</scope>
        </dependency>
        <!-- Log4j2 api -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>${log4j.version}</version>
            <scope>compile</scope>
        </dependency>
        <!-- Log4j2 implement -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>${log4j.version}</version>
            <scope>runtime</scope>
        </dependency>
        <!-- JCL bridge interface for log4j2 -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-jcl</artifactId>
            <version>${log4j.version}</version>
            <scope>compile</scope>
        </dependency>
        <!-- Slf4j bridge interface for log4j2 -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>${log4j.version}</version>
            <scope>runtime</scope>
        </dependency>
        <!-- Jackson implement -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>${jackson.version}</version>
            <scope>compile</scope>
        </dependency>
        <!-- Jackson annotation -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>${jackson.version}</version>
            <scope>compile</scope>
        </dependency>
        <!-- Jackson databind -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
            <scope>compile</scope>
        </dependency>
        <!-- Jackson extension support for JSR-310,like Date and Time API in Java 8 -->
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>${jackson.version}</version>
            <scope>compile</scope>
        </dependency>
        <!-- Json for handle some simple data -->
        <dependency>
            <groupId>org.json</groupId>
            <artifactId>json</artifactId>
            <version>20190722</version>
            <scope>compile</scope>
        </dependency>
        <!-- Use for generate String from entity -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
</project>

应用全局配置

  • config.properties
#################################################
##  Application properties configuration
#################################################

#######################################
## Database configuration
#######################################

# Datasource name
database.datasourceName=jdbc/DriftBottle
# Database dialect for hibernate
database.hibernateDialect=org.hibernate.dialect.MariaDB103Dialect


#######################################
## System payload config
#######################################

# Thread pool size
system.payload=20

Log4J2 配置

  • main/resources/log4j2.xml
<?xml version="1.0" encoding="UTF-8" ?>

<!-- DEBUG for system running information -->
<!-- INFO for user data running information -->
<!-- WARNING for user data error -->
<!-- ERROR for system running error -->

<!-- Root logger level is debug -->
<configuration status="DEBUG">
    <appenders>
        <!-- Define a console appender -->
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %l: %msg%n"/>
        </Console>
        <!-- Define a rolling file appender -->
        <RollingFile name="FileAppender"
                     fileName="/tmp/logs/application.log"
                     filePattern="/tmp/logs/application-%d{yyyy-MM-dd}-%i.log">
            <!-- fileName="../logs/application.log"-->
            <!-- filePattern="../logs/application-%d{yyyy-MM-dd}-%i.log">-->
            <PatternLayout>
                <pattern>%d{HH:mm:ss.SSS} [%t] %-5level %l: %msg%n</pattern>
            </PatternLayout>
            <Policies>
                <!-- Max log file size -->
                <SizeBasedTriggeringPolicy size="10 MB"/>
            </Policies>
            <!-- Count of log file -->
            <DefaultRolloverStrategy min="1" max="10"/>
        </RollingFile>
    </appenders>

    <loggers>
        <!-- Root log output to console -->
        <root level="INFO">
            <!-- TODO If in produce environment, remove this! -->
            <appender-ref ref="Console"/>
        </root>
        <!-- Package of com.seliote will put log to console and file -->
        <logger name="com.seliote" level="DEBUG" additivity="false">
            <appender-ref ref="FileAppender"/>
            <!-- TODO If in produce environment, remove this! -->
            <appender-ref ref="Console"/>
        </logger>
        <!-- Ensure framework log also could output -->
        <logger name="org.apache" level="DEBUG"/>
        <logger name="org.springframework" level="DEBUG"/>
    </loggers>
</configuration>
  • test/resources/log4j2-test.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Root logger level is debug -->
<configuration status="DEBUG">
    <appenders>
        <!-- Define a console appender -->
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %l: %msg%n"/>
        </Console>
    </appenders>

    <loggers>
        <!-- Root log output to console -->
        <root level="DEBUG">
            <!-- TODO If in produce environment, remove this! -->
            <appender-ref ref="Console"/>
        </root>
        <!-- Package of com.seliote will put log to console and file -->
        <logger name="com.seliote" level="DEBUG" additivity="false">
            <appender-ref ref="Console" />
        </logger>
        <!-- Ensure framework log also could output -->
        <logger name="org.apache" level="DEBUG"/>
        <logger name="org.springframework" level="DEBUG"/>
    </loggers>
</configuration>

Spring 配置与启动

  • 几个标记接口
package com.seliote.driftbottleendpoint;

/**
 * Root context component scan mark interface
 *
 * @author seliote
 * @version 1.0 2019-09-08
 */
public interface RootComponentScanMark {
}
package com.seliote.driftbottleendpoint.md;

/**
 * Md context component scan mark interface
 *
 * @author seliote
 * @version 1.0 2019-09-09
 */
public interface MdComponentScanMark {
}
package com.seliote.driftbottleendpoint.md.repository;

/**
 * Mobile device model repository component scan mark
 *
 * @author seliote
 * @version 1.0 2019-09-09
 */
public interface MdRepoComponentScanMark {
}
  • Root Context, RootContextConfig.java
package com.seliote.driftbottleendpoint.config;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.seliote.driftbottleendpoint.RootComponentScanMark;
import com.seliote.driftbottleendpoint.md.repository.MdRepoComponentScanMark;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.*;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.Ordered;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
import org.springframework.web.bind.annotation.ControllerAdvice;

import javax.persistence.SharedCacheMode;
import javax.persistence.ValidationMode;
import javax.sql.DataSource;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;

/**
 * Root context configuration file class
 *
 * @author seliote
 * @version 1.0 2019-09-08
 */
@SuppressWarnings("DefaultAnnotationParam")
@Configuration
// Scan anything besides @Controller or @ControllerAdvice
@ComponentScan(
        basePackageClasses = {RootComponentScanMark.class},
        excludeFilters = @ComponentScan.Filter({Controller.class, ControllerAdvice.class})
)
// Enable Spring transaction and config it
@EnableTransactionManagement(
        mode = AdviceMode.PROXY,
        proxyTargetClass = false,
        order = Ordered.LOWEST_PRECEDENCE
)
// Enable Spring Data Jpa and config it
@EnableJpaRepositories(
        basePackageClasses = {MdRepoComponentScanMark.class},
        entityManagerFactoryRef = "entityManagerFactory",
        transactionManagerRef = "jpaTransactionManager"
)
// Enable async
@EnableAsync(
        mode = AdviceMode.PROXY,
        proxyTargetClass = false,
        order = Ordered.HIGHEST_PRECEDENCE
)
@EnableScheduling
@PropertySource({"classpath:config.properties"})
public class RootContextConfig implements AsyncConfigurer, SchedulingConfigurer {
    @Value("${database.datasourceName}")
    private String mDatasourceName;
    @Value("${database.hibernateDialect}")
    private String mHibernateDialect;
    @Value("${system.payload}")
    private int mThreadPoolSize;

    // Thread pool logger
    private final static Logger THREAD_POOL_LOGGER = LogManager.getLogger("[Thread-Pool]");

    private Logger mLogger = LogManager.getLogger();

    @Override
    public Executor getAsyncExecutor() {
        mLogger.debug("Set async Executor");
        return threadPoolTaskScheduler();
    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        mLogger.debug("Set task Scheduler");
        scheduledTaskRegistrar.setTaskScheduler(threadPoolTaskScheduler());
    }

    // Use for Spring to parse @Value("${}")
    @Bean
    // Be careful that PropertySourcesPlaceholderConfigurer bean define must be static
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }

    /**
     * The bean of type Executor and Scheduler
     */
    @Bean
    public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
        mLogger.debug("Set thread pool with size " + mThreadPoolSize);
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        threadPoolTaskScheduler.setPoolSize(mThreadPoolSize);
        threadPoolTaskScheduler.setThreadNamePrefix("Thread-Pool: ");
        // The max seconds to prevent close
        threadPoolTaskScheduler.setAwaitTerminationSeconds(10);
        // Wait if thread is not shutdown
        threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(true);
        // Error handler for thread pool
        threadPoolTaskScheduler.setErrorHandler(throwable ->
                THREAD_POOL_LOGGER.warn("Thread pool occurred an exception: " + throwable.getMessage()));
        // Reject handler
        threadPoolTaskScheduler.setRejectedExecutionHandler((runnable, threadPoolExecutor) ->
                THREAD_POOL_LOGGER.warn("Thread " + runnable.toString() + " had bean reject by "
                        + threadPoolExecutor.toString())
        );
        return threadPoolTaskScheduler;
    }

    @Bean
    public DataSource dataSource() {
        mLogger.debug("Set JPA datasource with name " + mDatasourceName);
        JndiDataSourceLookup jndiDataSourceLookup = new JndiDataSourceLookup();
        return jndiDataSourceLookup.getDataSource(mDatasourceName);
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        // The properties of EntityManager
        Map<String, Object> properties = new HashMap<>();
        // Disable schema auto create
        properties.put("javax.persistence.schema-generation.database.action", "none");
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        // Set dialect
        mLogger.debug("Set JPA database platform: " + mHibernateDialect);
        hibernateJpaVendorAdapter.setDatabasePlatform(mHibernateDialect);
        LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
        entityManagerFactoryBean.setJpaVendorAdapter(hibernateJpaVendorAdapter);
        entityManagerFactoryBean.setDataSource(dataSource());
        // Parameter is String..., multiple package could add below
        String[] packageToScan = new String[]{"com.seliote.driftbottleendpoint.md.entity"};
        mLogger.debug("Set JPA entity manager factory packages: " + Arrays.toString(packageToScan));
        entityManagerFactoryBean.setPackagesToScan(packageToScan);
        entityManagerFactoryBean.setSharedCacheMode(SharedCacheMode.ENABLE_SELECTIVE);
        entityManagerFactoryBean.setValidationMode(ValidationMode.NONE);
        entityManagerFactoryBean.setJpaPropertyMap(properties);
        return entityManagerFactoryBean;
    }

    /**
     * Transaction manager for JPA
     */
    @Bean
    public PlatformTransactionManager jpaTransactionManager() {
        mLogger.debug("Set JPA transaction manager");
        return new JpaTransactionManager(entityManagerFactory().getObject());
    }

    /**
     * Bean validator factory, figure out the implement as hibernate validator
     */
    @Bean
    public LocalValidatorFactoryBean localValidatorFactoryBean() throws ClassNotFoundException {
        mLogger.debug("Set LocalValidatorFactoryBean and its message source");
        LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
        // Figure out implement
        localValidatorFactoryBean.setProviderClass(Class.forName("org.hibernate.validator.HibernateValidator"));
        return localValidatorFactoryBean;
    }

    /**
     * Method post processor, configuration for validation method parameter and return value,
     * figure out validator to prevent default use the validator not has message source.
     * This processor will find for @Validated or @ValidateOnExecution and create proxy.
     */
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() throws ClassNotFoundException {
        mLogger.debug("Set MethodValidationPostProcessor");
        MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
        methodValidationPostProcessor.setValidator(localValidatorFactoryBean());
        return methodValidationPostProcessor;
    }

    @Bean
    public ObjectMapper objectMapper() {
        mLogger.debug("Set ObjectMapper with WRITE_DATES_AS_TIMESTAMPS-false, " +
                "ADJUST_DATES_TO_CONTEXT_TIME_ZONE-false");
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.findAndRegisterModules();
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        objectMapper.configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false);
        return objectMapper;
    }
}
  • Servlet Context, MdContextConfig.java
package com.seliote.driftbottleendpoint.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.seliote.driftbottleendpoint.md.MdComponentScanMark;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.stereotype.Controller;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;

/**
 * Md context configuration file class
 *
 * @author seliote
 * @version 1.0 2019-09-09
 */
@Configuration
@EnableWebMvc
@ComponentScan(
        basePackageClasses = {MdComponentScanMark.class},
        // Be careful that useDefaultFilters must be false!!!
        useDefaultFilters = false,
        includeFilters = @ComponentScan.Filter({Controller.class, ControllerAdvice.class})
)
public class MdContextConfig implements WebMvcConfigurer {
    private Logger mLogger = LogManager.getLogger();

    private SpringValidatorAdapter mSpringValidatorAdapter;
    private ObjectMapper mObjectMapper;

    @Autowired
    public void setSpringValidatorAdapter(SpringValidatorAdapter springValidatorAdapter) {
        mSpringValidatorAdapter = springValidatorAdapter;
    }

    @Autowired
    public void setObjectMapper(ObjectMapper objectMapper) {
        mLogger.debug("Inject ObjectMapper.");
        mObjectMapper = objectMapper;
    }

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        mLogger.debug("Set content negotiation only support JSON_UTF-8, ignore path extension, " +
                "favor parameter, accept header");
        // Only support JSON
        configurer.favorPathExtension(false)
                .favorParameter(false)
                .ignoreAcceptHeader(true)
                .defaultContentType(MediaType.APPLICATION_JSON_UTF8)
                .mediaType("json", MediaType.APPLICATION_JSON_UTF8);
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        mLogger.debug("Set message converters only support json(application or text), " +
                "be careful that request header must set Content-Type like application/json");
        // Only support JSON
        // Be careful: This required mobile device set Content-Type: application/json
        MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter
                = new MappingJackson2HttpMessageConverter();
        mappingJackson2HttpMessageConverter.setSupportedMediaTypes(Arrays.asList(
                new MediaType("application", "json"),
                new MediaType("text", "json")
        ));
        mLogger.debug("Set message converts default charset to UTF-8");
        mappingJackson2HttpMessageConverter.setDefaultCharset(StandardCharsets.UTF_8);
        mappingJackson2HttpMessageConverter.setObjectMapper(mObjectMapper);
        converters.add(mappingJackson2HttpMessageConverter);
    }

    /**
     * Spring default use their own Spring Validator for SpringMVC parameter.
     * This will override this behavior.
     */
    @Override
    public Validator getValidator() {
        mLogger.debug("Set Validator for SpringMVC module MD");
        return mSpringValidatorAdapter;
    }
}
  • Boot config, Bootstrap.java
package com.seliote.driftbottleendpoint.config;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;

/**
 * Bootstrap for Spring
 *
 * @author seliote
 * @version 1.0 2019-09-09
 */
public class Bootstrap implements WebApplicationInitializer {
    private Logger mLogger = LogManager.getLogger();
    @Override
    public void onStartup(ServletContext servletContext) {
        mLogger.debug("Starting application DriftBottle...");
        // Use default servlet to handle static resources
        servletContext.getServletRegistration("default").addMapping("/resources/*");
        mLogger.debug("Set default servlet for static resources in /resources/*");
        // Root context
        AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
        rootContext.register(RootContextConfig.class);
        servletContext.addListener(new ContextLoaderListener(rootContext));
        mLogger.debug("Set root context finished");
        // Md Context
        AnnotationConfigWebApplicationContext mdContext = new AnnotationConfigWebApplicationContext();
        mdContext.register(MdContextConfig.class);
        ServletRegistration.Dynamic mdDynamic =
                servletContext.addServlet("mdDispatcherServlet", new DispatcherServlet(mdContext));
        mdDynamic.setLoadOnStartup(1);
        // Or "/" not "/*"
        mdDynamic.addMapping("/md/*");
        mLogger.debug("Set md context finished mapping to /md/*");
        mLogger.debug("Bootstrap finished!");
    }
}
  • Exception handler, ExceptionHandler.java
package com.seliote.driftbottleendpoint.md.config;

import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import javax.validation.ConstraintViolationException;

/**
 * Exception handler for md module
 *
 * @author seliote
 * @version 1.0 2019-09-14
 */
@SuppressWarnings("unused")
@ControllerAdvice
public class ExceptionHandler {
    private Logger mLogger = LogManager.getLogger();

    @org.springframework.web.bind.annotation.ExceptionHandler({HttpMessageNotReadableException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    private ErrorMsgResp handler(HttpMessageNotReadableException ex) {
        mLogger.warn("HttpMessageNotReadableException: " + ex.getMessage());
        return new ErrorMsgResp(400, "Request parameter convert had error!");
    }

    @org.springframework.web.bind.annotation.ExceptionHandler(
            {
                    MethodArgumentNotValidException.class,
                    ConstraintViolationException.class
            }
    )
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    private ErrorMsgResp handler(Exception ex) {
        mLogger.warn("MethodArgumentNotValidException: " + ex.getMessage());
        return new ErrorMsgResp(401, "Request parameter validation error!");
    }
}

/**
 * Response entity when catch exception
 */
@SuppressWarnings({"unused", "WeakerAccess"})
class ErrorMsgResp {
    private int mCode;
    private String mMsg;

    /**
     * Must include a non parameter constructor
     */
    public ErrorMsgResp() {
    }

    public ErrorMsgResp(int code, String msg) {
        mCode = code;
        mMsg = msg;
    }

    @JsonProperty("code")
    public int getCode() {
        return mCode;
    }

    @JsonProperty("code")
    public void setCode(int code) {
        mCode = code;
    }

    @JsonProperty("msg")
    public String getMsg() {
        return mMsg;
    }

    @JsonProperty("msg")
    public void setMsg(String msg) {
        mMsg = msg;
    }
}

Bean 验证

  • Not blank, NotBlank.java
package com.seliote.driftbottleendpoint.md.validation;

import javax.validation.*;
import javax.validation.constraints.NotNull;
import java.lang.annotation.*;

/**
 * Validation for String not null and trim().length() > 0
 *
 * @author seliote
 * @version 1.0 2019-09-15
 */
@SuppressWarnings("unused")
@Target({
        ElementType.ANNOTATION_TYPE,
        ElementType.CONSTRUCTOR,
        ElementType.FIELD,
        ElementType.METHOD,
        ElementType.PARAMETER
})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@NotNull
// If not has other validator could use {}, tis annotation could not omit
@Constraint(validatedBy = {NotBlankValidator.class})
@ReportAsSingleViolation
public @interface NotBlank {
    String message() default "Not null or empty";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @Target({
            ElementType.ANNOTATION_TYPE,
            ElementType.CONSTRUCTOR,
            ElementType.FIELD,
            ElementType.METHOD,
            ElementType.PARAMETER
    })
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @interface List {
        NotBlank[] value();
    }
}

/**
 * NotBlank validator implement
 */
class NotBlankValidator implements ConstraintValidator<NotBlank, CharSequence> {

    @Override
    public void initialize(NotBlank constraintAnnotation) {
        // Empty
    }

    @Override
    public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) {
        return charSequence != null && ((String) charSequence).trim().length() > 0;
    }
}

Spring CSR

  • Controller
    LoginController.java
package com.seliote.driftbottleendpoint.md.controller;

import com.seliote.driftbottleendpoint.md.controller.reqentity.LoginInfo;
import com.seliote.driftbottleendpoint.md.controller.respentity.AccessToken;
import com.seliote.driftbottleendpoint.md.service.LoginService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

/**
 * Login controller
 *
 * @author seliote
 * @version 1.0 2019-09-09
 */
@Controller
public class LoginController {
    private Logger mLogger = LogManager.getLogger();

    private LoginService mLoginService;

    @Autowired
    public void setLoginService(LoginService loginService) {
        mLoginService = loginService;
    }

    @RequestMapping(value = "login", method = {RequestMethod.POST})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    // Don't forget @RequestBody for parameter
    // @Valid to recursive validate
    public AccessToken requireAccessToken(@Validated @RequestBody LoginInfo loginInfo) {
        mLogger.info("Login info: " + loginInfo);
        String accessTokenString = mLoginService.generateAccessToken(loginInfo.getUserName());
        AccessToken accessToken = new AccessToken();
        accessToken.setAccessToken(accessTokenString);
        return accessToken;
    }
}

请求与响应实体

package com.seliote.driftbottleendpoint.md.controller.reqentity;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.seliote.driftbottleendpoint.md.validation.NotBlank;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

import javax.validation.constraints.Max;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

/**
 * Login info entity for LoginController
 *
 * @author seliote
 * @version 1.0 2019-09-10
 */
@SuppressWarnings("unused")
public class LoginInfo {
    @NotBlank
    // If this filed is a POJO and has its own validator, u can @Validated this filed to recursive validate
    private String mUserName;
    @NotBlank
    @Pattern(
            regexp = "^[a-zA-Z0-9!@#$%^&*]{6,16}$",
            flags = {Pattern.Flag.DOTALL}
    )
    private String mPassword;

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
    }

    @JsonProperty("user_name")
    public String getUserName() {
        return mUserName;
    }

    @JsonProperty("user_name")
    public void setUserName(String userName) {
        mUserName = userName;
    }

    @JsonProperty("password")
    public String getPassword() {
        return mPassword;
    }

    @JsonProperty("password")
    public void setPassword(String password) {
        mPassword = password;
    }
}
package com.seliote.driftbottleendpoint.md.controller.respentity;

import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

/**
 * Access token entity
 *
 * @author seliote
 * @version 1.0 2019-09-10
 */
public class AccessToken {
    private String mAccessToken;

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
    }

    @JsonProperty("access_token")
    public String getAccessToken() {
        return mAccessToken;
    }

    @JsonProperty("access_token")
    public void setAccessToken(String accessToken) {
        mAccessToken = accessToken;
    }
}
  • Service
    LoginService.java
package com.seliote.driftbottleendpoint.md.service;

import com.seliote.driftbottleendpoint.md.validation.NotBlank;
import org.springframework.validation.annotation.Validated;

import javax.validation.constraints.Pattern;

/**
 * Service for login
 *
 * @author seliote
 * @version 1.0 2019-09-13
 */
// Open MethodValidationPostProcessor validator for method execution
@Validated
public interface LoginService {
    /**
     * Generate access token for user
     *
     * @param userName User id for generate
     * @return The access token generated
     */
    // PbC programming
    @NotBlank
    String generateAccessToken(
            @NotBlank
            @Pattern(
                    regexp = "^[a-zA-Z0-9!@#$%^&*]{3,16}$",
                    flags = {Pattern.Flag.DOTALL}
            )
                    String userName);
}

实现

package com.seliote.driftbottleendpoint.md.service.Impl;

import com.seliote.driftbottleendpoint.md.entity.UserInfoEntity;
import com.seliote.driftbottleendpoint.md.repository.UserInfoRepository;
import com.seliote.driftbottleendpoint.md.service.LoginService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
import java.util.UUID;

/**
 * Service for login implement
 *
 * @author seliote
 * @version 1.0 2019-09-13
 */
@Service
public class LoginServiceImpl implements LoginService {
    private Logger mLogger = LogManager.getLogger();

    private UserInfoRepository mUserInfoRepository;

    @Autowired
    public void setUserInfoRepository(UserInfoRepository userInfoRepository) {
        mUserInfoRepository = userInfoRepository;
    }

    @Override
    public String generateAccessToken(String userName) {
        byte[] bytes = Base64.getEncoder().encode(userName.getBytes(StandardCharsets.UTF_8));
        String accessToken = new String(bytes, StandardCharsets.UTF_8);
        mLogger.info("Generate access token for: " + userName + ", " + accessToken);
        UserInfoEntity userInfoEntity = new UserInfoEntity();
        userInfoEntity.setRowId(UUID.randomUUID().toString());
        userInfoEntity.setUserName(userName);
        userInfoEntity.setCreateDate(Instant.now());
        mUserInfoRepository.save(userInfoEntity);
        return accessToken;
    }
}
  • Repository
    实体
package com.seliote.driftbottleendpoint.md.entity;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.time.Instant;

/**
 * user_info entity
 *
 * @author seliote
 * @version 1.0 2019-09-14
 */
@Entity
@Table(name = "user_info")
public class UserInfoEntity {
    private String mRowId;
    private String mUserName;
    private Instant mCreateDate;

    @Id
    @Column(name = "row_id")
    public String getRowId() {
        return mRowId;
    }

    public void setRowId(String rowId) {
        mRowId = rowId;
    }

    @Column(name = "user_name")
    public String getUserName() {
        return mUserName;
    }

    public void setUserName(String userName) {
        mUserName = userName;
    }

    @Column(name = "create_date")
    public Instant getCreateDate() {
        return mCreateDate;
    }

    public void setCreateDate(Instant createDate) {
        mCreateDate = createDate;
    }
}

UserInfoRepository.java

package com.seliote.driftbottleendpoint.md.repository;

import com.seliote.driftbottleendpoint.md.entity.UserInfoEntity;
import org.springframework.data.repository.CrudRepository;

/**
 * UserInfo Entity Spring Data JPA repository
 *
 * @author seliote
 * @version 1.0 2019-09-14
 */
public interface UserInfoRepository extends CrudRepository<UserInfoEntity, String> {
}
原文地址:https://www.cnblogs.com/seliote/p/10587468.html