An interview question (current limiting, idempotent key)

Topic introduction

Write a deduction HTTP interface of the banking system. The interface path is / bank / Account / cost JSON, the input parameters are three parameters
User account String userAccount, / / deduction amount BigDecimal cost, / / idempotent Key String requestKey.
Database table name user_account. It is assumed that there are only three fields: id, userAccount(varchar 32) and amount(bigint).
Any method name, complete the following logic:
1. Complete the deduction logic and complete the three-layer pseudo code of SpringMvc Controller, Manager and Mybatis Mapper.
2. The interface can only carry 300QPS, please realize simple current limiting.
3. There are idempotent key s in the input parameter. Please implement simple idempotent logic based on Nosql(Redis, etc.) API.
4. Add the log content at the key position you think with your accustomed log framework API.
5. Where you think you need to throw exceptions, don't save your try catch and let me see your code robustness.
(system concurrency shall be considered for the above implementation code) additional: reflect the design pattern, encapsulation polymorphism and other code styles as much as possible.

Reference code cloud address: https://gitee.com/huqidake/spring-boot-demo

critical code

Current limiting using token bucket

The simple token bucket algorithm enables a simple timing task to put tokens into the bucket at a certain rate. The size of the bucket is fixed. If there is still a place in the bucket, put the token into the bucket, otherwise discard the token. Every time a request comes, it will try to obtain the token. If it is obtained, it will be reduced by one, If there is no token in the bucket, it means that the token is not obtained and the token is put into the next scheduled task The capacity of the bucket and the rate of generating tokens can be adjusted freely according to the business needs
At present, the token bucket provided by guava can be used for current limiting. In addition to ordinary token acquisition current limiting, it can also be controlled. If no token is obtained for iterative operation, a first come first served operation has been controlled
In addition, you can also use the lua prevention script in redis to limit the flow. This method uses the principle of redis single thread and can reliably resist high concurrent requests, At the same time, it can also avoid the problem of the storage location of the ordinary token bucket (in this case, the token bucket is placed in the gateway for current limiting interception [such as zuul, getway, etc.) , because most popular systems are microservices, a zuul may have clusters. If the paintings are stored directly on zuul, they may not play the role of current limiting. At this time, you can also store the token bucket in a single redis or cluster redis[redis clusters do not store duplicate data and will be scattered in 10650 hash slots. I will explain the specific operations later in the article], To resist high concurrency and realize current limiting operation)

package com.huqi.limiting;

import com.huqi.log.LoggerUtil;
import com.huqi.thread.ScheduledExecutor;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Hu Qi
 * @date 2021-07-29 Thursday 23:00
 */
public class TokenBucket {
    private final static Logger LOGGER = LoggerFactory.getLogger(TokenBucket.class);

    private static final String KEY = "KEY";

    /**
     * Maximum 300 tokens stored
     */
    private static final int SIZE = 3;
    private final static List<String> bucket = new ArrayList<>(SIZE);

    static {
        setAndStartRefresh();
        for (int i = 0; i < SIZE; i++) {
            bucket.add(KEY);
        }
    }

    private static void setAndStartRefresh() {
        ScheduledExecutor.executorService.scheduleAtFixedRate(() -> {
            setToken();
            // LoggerUtil.info(LOGGER, "token scheduled task is executing!!!!");
        }, 1, 1000, TimeUnit.MILLISECONDS);
    }

    public static void init() {
        LoggerUtil.info(LOGGER, "TokenBucket init success");
    }

    /**
     * Put token
     */
    private static void setToken() {
        if (bucket.size() < SIZE) {
            bucket.add(KEY + System.currentTimeMillis() + UUID.randomUUID());
            LoggerUtil.info(LOGGER, "Put a token");
            LoggerUtil.warn(LOGGER, "");
        }
    }

    /**
     * Attempt to get token
     * @return
     */
    public static boolean getToken() {
        String remove = null;
        Lock lock = new ReentrantLock();
        lock.lock();
        try {
            remove = bucket.remove(bucket.size() - 1);
        }catch (Exception e) {
            LoggerUtil.error(e, LOGGER, "Failed to get token..");
        }finally {
            lock.unlock();
        }
        return StringUtils.isNotBlank(remove);
    }
}

