Bootstrap

银行产品秒杀系统——项目笔记

1.如何让值为null的字段不返回给前端?

字段为null的值同样会返回给前端,会让交互变得很奇怪,效率也变低。

在application.yml文件中添加以下配置:

jackson:
    default-property-inclusion: non_null

2.Mybatis-plus怎么接收前端返回的一个数组,然后以JSON形式存入MySQL,并且可以从MySQL中正常取出这个JSON数据返回给前端?

在Java实体类用于接收该数组的字段上添加@TableField(typeHandler = JacksonTypeHandler.class)注解,这一注解可以解决第问题,然后在整个实体类上添加@TableName(value = "deposit_good", autoResultMap = true)注解(主要是autoResultMap = true),可以解决第二个问题。

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value="deposit_result",autoResultMap = true)
public class DepositResult implements Serializable {
    @TableId(type = IdType.AUTO)
    private Integer id; //结果id
    private Integer userId;
    private Integer goodId;
    private Integer result;
    @TableField(typeHandler = JacksonTypeHandler.class)
    private List<Rule> reason;
    private String username;
    private String avatar;
    @TableField(exist = false)
    private String phone;
    private String reasonStr;
    private Date createTime;
}

3.如何完全解决跨域问题?

在项目中添加以下文件:

/**
 * 解决跨域问题,不能再在Controller上添加@CrossOrigin注解
 */

@Component
@WebFilter(urlPatterns = "/*", filterName = "CorsFilter")
public class CorsFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "*");
        response.setHeader("Access-Control-Allow-Headers", "*");
        chain.doFilter(req, res);
    }


    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void destroy() {
    }

}

重点是在响应头中添加以下四项:

 response.setHeader("Access-Control-Allow-Origin", "*");
 response.setHeader("Access-Control-Allow-Credentials", "true");
 response.setHeader("Access-Control-Allow-Methods", "*");
 response.setHeader("Access-Control-Allow-Headers", "*");

4.如何在Springboot项目中添加一个全局变量?

一个全局变量可以用来控制两个文件的执行顺序等,非常方便。因为Springboot项目中可以自动创建对象,并且这个对象默认是单例的,所以我们可以创建一个Bean来解决这个问题。

@Component
@Data
public class GlobalVariable {
    private Boolean variable = false;
}
public class ScheduledTaskConfig{
	private final GlobalVariable globalVariable;
	
	@Autowired
	ScheduledTaskConfig(GlobalVariable globalVariable){
    this.globalVariable = globalVariable;
  }
  
  globalVariable.setVariable(true); // 这样就成功的设置了全局变量
  ...
}

5.如何往redis中快速插入大批量数据?

在本次项目中有从MySQL中将大批量数据插入到redis中,仅用jedis遍历插入会非常慢,这里可以使用Pipeline来解决这个问题

Jedis jedis = new Jedis();
Pipeline pipeline = jedis.pipelined;

//下面开始进行管道操作
pipeline.hset(...);
pipeline.hset(...);

pipeline.sync(); // 这个必须要加,否则数据会有丢失!	
pipeline.close(); // 关闭管道

jedis.close();  // 回收redis资源

注意,在同一线程中,不能在使用pipeline的同时使用jedis操作redis,否则会报以下错误:

redis.clients.jedis.exceptions.JedisDataException: Cannot use Jedis when in Pipeline. Please use Pipeline or reset jedis state

6.如何在Springboot项目启动后直接执行一段代码?

这种需求可以用于数据的初始化

@Component
public class initialConfig implements ApplicationRunner {
	//do something
}

7.如何直接返回给前端一个合理的时间格式?

json默认格式化时间都是时间戳,这样看起来很不明显,前端可能还需要再度格式化。使用@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") 注解就可以解决。

public class ClientOrder extends PageSize implements Serializable {
    private Integer isLoans;
    private Integer orderId;
    private String goodName;
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date startTime;
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date endTime;
    private Integer status;
    private Integer userId;
    private Double totalPrice;
    private String name;
}

8.Springboot跨域访问为什么会出现OPTION请求呢?

