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