Usage: intercept all requests in the controller aspect. First, get the token. If you get it, you can go to the next step, otherwise it will be intercepted

package com.huqi.aspect;

import com.huqi.json.JsonUtil;
import com.huqi.limiting.TokenBucket;
import com.huqi.log.LoggerUtil;
import com.huqi.result.ResultUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * Current limiting treatment section
 * @author Hu Qi
 * @date 2021-07-24 Saturday 21:37
 */
@Aspect
@Component
public class ControllerAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(ControllerAspect.class);

    /**
     * Interface requiring current limiting
     * Match CN All methods under the controller package
     */
    @Pointcut("execution(public * com.huqi.controller.HttpController.deductions(..))")
    public void check(){}

    /**
     * You need to surround the interface for printing logs, mainly the controller
     */
    @Pointcut("execution(public * com.huqi.controller.*.*(..))")
    public void log(){}

    /**
     * Surround print log
     *             //Get class name
     *             String clazzName = joinPoint.getTarget().getClass().getSimpleName();
     *             //Get method name
     *             String methodName = joinPoint.getSignature().getName();
     *             //Get parameter list
     *             Object[] args = joinPoint.getArgs();
     *
     */
    @Around("log()")
    public Object round(ProceedingJoinPoint proceedingJoinPoint){
        String method = new StringBuilder(proceedingJoinPoint.getSignature().getDeclaringTypeName())
                .append(" -> ")
                .append(proceedingJoinPoint.getSignature().getName())
                .toString();
        if (proceedingJoinPoint.getArgs().length != 0) {
            LoggerUtil.info(LOGGER, "{0}  args = {1}",
                    method, JsonUtil.toJson(proceedingJoinPoint.getArgs()));
        }
        try {
            Object proceed = proceedingJoinPoint.proceed();
            LoggerUtil.info(LOGGER, "{0}  result = {1}", method, JsonUtil.toJson(proceed));
            return proceed;
        } catch (Throwable throwable) {
            LoggerUtil.error(throwable, LOGGER, "round Program execution exception ....");
            return ResultUtil.fail(throwable.getMessage());
        }
    }

    /**
     *
     * @param joinPoint
     */
    @Before(value = "check()")
    public void before(JoinPoint joinPoint) throws IllegalAccessException {
        // Try to get a token. If you get it, release it. If you don't get it, intercept it
        boolean token = TokenBucket.getToken();
        if (!token) {
            LoggerUtil.error(LOGGER, "Token acquisition failed");
            throw new IllegalAccessException("It has been intercepted by current restriction, Please try again later!!!!!");
        }
        LoggerUtil.info(LOGGER, "ControllerAspect Token obtained method = {0}, args = {1}",
                joinPoint.getSignature().getName(), JsonUtil.toJson(joinPoint.getArgs()));
    }
}

Idempotent check

Before accessing the deduction interface, the front end first obtains an idempotent key by querying the idempotent key interface

    @RequestMapping(value = "/getKey.json", method = {RequestMethod.POST, RequestMethod.GET})
    @ResponseBody
    public WebResult getKey() {
        // Generate a UUID as a key
        return ResultUtil.success(new StringBuilder(UUID.randomUUID().toString())
                .append("-")
                .append(DateUtil.getDateStringFormat(new Date(), "yyyy-MM-dd-HH-mm-ss"))
                .toString());
    }

Then, when accessing the deduction interface, the idempotent key is passed in. After receiving the input parameter, the back-end interface will store the idempotent key in redis and set an expiration time [the expiration time can be set according to the actual situation]. When a certain time class and the same input parameter request, it will first verify whether the key exists in redis. If it already exists, It is not allowed to operate downward. [there can also be an operation to check whether the key is legal to prevent the user from randomly entering the key, and the key generation rules can be defined freely]. This method can check simple idempotence

