银行产品秒杀系统——项目笔记
银行产品秒杀系统——项目笔记
-
-
- 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");