请求方式有两大类,一类是简单的请求,一类是非简单的请求。简单的请求类似于GET、POST、HEAD等,非简单的请求如PUT、DELETE等。对于简单请求,浏览器直接发本次请求,而对于非简单请求,浏览器会先发预检请求,然后再发本次请求。而预检请求就是OPTION请求,OPTION请求只是检测作用,并没有具体数据。对于跨域访问设置拦截器时,需要对OPTION请求做相应的处理,否则对于非简单的请求会拦截访问。

因为OPTION请求中没有具体数据,所以在使用JWT进行登录认证时,那次OPTION请求会被拦截,导致程序出错。那么如何解决?

解决方案:在Springboot的JWT拦截器中全局捕获OPTION请求,并放行。

public class JWTInterceptor implements HandlerInterceptor{
		@Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (request.getMethod().equals("OPTIONS")){ //捕获OPTIONS请求,进行放行。
          return true;	
        }
}

注册该拦截器

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JWTInterceptor())
                .addPathPatterns("/**")         //其他接口token验证
                .excludePathPatterns("/client/user/loginPwd",
                                    "/client/user/register",
                                    "/client/user/loginSMS",
                                    "/client/user/getCode",
                                    "/admin/user/login");

    }

}

9.登录时判断用户输入的是用户名还是手机号

		/**
     * 判读输入的是手机号还是用户名
     *
     * @param pOrU 手机号或者用户名
     * @return
     */
    private Client getUserByPOrU(String pOrU) {
        QueryWrapper<Client> queryWrapper = new QueryWrapper<>();
        if (Pattern.matches("^1[3-9]\\d{9}$", pOrU)) {
            queryWrapper.eq("phone", pOrU);
        } else {
            queryWrapper.eq("username", pOrU);
        }
        return userMapper.selectOne(queryWrapper); //查询数据库中的用户信息
    }

使用正则表达式判断(Pattern.matches("^1[3-9]\\d{9}$", pOrU),其中porU就是手机号或者用户名的字符串

10.如何将MultipartFile类型的文件转换为InputStream流,并且把图片转换为Base64

public static String convertFileToBase64(MultipartFile file) {
    byte[] data = null;
    // 读取图片字节数组
    try {
        /*
          byte[] bytes = file.getBytes();
          InputStream in = new ByteArrayInputStream(bytes);
          该代码可以将MultipartFile类型的文件转换为InputStream流
         */
        byte[] bytes = file.getBytes();
        InputStream in = new ByteArrayInputStream(bytes);
        //System.out.println("文件大小(字节)="+in.available());
        data = new byte[in.available()];
        in.read(data);
        in.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
    // 对字节数组进行Base64编码,得到Base64编码的字符串
    return Base64Util.encode(data);
}

以下是Base64Util:

/**
 * Base64 工具类
 */
public class Base64Util {
    private static final char last2byte = (char) Integer.parseInt("00000011", 2);
    private static final char last4byte = (char) Integer.parseInt("00001111", 2);
    private static final char last6byte = (char) Integer.parseInt("00111111", 2);
    private static final char lead6byte = (char) Integer.parseInt("11111100", 2);
    private static final char lead4byte = (char) Integer.parseInt("11110000", 2);
    private static final char lead2byte = (char) Integer.parseInt("11000000", 2);
    private static final char[] encodeTable = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'};

    public Base64Util() {
    }

    public static String encode(byte[] from) {
        StringBuilder to = new StringBuilder((int) ((double) from.length * 1.34D) + 3);
        int num = 0;
        char currentByte = 0;

        int i;
        for (i = 0; i < from.length; ++i) {
            for (num %= 8; num < 8; num += 6) {
                switch (num) {
                    case 0:
                        currentByte = (char) (from[i] & lead6byte);
                        currentByte = (char) (currentByte >>> 2);
                    case 1:
                    case 3:
                    case 5:
                    default:
                        break;
                    case 2:
                        currentByte = (char) (from[i] & last6byte);
                        break;
                    case 4:
                        currentByte = (char) (from[i] & last4byte);
                        currentByte = (char) (currentByte << 2);
                        if (i + 1 < from.length) {
                            currentByte = (char) (currentByte | (from[i + 1] & lead2byte) >>> 6);
                        }
                        break;
                    case 6:
                        currentByte = (char) (from[i] & last2byte);
                        currentByte = (char) (currentByte << 4);
                        if (i + 1 < from.length) {
                            currentByte = (char) (currentByte | (from[i + 1] & lead4byte) >>> 4);
                        }
                }

                to.append(encodeTable[currentByte]);
            }
        }

        if (to.length() % 4 != 0) {
            for (i = 4 - to.length() % 4; i > 0; --i) {
                to.append("=");
            }
        }

        return to.toString();
    }
}

11.后端限流——Guava令牌桶

1.首先需要在pom.xml中添加依赖:

<dependency>
   <groupId>com.google.guava</groupId>
   <artifactId>guava</artifactId>
   <version>21.0</version>
</dependency>

2.设置拦截器:

public class RateInterceptor implements HandlerInterceptor {
    private final RateLimiter rateLimiter = RateLimiter.create(670);  //初始给670个令牌

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //创建令牌桶实例
        if (!rateLimiter.tryAcquire(5, TimeUnit.SECONDS)) {
            throw new SystemException("请求超时");
        }
        return true;
    }
}

3.注册拦截器:

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new RateInterceptor())
                .addPathPatterns("/client/good", "client/order");
    }

}