Or use another operation to check the idempotent. Before accessing the interface, first obtain an idempotent key, store the idempotent key in rdis, and set an expiration time. When the current end accesses the interface with this parameter, we will check whether the key exists in redis. If it exists, we can further operate and delete the key immediately, When the next same request comes, the idempotent key will not be found, and the next operation is not allowed. This method can avoid the problem that users use a duplicate key, and can also prevent users from generating keys themselves. However, the deletion of keys must be timely, otherwise concurrency will still occur. Transactions and locking can be considered

    /**
     * Check idempotent key
     * @return
     */
    private void checkKey(String key) throws IllegalAccessException {
        // Save this key in redis
        boolean exit = CacheUtil.setNX(key, key, 1);
        if (!exit) {
            throw new IllegalAccessException("key Already exists,Idempotent check failed...");
        }
    }

Easy to use log framework

slfj + logback is used here for a simple encapsulation

package com.huqi.log;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.MessageFormat;

/**
 * Logging tool class
 * @author Hu Qi
 * @date 2021-07-24 Saturday 16:03
 */
public class LoggerUtil {
    private final static Logger LOGGER = LoggerFactory.getLogger(LoggerUtil.class);

    public static void info(Logger logger, String template, String... args) {
        if (logger.isInfoEnabled()) {
            logger.info(joint(template, args));
        }
    }

    public static void warn(Logger logger, String template, String... args) {
        logger.warn(joint(template, args));
    }

    public static void warn(Throwable throwable, Logger logger, String template, String... args) {
        logger.warn(joint(template, args), throwable);
    }

    public static void error(Logger logger, String template, String... args) {
        logger.error(joint(template, args));
    }

    public static void error(Throwable throwable, Logger logger, String template, String... args) {
        logger.error(joint(template, args), throwable);
    }

    /**
     * Parse the placeholder and return the spliced string
     *
     *
     * My own implementation
     *
     *     // {
     *     private static final char PREFIX = 123;
     *     // }
     *     private static final char SUFFIX = 125;
     *
     *     private static Set numSet = new HashSet(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9));
     *
     * private static String joint(String msg, String... msgs) {
     *         if (StringUtils.isBlank(msg) || msg == null || msgs.length == 0) {
     *             return msg;
     *         }
     *         final char[] chars = msg.toCharArray();
     *         StringBuilder stringBuilder = new StringBuilder();
     *         try {
     *             for (int i = 0; i < chars.length;) {
     *                 char c = chars[i];
     *                 if (c == PREFIX && chars[i+2] == SUFFIX) {
     *                     Integer index = Integer.valueOf(Character.toString(chars[i + 1]));
     *                     boolean contains = numSet.contains(index);
     *                     if (contains) {
     *                         stringBuilder.append(msgs[index]);
     *                     }
     *                     i = i+2;
     *                     continue;
     *                 }
     *                 if (c == SUFFIX) {
     *                     i++;
     *                     continue;
     *                 }
     *                 stringBuilder.append(c);
     *                 i++;
     *             }
     *         }catch (Exception e) {
     *             LOGGER.error("LoggerUtils joint error");
     *             return msg;
     *         }
     *         return stringBuilder.toString();
     *     }
     *
     *
     * @param msg  Template parameter example
     *                          test1 = {0}, test2 = {1}
     *             The filled parameter starts from 0, and there can be no spaces between numbers and curly braces
     * @param args Parameters to be filled,
     * @return
     */
    private static String joint(String msg, String... args) {
        try {
            return MessageFormat.format(msg, args);
        } catch (Exception e) {
            LOGGER.error("Log printing exception", e);
        }
        return null;
    }

}

Configuration of xml file

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
    <!--Define the storage address of the log file LogBack Relative paths are used in the configuration of-->
    <property name="LOG_HOME" value="/home" />
    <!-- console output  -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--Format output:%d Indicates the date,%thread Represents the thread name,%-5level: The level is displayed 5 characters wide from the left%msg: Log messages,%n Is a newline character-->
