> 文档中心 > 银行产品秒杀系统——项目笔记

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

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

      • 1.如何让值为null的字段不返回给前端?
      • 2.Mybatis-plus怎么接收前端返回的一个数组,然后以JSON形式存入MySQL,并且可以从MySQL中正常取出这个JSON数据返回给前端?
      • 3.如何完全解决跨域问题?
      • 4.如何在Springboot项目中添加一个全局变量?
      • 5.如何往redis中快速插入大批量数据?
      • 6.如何在Springboot项目启动后直接执行一段代码?
      • 7.如何直接返回给前端一个合理的时间格式?
      • 8.Springboot跨域访问为什么会出现OPTION请求呢?
      • 9.登录时判断用户输入的是用户名还是手机号
      • 10.如何将MultipartFile类型的文件转换为InputStream流,并且把图片转换为Base64
      • 11.后端限流——Guava令牌桶
      • 12.Mybatis-plus在关联表与实体类时,必须要在自增主键上加上以下注解
      • 13.Druid连接池配置
      • 14.Sm4国密加密
      • 15.如何把fastJSON中的列表字段转换为Java实体类链表

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@Datapublic class GlobalVariable {    private Boolean variable = false;}
public class ScheduledTaskConfig{private final GlobalVariable globalVariable;@AutowiredScheduledTaskConfig(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项目启动后直接执行一段代码?

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

@Componentpublic 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; }}

注册该拦截器

@Configurationpublic 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.注册拦截器:

@Configurationpublic 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 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");