12.Mybatis-plus在关联表与实体类时,必须要在自增主键上加上以下注解

@TableId(type = IdType.AUTO)
private Integer id;

13.Druid连接池配置

druid:
    # 以下是druid的配置
    # 最大存活
    max-active: 20
    # 初始化连接个数
    initial-size: 1
    # 最小连接个数
    min-idle: 1
    # 最大等待时间
    max-wait: 10000
    # 间隔多久检测需要关闭空闲连接
    time-between-eviction-runs-millis: 60000
    # 连接在池中最小生存是时间
    min-evictable-idle-time-millis: 300000
    # 检测空闲连接是否有效
    keep-alive: true

14.Sm4国密加密

本次项目用到了这个工具类——Sm4Util

public class Sm4Util {
    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    private static final String ENCODING = "UTF-8";
    public static final String ALGORITHM_NAME = "SM4";
    // 加密算法/分组加密模式/分组填充方式
    // PKCS5Padding-以8个字节为一组进行分组加密
    // 定义分组加密模式使用:PKCS5Padding
    public static final String ALGORITHM_NAME_ECB_PADDING = "SM4/ECB/PKCS5Padding";
    // 128-32位16进制;256-64位16进制
    public static final int DEFAULT_KEY_SIZE = 128;

    /**
     * 生成ECB暗号
     *
     * @param algorithmName 算法名称
     * @param mode          模式
     * @param key
     * @return
     * @throws Exception
     * @explain ECB模式(电子密码本模式:Electronic codebook)
     */
    private static Cipher generateEcbCipher(String algorithmName, int mode, byte[] key) throws Exception {
        Cipher cipher = Cipher.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME);
        Key sm4Key = new SecretKeySpec(key, ALGORITHM_NAME);
        cipher.init(mode, sm4Key);
        return cipher;
    }


    /**
     * 自动生成密钥
     *
     * @return
     * @explain
     */
    public static byte[] generateKey() throws Exception {
        return generateKey(DEFAULT_KEY_SIZE);
    }

    /**
     * @param keySize
     * @return
     * @throws Exception
     * @explain
     */
    public static byte[] generateKey(int keySize) throws Exception {
        KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME);
        kg.init(keySize, new SecureRandom());
        return kg.generateKey().getEncoded();
    }

    /**
     * sm4解密
     *
     * @param hexKey     16进制密钥
     * @param cipherText 16进制的加密字符串(忽略大小写)
     * @return 解密后的字符串
     * @throws Exception
     * @explain 解密模式:采用ECB
     */
    public static String decryptEcb(String hexKey, String cipherText) throws Exception {
        // 用于接收解密后的字符串
        String decryptStr = "";
        // hexString-->byte[]
        byte[] keyData = ByteUtils.fromHexString(hexKey);
        // hexString-->byte[]
        byte[] cipherData = ByteUtils.fromHexString(cipherText);
        // 解密
        byte[] srcData = decrypt_Ecb_Padding(keyData, cipherData);
        // byte[]-->String
        decryptStr = new String(srcData, ENCODING);
        return decryptStr;
    }

    /**
     * 解密
     *
     * @param key
     * @param cipherText
     * @return
     * @throws Exception
     * @explain
     */
    public static byte[] decrypt_Ecb_Padding(byte[] key, byte[] cipherText) throws Exception {
        Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.DECRYPT_MODE, key);
        return cipher.doFinal(cipherText);
    }

    /**
     * 校验加密前后的字符串是否为同一数据
     *
     * @param hexKey     16进制密钥(忽略大小写)
     * @param cipherText 16进制加密后的字符串
     * @param paramStr   加密前的字符串
     * @return 是否为同一数据
     * @throws Exception
     * @explain
     */
    public static boolean verifyEcb(String hexKey, String cipherText, String paramStr) throws Exception {
        // 用于接收校验结果
        boolean flag = false;
        // hexString-->byte[]
        byte[] keyData = ByteUtils.fromHexString(hexKey);
        // 将16进制字符串转换成数组
        byte[] cipherData = ByteUtils.fromHexString(cipherText);
        // 解密
        byte[] decryptData = decrypt_Ecb_Padding(keyData, cipherData);
        // 将原字符串转换成byte[]
        byte[] srcData = paramStr.getBytes(ENCODING);
        // 判断2个数组是否一致
        flag = Arrays.equals(decryptData, srcData);
        return flag;
    }

    /**
     * sm4加密
     * @explain 加密模式:ECB
     *          密文长度不固定,会随着被加密字符串长度的变化而变化
     * @param hexKey
     *            16进制密钥(忽略大小写)
     * @param paramStr
     *            待加密字符串
     * @return 返回16进制的加密字符串
     * @throws Exception
     */
    public static String encryptEcb(String hexKey, String paramStr) throws Exception {
        String cipherText = "";
        // 16进制字符串-->byte[]
        byte[] keyData = ByteUtils.fromHexString(hexKey);
        // String-->byte[]
        byte[] srcData = paramStr.getBytes(ENCODING);
        // 加密后的数组
        byte[] cipherArray = encrypt_Ecb_Padding(keyData, srcData);
        // byte[]-->hexString
        cipherText = ByteUtils.toHexString(cipherArray);
        return cipherText;
    }

    /**
     * 加密模式之Ecb
     * @explain
     * @param key
     * @param data
     * @return
     * @throws Exception
     */
    public static byte[] encrypt_Ecb_Padding(byte[] key, byte[] data) throws Exception {
        Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.ENCRYPT_MODE, key);
        return cipher.doFinal(data);
    }

    /**
     * 常量池
     */
    public static final String[] POOL = new String[]{"0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"};


    /**
     * 生成字符串
     * @return 生成的32位长度的16进制字符串
     */
    public static String generateHexString(){
        StringBuilder sb = new StringBuilder();
        Random random = new Random();
        for (int i = 0; i < 32; i++) {
            sb.append(POOL[random.nextInt(POOL.length)]);
        }
        return sb.toString();
    }

}

15.如何把fastJSON中的列表字段转换为Java实体类链表

例如一个json字符串中有一个key为reason的value值是一个列表:

{
  'reason':[
    {
      'id':1,
      'name':'rule1',
      ...
    },
    {
      'id':2,
      'name':'rule2',
      ...
    },
    {
      'id':3,
      'name':'rule3',
      ...
    }
  ]
}

其中列表的内容对应的是Java的Rule实体类:

public class Rule{
  private Integer id;
  private String name;
  ...
}

那么如何将这个reason字段转换为List<Rule> list 呢?以下为解决方案:

List<Rule> reason = json.getJSONArray("reason").toJavaList(Rule.class);

另外,fastJSON在获取可以直接转为String、Integer、Date等类型,不用强制类型转换

Integer goodId = json.getInteger("goodId");
String result = json.getString("result");
Date createTime = json.getDate("createTime");

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;