<!--            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%logger{50}] - %msg%n</pattern>
        </encoder>
    </appender>
    <!-- Generate log files on a daily basis -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--The file name of the log file output-->
            <FileNamePattern>${LOG_HOME}/TestWeb.log.%d{yyyy-MM-dd}.log</FileNamePattern>
            <!--Log file retention days-->
            <MaxHistory>30</MaxHistory>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--Format output:%d Indicates the date,%thread Represents the thread name,%-5level: The level is displayed 5 characters wide from the left%msg: Log messages,%n Is a newline character-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
        <!--Maximum size of log file-->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>10MB</MaxFileSize>
        </triggeringPolicy>
    </appender>

    <!-- Log output level -->
    <root level="INFO">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

Use example

        LoggerUtil.info(LOGGER, "ControllerAspect Token obtained method = {0}, args = {1}",
                joinPoint.getSignature().getName(), JsonUtil.toJson(joinPoint.getArgs()));

Simple redis function implementation

Because I don't want to introduce redis, I wrote a simple redis implementation, which can realize simple redis artifact, get set setNx, etc., and wrote a simple elimination strategy,
Common redis elimination strategies include the following:

  1. Active deletion

Actively select 20 keys for verification. If a key has expired, delete it. If 1 / 4 of the expired keys account for it, repeat this step again
Delete the recently expired key; Do not delete the key, and discard the latest key; Delete the least used key, that is, delete the hotspot key; Delete the elimination strategy such as the first key. The specific deletion strategy will be written later

  1. Inert deletion

When you need to put a key into redis, you will first check whether the redis has expired. If it has expired, you will delete the key and return a null value Why hesitate again

package com.huqi.cache;

import com.huqi.date.DateUtil;
import com.huqi.json.JsonUtil;
import com.huqi.log.LoggerUtil;
import com.huqi.thread.ScheduledExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;

import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * Simulate redis operation, too lazy to introduce redis
 * @author Hu Qi
 * @date 2021-07-24 Saturday 20:21
 */
public class CacheUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(CacheUtil.class);

    /**
     * 2000 by default
     */
    private static final int SIZE = 2000;
    private static final double RATIO = 0.95;

    /**
     * Storage principal
     */
    private static Map<String, Object> CACHE = new HashMap<>(SIZE);
    /**
     * Storage expiration time
     */
    private static Map<String, LocalDateTime> OVERDUE_TIME = new HashMap<>(SIZE);

    static {
        clearScheduled();
    }

    public static void init() {
        LoggerUtil.info(LOGGER, "CacheUtil init success");
    }

    /**
     * Store one
     * @param key
     * @return
     */
    public static boolean set(String key, Object value) {
        try {
            clearData();
            clearByKey(key);
            CACHE.put(key, value);
        }catch (Exception e) {
            LoggerUtil.error(e, LOGGER, "CacheUtil set error... key = {0} value = {1}"
                    , key, JsonUtil.toJson(value));
            return false;
        }
        return true;
    }

    /**
     * Store a key with expiration time, and some expiration strategies need to be considered during the period
     * @param key
     * @param value
     * @param sec  Expiration time, seconds
     * @return
     */
    public static boolean set(String key, Object value, long sec) {
        try {
            clearData();
            clearByKey(key);
            CACHE.put(key, value);
            OVERDUE_TIME.put(key, LocalDateTime.now().plusSeconds(sec));
            clearData();
        }catch (Exception e) {
            LoggerUtil.error(e, LOGGER, "CacheUtil set error... key = {0} value = {1}, sec = {2}"
                    , key, JsonUtil.toJson(value), String.valueOf(sec));
            return false;
        }
        return true;
    }

    /**
     * take
     * @param key
     * @return
     */
    public static Object get(String key) {
        try {
            clearData();
            clearByKey(key);
            return CACHE.get(key);
        }catch (Exception e) {
            LoggerUtil.error(e, LOGGER, "CacheUtil get error... key = {0}", key);
            return null;
        }
    }

    /**
     * setnx operation
     * @param key
     * @param object
     * @return
     */
    public static boolean setNX(String key, Object object) {
        clearData();
        clearByKey(key);
        boolean ex = CACHE.containsKey(key);
        if (ex) {
            return false;
        }
        return set(key, object);
    }

    /**
     * setnx operation
     * @param key
     * @param object
     * @param sec
     * @return
     */
    public static boolean setNX(String key, Object object, long sec) {
        clearData();
        clearByKey(key);
        boolean ex = CACHE.containsKey(key);
        if (ex) {
            return false;
        }
        return set(key, object, sec);
    }

    /**
     * Delete data
     * @param key
     * @return
     */
    public static boolean remove(String key) {
        try {
            clearData();
            CACHE.remove(key);
            clearByKey(key);
        }catch (Exception e) {
            LoggerUtil.error(e, LOGGER, "CacheUtil set remove... key = {0}", key);
            return false;
        }
        return true;
    }

    /**
     * Number of acquired data
     * @return
     */
    public static int keys() {
        clearData();
        return CACHE.size();
    }

    /**
     * Check whether a key has expired. If it has expired, delete it
     * @param key
     */
    private static void clearByKey(String key) {
        // Check whether the key has expired
        LocalDateTime time = OVERDUE_TIME.get(key);
        boolean exceeded = DateUtil.timeExceeded(time);
        if (exceeded) {
            OVERDUE_TIME.remove(key);
            CACHE.remove(key);
            LoggerUtil.info(LOGGER, "cache wipe out key = {0}", key);
        }
    }

    /**
     * Check whether all key s are deleted. You should use scheduled tasks to process them, or actively clear data when there is too much cache
     * ,Start a thread separately to prevent blocking the current thread
     */
    private static void clear() {
        new Thread(() -> {
            Set<String> keySet = CACHE.keySet();
            if (!CollectionUtils.isEmpty(keySet)) {
                keySet.forEach(key -> clearByKey(key));
            }
        }).start();
    }

    /**
     * When the data is greater than 95% of the preset value, the expired key can be cleared. You can also write an algorithm to clear the recently expired key, a random algorithm to clear, and discard the latest value
     */
    private static void clearData() {
        if (CACHE.size() > SIZE * RATIO) {
            clear();
        }
    }

    /**
     * Take 20 at random and judge the expired data. If more than 1 / 4 of the data is expired, repeat this method to call, which is a recursive call
     * Actively delete expired key s and randomly take 20 data
     */
    private static void clearEliminate() {
        int num = 0;
        String[] strings = CACHE.keySet().toArray(new String[CACHE.size()]);
        if (strings.length == 0) {
            return;
        }
        for (int i = 0; i < 20; i++) {
            String key = strings[new SecureRandom().nextInt(strings.length)];
            boolean timeExceeded = DateUtil.timeExceeded(OVERDUE_TIME.get(key));
            if (timeExceeded) {
                OVERDUE_TIME.remove(key);
                CACHE.remove(key);
                LoggerUtil.info(LOGGER, "cache wipe out key = {0}", key);
                num++;
            }
        }
        if (num >= 20/4) {
            clearEliminate();
        }
    }

    /**
     * 10 Perform a check purge once a minute
     */
    private static void clearScheduled() {
        ScheduledExecutor.executorService.scheduleAtFixedRate(() -> {
            clearEliminate();
            clearData();
        }, 1, 1000, TimeUnit.MILLISECONDS);
    }
}

bean conversion tool

It mainly uses the bean utils provided by the spring framework for conversion

package com.huqi.beanutils;

import com.huqi.json.JsonUtil;
import com.huqi.log.LoggerUtil;
import org.apache.commons.beanutils.BeanUtilsBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.sql.Timestamp;
import java.util.*;

/**
 * Type conversion tool class
 * @author Hu Qi
 * @date 2021-07-24 Saturday 15:47
 */
public class ModelUtil {
    private final static Logger LOGGER = LoggerFactory.getLogger(ModelUtil.class);

    public static <T> T dtoToModel(Object dto, Class<T> modelClazz) {
        T t = null;
        if (dto == null) {
            return null;
        }
        try {
            dto = deepCopyBean(dto);
            t = modelClazz.newInstance();
            BeanUtils.copyProperties(dto, t);
        }catch (Exception e) {
            LoggerUtil.error(e, LOGGER, "ModelUtil dtoToModel Object copy exception Object = {0}", JsonUtil.toJson(dto));
            return null;
        }
        return t;
    }

    /**
     *  Deep copy, serialization is not used
     * @param source
     * @param <T>
     * @return
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    public static <T> T deepCopyBean(T source) {
        if (source == null) {
            return null;
        }
        try {
            if (source instanceof Collection) {
                return (T) deepCopyCollection((Collection) source);
            }
            if (source.getClass().isArray()) {
                return (T) deepCopyArray(source);
            }
            if (source instanceof Map) {
                return (T) deepCopyMap((Map) source);
            }
            if (source instanceof Date) {
                return (T) new Date(((Date) source).getTime());
            }
            if (source instanceof Timestamp) {
                return (T) new Timestamp(((Timestamp) source).getTime());
            }
            // The basic type returns the original value directly
            if (source.getClass().isPrimitive() || source instanceof String || source instanceof Boolean
                    || Number.class.isAssignableFrom(source.getClass())) {
                return source;
            }
            if (source instanceof StringBuilder) {
                return (T) new StringBuilder(source.toString());
            }
            if (source instanceof StringBuffer) {
                return (T) new StringBuffer(source.toString());
            }
            Object dest = source.getClass().newInstance();
            BeanUtilsBean bean = BeanUtilsBean.getInstance();
            PropertyDescriptor[] origDescriptors = bean.getPropertyUtils().getPropertyDescriptors(source);
            for (int i = 0; i < origDescriptors.length; i++) {
                String name = origDescriptors[i].getName();
                if ("class".equals(name)) {
                    continue;
                }

                if (bean.getPropertyUtils().isReadable(source, name)
                        && bean.getPropertyUtils().isWriteable(dest, name)) {
                    try {
                        Object value = deepCopyBean(bean.getPropertyUtils().getSimpleProperty(source, name));
                        bean.getPropertyUtils().setSimpleProperty(dest, name, value);
                    } catch (NoSuchMethodException e) {
                        LOGGER.error("deepCopyBean", e);
                    }
                }
            }
            return (T) dest;
        } catch (Exception e) {
            LOGGER.error("deepCopyBean", e);
            return null;
        }
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    private static Collection deepCopyCollection(Collection source)
            throws InstantiationException, IllegalAccessException {
        Collection dest = source.getClass().newInstance();
        for (Object o : source) {
            dest.add(deepCopyBean(o));
        }
        return dest;
    }

    private static Object deepCopyArray(Object source) throws InstantiationException, IllegalAccessException,
            ArrayIndexOutOfBoundsException, IllegalArgumentException {
        int length = Array.getLength(source);
        Object dest = Array.newInstance(source.getClass().getComponentType(), length);
        if (length == 0) {
            return dest;
        }
        for (int i = 0; i < length; i++) {
            Array.set(dest, i, deepCopyBean(Array.get(source, i)));
        }
        return dest;
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private static Map deepCopyMap(Map source) throws InstantiationException, IllegalAccessException {
        Map dest = source.getClass().newInstance();
        for (Object o : source.entrySet()) {
            Map.Entry e = (Map.Entry) o;
            dest.put(deepCopyBean(e.getKey()), deepCopyBean(e.getValue()));
        }
        return dest;
    }
}

Date tool class

package com.huqi.date;

import com.huqi.log.LoggerUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;

/**
 * Time tool class
 * @author Hu Qi
 * @date 2021-07-24 Saturday 15:59
 */
public class DateUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(DateUtil.class);

    /**
     * Default date resolution format
     */
    private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";

    /**
     * Gets the current time string
     * @return
     */
    public static String getNowDateString() {
        final SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
        final String date = dateFormat.format(new Date());
        LOGGER.info("DateUtil getNowDateString = " + date);
        return date;
    }

    /**
     * Convert the specified time to standard format
     * @param date
     * @return
     */
    public static String getDateString(Date date) {
        final SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
        String dateString = null;
        try {
           dateString = dateFormat.format(date);
        }catch (Exception e) {
            LoggerUtil.error(e, LOGGER, "DateUtil getDateString Conversion exception");
            return null;
        }
        LOGGER.info("DateUtil dateString = " + dateString);
        return dateString;
    }

    /**
     * Gets the specified format String of the specified date
     * @param date
     * @param formatString
     * @return
     */
    public static String getDateStringFormat(Date date, String formatString) {
        String dateString = null;
        try {
            SimpleDateFormat dateFormat = new SimpleDateFormat(formatString);
            dateString = dateFormat.format(date);
        }catch (Exception e) {
            LoggerUtil.error(e, LOGGER, "DateUtil getDateStringFormat Conversion exception");
            return null;
        }
        LOGGER.info("DateUtil dateString = " + dateString);
        return dateString;
    }

    /**
     * Whether the incoming time is greater than the current time, that is, it has not expired
     * @param time
     * @return
     */
    public static boolean timeExceeded(LocalDateTime time) {
        if (time == null)  {
            return false;
        }
        LocalDateTime now = LocalDateTime.now();
        return now.isAfter(time);
    }
}

json tool class

package com.huqi.json;


import com.alibaba.fastjson.JSON;
import com.huqi.log.LoggerUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * json Tool class
 * @author Hu Qi
 * @date 2021-07-24 Saturday 15:56
 */
public class JsonUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(JsonUtil.class);

    /**
     * Use fastJson to convert objects to strings
     * @param object
     * @return
     */
    public static String toJson(Object object) {
        if (object == null) {
            return null;
        }
        try {
         return JSON.toJSONString(object);
        }catch (Exception e) {
            LoggerUtil.error(e, LOGGER, "Convert object to Json Format failed");
            return null;
        }
    }

}

Return value

package com.huqi.result;

import com.huqi.httpmianshidemo.model.common.enums.ResultCode;
import com.huqi.httpmianshidemo.model.common.WebResult;

/**
 * Return value tool class
 * @author Hu Qi
 * @date 2021-07-24 Saturday 22:55
 */
public class ResultUtil {

    public static WebResult success() {
        WebResult webResult = new WebResult();
        webResult.setSuccess(true);
        webResult.setData("ok");
        webResult.setCode(ResultCode.SUCCESS.getCode());
        return webResult;
    }

    public static <T> WebResult<T> success(T data) {
        WebResult<T> webResult = new WebResult();
        webResult.setSuccess(true);
        webResult.setCode(ResultCode.SUCCESS.getCode());
        webResult.setData(data);
        return webResult;
    }

    public static WebResult fail() {
        WebResult webResult = new WebResult();
        webResult.setSuccess(false);
        webResult.setCode(ResultCode.FAIL.getCode());
        webResult.setErrorMsg(ResultCode.FAIL.getDesc());
        return webResult;
    }

    public static WebResult fail(ResultCode resultCode) {
        WebResult webResult = new WebResult();
        webResult.setSuccess(false);
        webResult.setCode(resultCode.getCode());
        webResult.setErrorMsg(resultCode.getDesc());
        return webResult;
    }

    public static WebResult fail(String errorMsg) {
        WebResult webResult = new WebResult();
        webResult.setSuccess(false);
        webResult.setCode(ResultCode.FAIL.getCode());
        webResult.setErrorMsg(errorMsg);
        return webResult;
    }

    public static WebResult fail(ResultCode resultCode, String errorMsg) {
        WebResult webResult = new WebResult();
        webResult.setSuccess(false);
        webResult.setCode(resultCode.getCode());
        webResult.setErrorMsg(resultCode.getDesc() + errorMsg);
        return webResult;
    }

}
package com.huqi.httpmianshidemo.model.common;

import lombok.Data;

/**
 * Return value object
 * @author Hu Qi
 * @date 2021-07-24 Saturday 22:52
 */
@Data
public class WebResult<T> {

    private int code;

    private T data;

    private String errorMsg;

    private boolean success;
}

Added by icon4tech on Mon, 27 Dec 2021 07:41:56 +0200