一、创建一个空的SpringBoot项目(就是依赖选个Web就行了)2023-5-23
二、导包
<dependencies>
<!--Web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--阿里的json转换jar包-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.60</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--mybatis-plus-->
<!--mybatis-plus是自己开发的,并非官方的-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!--非官方: mybatis-spring-boot-starter-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!--Druid-->
<!-- <dependency>-->
<!-- <groupId>com.baomidou</groupId>-->
<!-- <artifactId>mybatis-plus-boot-starter</artifactId>-->
<!-- <version>3.5.1</version>-->
<!-- </dependency>-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.16</version>
</dependency>
<!--Druid会使用log4j的日志记录,使用导入依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.11</version>
</dependency>
<!--JDBC-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--MySQL-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--mybatis-plus-generator 生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.2</version>
</dependency>
<!-- freemarker,作为代码生成器mapper文件的模板引擎使用(当然也可以使用velocity,二选一即可) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!--thymeleaf-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-thymeleaf</artifactId>-->
<!-- </dependency>-->
<!--jcakson-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
三、写配置文件(最好,多创建几个application-{}.yaml文件)
application.yaml
spring:
profiles:
active: release
application-dev.yaml
spring:
# thymeleaf 的配置基本上可以抛弃了,使用前段框架开发前段就可以了
#thymeleaf:
#cache: false
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=true&allowMultiQueries=true
# Oracle配置
#url: jdbc:oracle:thin:@127.0.0.1:1521:test
#username: root
#password: 123456
#driver-class-name: oracle.jdbc.driver.OracleDriver
# 配置druid数据源
type: com.alibaba.druid.pool.DruidDataSource
# druid 配置
druid:
# 打开我们的内置监控页面,打开后我们才可以有相应的web界面
stat-view-servlet:
enabled: true
url-pattern: /druid/*
login-username: user
login-password: 123456
# 打开我们的内置监控功能,打开之后我们才可以对我们的操作进行统计
filter:
stat:
enabled: true
slow-sql-millis: 2000
# 配置防火墙,防止sql语句注入,我们可以设置我们想要那些语句可以执行,哪些不行
wall:
enabled: true
config:
delete-allow: true
# select-all-column-allow: false,如果我们设置了拒绝查询,那么我们查询时就会报错,返回一个whitePage
# selelct-allow: false
# 打开我们的Web关联监控配置,即我们对某些路径进行精确统计
web-stat-filter:
enabled: true
# 初始化时建立的物理连接数。初始化发生在显式调用init方法,或者第一次getConnection时.
initial-size: 5
# 连接池最大物理连接数量。
max-active: 50
# 连接池最小物理连接数量。
min-idle: 5
# 获取连接时最大等待时间,单位为毫秒。
# 配置之后,缺省启用公平锁,并发效率会有所下降,若需要可以通过配置useUnfairLock属性为true使用非公平锁。
max-wait: 6000
# 是否缓存preparedStatement,也就是PSCache。
# PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
pool-prepared-statements: true
# 要启用PSCache,其值必须大于0,当大于0时,poolPreparedStatements自动触发修改为true。
# 在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100。
max-pool-prepared-statement-per-connection-size: 20
# 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。
# 如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
validation-query: select 1 from dual
# 检测连接是否有效的超时时间,单位为秒。
# 底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法。
# validation-query-timeout: 30
# 有两个含义:
# 1) Destroy线程会检测连接的间隔时间,若连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
# 2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明。
time-between-eviction-runs-millis: 60000
# 连接保持空闲而不被驱逐的最长时间。
min-evictable-idle-time-millis: 300000
# 建议配置为true,不影响性能,并且保证安全性。
# 申请连接的时候检测,若空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
test-while-idle: true
# 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
test-on-borrow: false
# 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
test-on-return: false
# 类型是字符串,通过别名的方式配置扩展的拦截器插件,常用的拦截器插件有:
# 监控统计用的filter:stat,日志用的filter:log4j,防御sql注入攻击的filter:wall,三个同时配置的化,用逗号隔开。
# 注意,Druid中的filter-class-names配置项是不起作用的,必须采用filters配置项才可以。
filters: stat,wall,log4j2
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录。
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 合并多个DruidDataSource的监控数据
use-global-data-source-stat: true
# 时间日期格式化 spring默认提交格式 yyyy/MM/dd
mvc:
format:
date: yyyy-MM-dd HH:mm:ss
servlet:
multipart:
enabled: true
max-file-size: 30MB
max-request-size: 30MB
# redis
redis:
host: ip地址 # 默认127.0.0.1
port: 6379 # 默认6379
password: 密码
# 整合mybatis
mybatis:
mapper-locations:
- classpath:com/zhao/mapper/*.xml
# 配置日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 开启驼峰命名
map-underscore-to-camel-case: true
# 配置逻辑删除
global-config:
db-config:
logic-delete-value: 1 # 1 为逻辑删除后的字段
logic-not-delete-value: 0 # 0 为没有逻辑删除的字段
logic-delete-field: deleted #对应实体类的字段 写了这个在实体类中就不需要写注解了
application-release.yaml
spring:
thymeleaf:
cache: false
datasource:
username: 账号
password: 密码
# 假如时区报错了加 &serverTimezone=UTC
url: jdbc:mysql://ip地址:3306/school_vote?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=true&allowMultiQueries=true
driver-class-name: com.mysql.cj.jdbc.Driver
# 配置druid数据源
type: com.alibaba.druid.pool.DruidDataSource
# 配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
# 如果允许时报错 java.lang.ClassNotFoundExcption :org.apache.log4j.Priority
# 则导入 log4j 依赖即可,Maven 地址: https://mvnrepository.com/artifact/log4j/log4j
druid:
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
# 时间日期格式化 spring默认提交格式 yyyy/MM/dd
mvc:
format:
date: yyyy-MM-dd HH:mm:ss
servlet:
multipart:
enabled: true
max-file-size: 30MB
max-request-size: 30MB
# redis
redis:
host: ip地址 # 默认127.0.0.1
port: 6379 # 默认6379
password: 密码
# 整合mybatis 下面使用MP也可以配置映射的文件
# mybatis:
# mapper-locations:
# - classpath:com/zhao/mapper/*.xml
mybatis-plus:
configuration:
# 更新字段的时候设置为null,忽略实体null判断之后,解决Oracle无效的列异常, oracle数据库必须配置
jdbc-type-for-null: 'null' # 注意要有单引号
# mybatis-plus驼峰映射
map-underscore-to-camel-case: true
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# mybatis的xml文件地址,如果启动文件找不到xml文件,如下去修改pom.xml文件
mapper-locations: classpath:mybatis/mapper/*.xml
global-config:
# 禁用mybatis-plus的LOGO
banner: false
db-config:
# 逻辑未删除值,(逻辑删除下有效)
logic-delete-value: 1
# 逻辑未删除值,(逻辑删除下有效)需要注入逻辑策略LogicSqlInjector,以@Bean方式注入
logic-not-delete-value: 0
# 对应实体类的字段,写了这个在实体类中就不需要写注解了
logic-delete-field: deleted
server:
port: 9800
三.五、logback.xml(记录日志)
<?xml version="1.0" encoding="UTF-8" ?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<!-- 工程名/项目名 -->
<contextName>project_name</contextName>
<!-- 路径变量 -->
<property name="logPath" value="/opt/logs" />
<!-- 日志格式变量 -->
<property name="logPattern" value="%date{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg%n" />
<!--
1. %d{HH:mm:ss.SSS} 显示的时间
2. [%thread] 打印线程号,log4j2使用%t]也可以
3. %-5level 日志级别,并且使用5个字符靠左对齐
4. %logger{35} 日志输出者的名字,即类的类名
5. %file 打印类名,也可用%class,打印的全限定类名
6. %line 打印日志所在代码行数
7. %msg 日志消息
8. %n 平台的换行符
-->
<!--把>=debug的日志输出到控制台 -->
<appender name="SDTOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${logPattern}</pattern>
</encoder>
</appender>
<!--输出到文件(>=info的日志)-->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- /opt 目录下(linux环境) ,没有的话会自己新建的-->
<file>${logPath}/info.log</file>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 每天日志归档路径以及格式 -->
<fileNamePattern>${logPath}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${logPattern}</pattern>
</encoder>
<!--临界值日志过滤级别配置 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- 在日志配置级别的基础上过滤掉info级别以下的日志 -->
<level>INFO</level>
</filter>
</appender>
<!--输出到文件(=error的日志)-->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logPath}/error.log</file>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 每天日志归档路径以及格式 -->
<fileNamePattern>${logPath}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${logPattern}</pattern>
</encoder>
<!-- 此日志文件只记录ERROR级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- <logger name="org.mybatis" level="INFO"></logger> -->
<!-- 开发阶段使用DEBUG 生产环境使用INFO -->
<root level="INFO">
<appender-ref ref="SDTOUT" />
<appender-ref ref="INFO_FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
</configuration>
四、创建项目结构
五、404、500页面
resources > error > 404.html(500.html)
六、工具类
1、JSON返回工具类,也可以自己写
SpringBoot的JSON工具类(java),用于前后端分离_我认不到你的博客-CSDN博客
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Results<T> {
public static final String ERROR = "500";
public static final String SUCCESS = "200";
/**
* 返回码
*/
private String code;
/**
* 返回消息
*/
private String message;
/**
* 返回实体
*/
private T data;
public static <T> Results<T> success(String msg){
return success(SUCCESS,msg,null);
}
public static <T> Results<T> success(T obj){
return success(SUCCESS,"成功",obj);
}
public static <T> Results<T> success(String msg,T obj){
return success(SUCCESS,msg,obj);
}
public static <T> Results<T> success(String resCode,String msg,T obj){
Results<T> result = new Results<T>();
result.setCode(resCode);
result.setMessage(msg);
result.setData(obj);
return result;
}
public static <T> Results<T> failed(String msg) {
return failed(ERROR,msg,null);
}
public static <T> Results<T> failed(String msg,T obj) {
return failed(ERROR,msg,obj);
}
public static <T> Results<T> failed(String resCode,String msg) {
return failed(resCode,msg,null);
}
public static <T> Results<T> failed(String resCode,String msg,T obj) {
Results<T> result = new Results<T>();
result.setCode(resCode);
result.setMessage(msg);
result.setData(obj);
return result;
}
}
2、Redis工具类(utils包下)
Redis工具类(redisTemplate)以及 redisTemplate 的用法_我认不到你的博客-CSDN博客
SpringBoot整合Rides_我认不到你的博客-CSDN博客
3、MD5工具类(utils包下)
/**
* MD5工具类
* @author PING.AN.ZHAO
* @date 2022年7月10日
*/
public class MD5Util {
public static String md5(String source) throws NoSuchAlgorithmException {
String des = "";
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] result = md.digest(source.getBytes());
StringBuilder buf = new StringBuilder();
for (byte b : result) {
buf.append(String.format("%02X", b));
}
des = buf.toString().toLowerCase();
return des;
}
}
此工具类转自:Java MD5 加密工具类_LookingTomorrow的博客-CSDN博客_java md5加密工具类
4、字母编号+数字,并且需要自增工具类
/**
* @Role 字母+数字工具类
* @Author 赵平安
* @GMTCreate 2022年7月20日
* @Modifier 修改bug,且第一个set弃用
* @GMTModified 赵平安
* @ModifyContent 2022年7月28日
*/
public class LetterNumberUtil {
public static String gal(String LetterNumber,String name) {
//没有的时候填充
if (LetterNumber==null||LetterNumber.trim().isEmpty()){
return name+"000001";
}
//截取头部字母编号
StringBuffer head = new StringBuffer();
String[] split = LetterNumber.split("");
for (String s : split) {
if (!s.matches("^[0-9]*$")){
head.append(s);
}else break;
}
//截取尾部数字
String tail = LetterNumber.replaceAll(" ","").substring(head.length(), LetterNumber.replaceAll(" ","").length());
//尾部数字 +1
int num = Integer.valueOf(tail) + 1;
//填充 0
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < tail.length(); i++) {
buffer.append(0);
}
String number = buffer.toString();
//合并字符串
number = number + num;
//进位
if (tail.length()<String.valueOf(num).length()){
number = number.substring(number.length() - tail.length()-1, number.length());
}else number = number.substring(number.length() - tail.length(), number.length());
return head + number;
}
}
5、BeansUtils
使用反射:类中的String如果为null转为"",应用场景,数据库不能为null的字段,再比如Mybatis插入Oracle时,为null会报错
public class BeanUtils extends BeanUtil{
/**
* 如果对象中的属性类别为 String 且值为null的话设置为 ""
* 作用的话,不是很清楚,应该是怕数据库字段设置不能为空吧
* @param t 对象
* @param <T> 泛型
* @throws Exception 往上抛,由调用者解决异常问题
* @author ZHAOPINGAN
*/
public static <T> void stringNotNull(T t) throws Exception{
if (Objects.isNull(t)){
return;
}
Class<?> tClass = t.getClass();
Field[] fields = tClass.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
if (field.getType() == String.class) {
Object o = field.get(t);
if (Objects.isNull(o)){
field.set(t,"");
}
}
}
}
}
6、线程池工具类
/**
* 线程池工具类,单例模式,全局唯一
* @author ZHAOPINGAN
*/
public class ThreadPoolUtil {
private static volatile ExecutorService threadPool = null;
private ThreadPoolUtil(){}
public static ExecutorService getThreadPool() {
if (threadPool == null) {
synchronized (ThreadPoolUtil.class) {
if (threadPool == null) {
threadPool = new ThreadPoolExecutor(
// 核心线程数
4,
10,
// 10s 过期时间
10000L, TimeUnit.MILLISECONDS,
// 队列 1000
new LinkedBlockingQueue<>(1000),
// 线程池工厂
Executors.defaultThreadFactory(),
// 拒绝策略 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息
new ThreadPoolExecutor.AbortPolicy()
);
}
}
}
return threadPool;
}
}
7、生成Token的工具类
8、线程缓存工具类(可以用于JWT生成Token后拦截token并缓存用户数据,可以直接通过getUserInfo,获取用户信息,当然UserInfo是我用来保存用户的数据,你们可以自定义)
/**
* 线程缓存工具类
*
* @author ZPA
*/
public class ThreadLocalUtils {
private static final ThreadLocal<Map<String, Object>> cache = new ThreadLocal<Map<String, Object>>();
private static final String KEY = "userInfo";
/**
* 向ThreadLocal缓存用户登录信息
* @param userInfo 要缓存的VALUE
*/
public static void setUserInfo(UserInfo userInfo) {
if (!isCaheIsNull()) {
cache.get().put(KEY, userInfo);
} else {
Map<String, Object> map = new HashMap<>();
map.put(KEY, userInfo);
cache.set(map);
}
}
/**
* 向ThreadLocal缓存用户登录信息
*/
public static UserInfo getUserInfo() {
Map<String, Object> map = cache.get();
if (isCaheIsNull()) {
return null;
}
return (UserInfo)map.getOrDefault(KEY, null);
}
/**
* 向ThreadLocal缓存值
*
* @param key 要缓存的KEY
* @param value 要缓存的VALUE
*/
public static void set(String key, Object value) {
if (!isCaheIsNull()) {
cache.get().put(key, value);
} else {
Map<String, Object> map = new HashMap<>();
map.put(key, value);
cache.set(map);
}
}
/**
* 从ThreadLocal里获取缓存的值
*
* @param key 要获取的数据的KEY
* @return 要获取的值
*/
public static Object getCache(String key) {
Map<String, Object> map = cache.get();
if (isCaheIsNull()) {
return null;
}
return map.getOrDefault(key, null);
}
/**
* 根据KEY移除缓存里的数据
*
* @param key
*/
public static void removeByKey(String key) {
if (isCaheIsNull()) {
return;
} else {
cache.get().remove(key);
}
}
/**
* 移除当前线程缓存
* 用于释放当前线程threadlocal资源
*/
public static void remove() {
cache.remove();
}
private static boolean isCaheIsNull() {
return cache.get() == null;
}
}
9、RSA加密工具类
/**
* RSA加密解密工具类
*/
public class RSAUtils {
/**
* 常量字符串
*/
private static final String RSA = "RSA";
/**
* 私钥
*/
private static final String PRIVATE_KEY = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAItn9b5IvrTh1rnhzJJFNQ4UnJBL9kRitVHHklkz28ykYB5IWWM4lvZ7VzBP01AFG/I+otjr62Xx+gurInr22/F8FA4uKOmiYCPPf5h1mEvRK0y20ShYtDiwu9eM8hYkCqBvBHDKYjNAFmw3+HRzgCmGFEnmAG44bKCnZiMqUEf9AgMBAAECgYA05uCksypbrhA0PfHJ2CWIEF5Ri+IKlYLFY/yviTRx9jbbhw0U0BbJtoihtskz5pxyUz6tHuoXp7oBz5GoJCHWcvUGHTfJx8aq2IJwfJKGfi5V1I7ke3H6A8fI9lE8Y62lFSJmZITbUv3TsBiec/GhotYn3x5cu329MFX6miY7QQJBANwuq/jRM9RB+d8NUdO9XsWHVqCVp59J1KyKR7UHTgshH5ahHUGiSBtqM9yirKPnPHJLZpDsloNUDm7xhoLfULkCQQCiFWw7VZ7gKwY6q4nxBcb5BXDb3Fa5JKr7ggduvF8C90nue7TMbVMr0z8HHDvqhjzD06XTYG2P4+xPBwm5E2dlAkAMHkl6xVDb8tGk1B/Xzoljx8Idzn7ORor9ABNYRFGoTSdm6/EnRp4/XAYEs7NaxgROqhW4Dj1udvbgZkyn8VCJAkAJPV9mIoNkFA/O2GiMrN+i4oSEhBMNiuGUZN03mtVvvdkhFzw/Sxwqq2g0Z4+i1vQv1ajmW+DjCwM1nhkXy9thAkBPh46mKPQ58yURs9hlhLlUziUFjq83sSzXJkZQx7EWOud0U8Nriq+xsrVKJRkzntmIhLBacdwQSyImIWkStd7v";
/**
* 公钥
*/
private static final String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCLZ/W+SL604da54cySRTUOFJyQS/ZEYrVRx5JZM9vMpGAeSFljOJb2e1cwT9NQBRvyPqLY6+tl8foLqyJ69tvxfBQOLijpomAjz3+YdZhL0StMttEoWLQ4sLvXjPIWJAqgbwRwymIzQBZsN/h0c4AphhRJ5gBuOGygp2YjKlBH/QIDAQAB";
/**
* 获取密钥对象
* @param keySize RSA算法模长(个人理解应该是模长越大加密安全性越高,但加密过程可能也越长)
* @return List
*/
public static List<Key> getRsaObject(int keySize) throws NoSuchAlgorithmException {
//创建list用来接收公钥对象和私钥对象
List<Key> keyList = new ArrayList<>();
//创建RSA密钥生成器
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA);
//设置密钥大小,RSA算法的模长=最大加密数据的大小
keyPairGenerator.initialize(keySize);
//调用函数生成公钥私钥对象(以对生成密钥)
KeyPair keyPair = keyPairGenerator.generateKeyPair();
//获取公钥放入list
keyList.add(keyPair.getPublic());
//获取私钥放入list
keyList.add(keyPair.getPrivate());
//返回list
return keyList;
}
/**
* 生成公钥私钥的字符串
* @param keySize 模长
* @return List
*/
public static List<String> getRsaKeyString(int keySize) throws NoSuchAlgorithmException {
//创建list用来接收公钥对象和私钥对象
List<String> keyList = new ArrayList<>();
//创建RSA密钥生成器
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA);
//设置密钥大小,RSA算法的模长=最大加密数据的大小
keyPairGenerator.initialize(keySize);
//调用函数生成公钥私钥对象(以对生成密钥)
KeyPair keyPair = keyPairGenerator.generateKeyPair();
//将公钥对象转换为字符串通过base64加密
String publicKey = Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
//将私钥对象转换为字符串通过base64加密
String privateKey = Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded());
//获取公钥放入list
keyList.add(publicKey);
//获取私钥放入list
keyList.add(privateKey);
//返回list
return keyList;
}
/**
* 通过公钥字符串生成公钥对象(RSAPublicKey类型)
* X509EncodeKeySpec方式(字符串公钥转为RSAPublicKey公钥)
* @param publicKeyStr 公钥字符串
* @return 返回RSAPublicKey类型的公钥对象
*/
public static RSAPublicKey getRSAPublicKeyByX509(String publicKeyStr) throws NoSuchAlgorithmException, InvalidKeySpecException {
//密钥工厂创建
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
//公钥字符解密为bytes数组
byte[] keyBytes = Base64.getDecoder().decode(publicKeyStr);
//公钥字符串转x509
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(keyBytes);
//x509转RSAPublicKey
return (RSAPublicKey) keyFactory.generatePublic(x509EncodedKeySpec);
}
/**
* 通过私钥字符串生成私钥对象(RSAPrivateKey类型)
* PKCS8EncodedKeySpec方式(字符串私钥转为RSAPrivateKey公钥)
* @param privateKey 私钥字符串
* @return 返回RSAPrivateKey类型的私钥对象
*/
public static RSAPrivateKey getRSAPrivateKeyByPKCS8(String privateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
//密钥工厂创建
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
//私钥字符串解密为bytes数组
byte[] keyBytes = Base64.getDecoder().decode(privateKey);
//私钥字符串转pkcs8
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(keyBytes);
//pkcs8转RSAPrivateKey
return (RSAPrivateKey) keyFactory.generatePrivate(pkcs8EncodedKeySpec);
}
/**
* 公钥加密
* @param message 需要加密的信息
* @param rsaPublicKey rsa公钥对象
* @return 返回信息被加密后的字符串
*/
public static String encryptByPublicKey(String message, RSAPublicKey rsaPublicKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, IOException {
//RSA加密实例
Cipher cipher = Cipher.getInstance(RSA);
//初始化公钥
cipher.init(Cipher.ENCRYPT_MODE, rsaPublicKey);
//模长转为字节数
int modulusSize = rsaPublicKey.getModulus().bitLength()/8;
//PKCS PADDING长度为11字节,解密数据是除去这11byte
int maxSingleSize = modulusSize-11;
//切分字节数,每段不大于maxSingleSize
byte[][] dataArray = splitArray(message.getBytes(), maxSingleSize);
//字节数组输出流
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//分组加密,加密后内容写入输出字节流
for (byte[] s : dataArray){
byteArrayOutputStream.write(cipher.doFinal(s));
}
//使用base64将字节数组转为string类型
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}
/**
* 公钥加密
* @param message 需要加密的信息
* @param publicKey rsa公钥字符串
* @return 返回信息被加密后的字符串
*/
public static String encryptByPublicKey(String message, String publicKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, IOException, InvalidKeySpecException {
RSAPublicKey rsaPublicKeyByX509 = getRSAPublicKeyByX509(publicKey);
return encryptByPublicKey(message,rsaPublicKeyByX509);
}
/**
* 私钥解密密
* @param encryptedMessage 信息加密后的字符串
* @param rsaPrivateKey rsa私钥对象
* @return 返回解密后的字符串
*/
public static String decryptByPrivateKey(String encryptedMessage, RSAPrivateKey rsaPrivateKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, IOException {
//RSA加密实例
Cipher cipher = Cipher.getInstance(RSA);
//初始化公钥
cipher.init(Cipher.DECRYPT_MODE, rsaPrivateKey);
//加密算法模长
int modulusSize = rsaPrivateKey.getModulus().bitLength()/8;
byte[] dataBytes = encryptedMessage.getBytes();
//加密做了转码,这里也要用base64转回来
byte[] decodeData = Base64.getDecoder().decode(dataBytes);
//切分字节数,每段不大于maxSingleSize
byte[][] dataArray = splitArray(decodeData, modulusSize);
//字节数组输出流
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//分组解密,解密后内容写入输出字节流
for (byte[] s : dataArray){
byteArrayOutputStream.write(cipher.doFinal(s));
}
//使用base64将字节数组转为string类型
return byteArrayOutputStream.toString();
}
/**
* 私钥解密密
* @param encryptedMessage 信息加密后的字符串
* @param privateKey rsa私钥字符串
* @return 返回解密后的字符串
*/
public static String decryptByPrivateKey(String encryptedMessage, String privateKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, IOException, InvalidKeySpecException {
RSAPrivateKey rsaPublicKeyByX509 = getRSAPrivateKeyByPKCS8(privateKey);
return decryptByPrivateKey(encryptedMessage,rsaPublicKeyByX509);
}
/**
* 私钥解密密
* @param encryptedMessage 信息加密后的字符串
* @return 返回解密后的字符串
*/
public static String decryptByPrivateKey(String encryptedMessage) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, IOException, InvalidKeySpecException {
return decryptByPrivateKey(encryptedMessage,PRIVATE_KEY);
}
/**
* 按指定长度切分数组
* @param byteArrayInfo 需要切分的byte数组
* @param maxSize 单个字节数组长度
* @return byte[][]
*/
private static byte[][] splitArray(byte[] byteArrayInfo, int maxSize){
int dataLen = byteArrayInfo.length;
if(dataLen<=maxSize){
return new byte[][]{byteArrayInfo};
}
byte[][] result = new byte[(dataLen-1)/maxSize+1][];
int resultLen = result.length;
for (int i = 0; i < resultLen; i++) {
if(i==resultLen-1){
int sLen = dataLen-maxSize*i;
byte[] single = new byte[sLen];
System.arraycopy(byteArrayInfo, maxSize*i, single, 0, sLen);
result[i] = single;
break;
}
byte[] single = new byte[maxSize];
System.arraycopy(byteArrayInfo, maxSize*i, single, 0, maxSize);
result[i] = single;
}
return result;
}
/**
* 返回公钥
* @return String
*/
public static String getPublicKey(){
return PUBLIC_KEY;
}
public static void main(String[]args) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, IOException, InvalidKeyException, InvalidKeySpecException {
String message = "fdsihufdiuheiuhasfd";
System.out.println("加密前的信息内容:"+message);
System.out.println("-----------------------RSA密钥对象加密解密--------------------------------");
//生成密钥对象
List<Key> keyList = RSAUtils.getRsaObject(1024);
//RSA类型的公钥对象
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyList.get(0);
//RSA类型的私钥对象
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyList.get(1);
//通过RSA类型的公钥对象加密信息
String encryptedMsg = RSAUtils.encryptByPublicKey(message, rsaPublicKey);
System.out.println("加密后的字符串encryptedMsg:\r\n"+encryptedMsg);
//通过RSA类型的私钥对象解密
String decryptedMsg = RSAUtils.decryptByPrivateKey(encryptedMsg, rsaPrivateKey);
System.out.println("------");
System.out.println("解密后的字符串decryptedMsg:\r\n"+decryptedMsg);
System.out.println();
System.out.println("-----------------------字符串类型的密钥对象转为RSA密钥对象后的加密解密--------------------------");
//生成密钥(字符串类型的密钥)
List<String> keyStringList = RSAUtils.getRsaKeyString(1024);
//字符串类型的公钥
String publicKeyString = keyStringList.get(0);
System.out.println("生成的公钥,可以把 PUBLIC_KEY 改为此值:\r\n"+publicKeyString);
//字符串类型的私钥
String privateKeyString = keyStringList.get(1);
System.out.println("生成的私钥,可以把 PRIVATE_KEY 改为此值:\r\n"+publicKeyString);
//将字符串类型的公钥私钥转为RSA类型
RSAPublicKey rsaPublicKey1 = RSAUtils.getRSAPublicKeyByX509(publicKeyString);
RSAPrivateKey rsaPrivateKey1 = RSAUtils.getRSAPrivateKeyByPKCS8(privateKeyString);
//通过字符串公钥转为RSA类型的公钥 加密
String encryptedMsg1 = RSAUtils.encryptByPublicKey(message, rsaPublicKey1);
System.out.println("使用字符串转的rsa公钥对象加密后的字符串encryptedMsg1:\r\n"+encryptedMsg1);
//通过字符串私钥转为RSA类型的私钥 解密
String decryptedMsg1 = RSAUtils.decryptByPrivateKey(encryptedMsg1, rsaPrivateKey1);
System.out.println("------");
System.out.println("使用字符串转的私钥对象解密后的字符串decryptedMsg1:\r\n"+decryptedMsg1);
}
}
10、校验图片的静态方法
图片文件的二进制数据也包含了一些特定的标识信息,通过解析这些标识信息可以判断出图片的格式。以下是一些常见图片格式的标识信息:
图片格式 | 标识信息(十六进制) |
---|---|
JPEG | FF D8 |
PNG | 89 50 4E 47 0D 0A 1A 0A |
GIF | 47 49 46 38 |
BMP | 42 4D |
TIFF | 49 49 2A 00 |
/**
* 通过二进制数据校验上传图片时的格式
* 支持:jpeg|jpg、png、gif、bmp、tiff 格式的图片
* @param file 文件
* @return Boolean
* @throws IOException IO异常
*/
public static Boolean verifyImageFormat(MultipartFile file) throws IOException {
if (file == null){
return false;
}
FileInputStream inputStream = (FileInputStream) file.getInputStream();
byte[] buffer = new byte[8];
inputStream.read(buffer);
// jpeg|jpg
if (buffer[0] == (byte) 0xFF && buffer[1] == (byte) 0xD8) {
return true;
}
// png
if (buffer[0] == (byte) 0x89 && buffer[1] == (byte) 0x50 && buffer[2] == (byte) 0x4E && buffer[3] == (byte) 0x47
&& buffer[4] == (byte) 0x0D && buffer[5] == (byte) 0x0A && buffer[6] == (byte) 0x1A && buffer[7] == (byte) 0x0A) {
return true;
}
// gif
if (buffer[0] == (byte) 0x47 && buffer[1] == (byte) 0x49 && buffer[2] == (byte) 0x46 && buffer[3] == (byte) 0x38) {
return true;
}
// bmp
if (buffer[0] == (byte) 0x42 && buffer[1] == (byte) 0x4D) {
return true;
}
// tiff
return buffer[0] == (byte) 0x49 && buffer[1] == (byte) 0x49 && buffer[2] == (byte) 0x2A && buffer[3] == (byte) 0x00;
}
11、树生成工具类
public class TreeUtils {
private TreeUtils() {
}
/**
* @param collection 集合
* @param getId 子节点
* @param getParentId 父节点
* @param setNode 设置子集合
* @param <E> 集合的类型
* @param <R> 节点属性的类型
* @return Collection
*/
public static <E, R> Collection<E> tree(Collection<E> collection, Function<E, R> getId, Function<E, R> getParentId, BiConsumer<E, Collection<E>> setNode) {
Collection<E> root = new LinkedList<>();
for (E node : collection) {
R parentId = getParentId.apply(node);
R id = getId.apply(node);
Collection<E> elements = new LinkedList<>();
boolean isParent = true;
for (E element : collection) {
if (id.equals(getParentId.apply(element))) {
elements.add(element);
}
if (isParent && getId.apply(element).equals(parentId)) {
isParent = false;
}
}
if (isParent) {
root.add(node);
}
setNode.accept(node, elements);
}
return root;
}
/**
* 递归构造树
*
* @param allList 全部数据
* @param getId 子节点
* @param getParentId 父节点
* @param vertexId 顶点ID 可为空
* @param setNode 设置子集合
* @param <E> 集合的类型
* @param <R> 节点属性的类型
* @return List
*/
private static <E, R> List<E> constructionTree(List<E> allList, Function<E, R> getId, Function<E, R> getParentId, R vertexId, BiConsumer<E, List<E>> setNode) {
List<E> rootNodeList = new ArrayList<>();
List<E> treeList = new ArrayList<>();
allList.forEach(x -> {
if (Objects.equals(vertexId, getParentId.apply(x))) {
rootNodeList.add(x);
}
});
for (E treeRootNode : rootNodeList) {
buildChildTree(treeRootNode, allList, getId, getParentId, setNode);
treeList.add(treeRootNode);
}
return treeList;
}
/**
* 递归构造树(过滤)
*/
private static <E, R> List<E> constructionTree(List<E> list, Function<E, R> getId, Function<E, R> getParentId,
R vertexId, BiConsumer<E, List<E>> setNode, Function<E, E> filter) {
List<E> allList = new ArrayList<>();
List<E> rootNodeList = new ArrayList<>();
List<E> treeList = new ArrayList<>();
list.forEach(x -> {
E apply = filter.apply(x);
allList.add(apply);
if (Objects.equals(vertexId, getParentId.apply(x))) {
rootNodeList.add(apply);
}
});
for (E treeRootNode : rootNodeList) {
buildChildTree(treeRootNode, allList, getId, getParentId, setNode);
treeList.add(treeRootNode);
}
return treeList;
}
/**
* 递归-----构建子树形结构
*
* @param pNode 根节点(顶级节点)
* @param treeList 全部数据
* @return 整棵树
*/
public static <E, R> E buildChildTree(E pNode, List<E> treeList, Function<E, R> getId, Function<E, R> getParentId, BiConsumer<E, List<E>> setNode) {
List<E> childTree = new ArrayList<>();
for (E treeNode : treeList) {
if (getId.apply(pNode).equals(getParentId.apply(treeNode))) {
childTree.add(buildChildTree(treeNode, treeList, getId, getParentId, setNode));
}
}
setNode.accept(pNode, childTree);
return pNode;
}
/**
* 测试
*/
public static void main(String[] args) {
// 普通树1
List<MenuItem> menuList = Arrays.asList(
new MenuItem(1, "二级菜单1", 0),
new MenuItem(2, "二级菜单1", null),
new MenuItem(3, "二级菜单1", 2),
new MenuItem(4, "二级菜单2", 2),
new MenuItem(5, "三级菜单1", 3),
new MenuItem(6, "三级菜单2", 3)
);
// 普通树2
List<MenuItem> menuList2 = Arrays.asList(
new MenuItem(1, "二级菜单1", 0),
new MenuItem(2, "二级菜单1", null),
new MenuItem(3, "二级菜单1", 1),
new MenuItem(4, "二级菜单2", 2),
new MenuItem(5, "三级菜单1", 3),
new MenuItem(6, "三级菜单1", 3),
new MenuItem(7, "三级菜单2", 4)
);
// 过滤
List<DiyData> diyData = Arrays.asList(
new DiyData(1, "二级菜单1", 0),
new DiyData(2, "二级菜单1", null),
new DiyData(3, "二级菜单1", 1),
new DiyData(4, "二级菜单2", 2),
new DiyData(5, "三级菜单1", 3),
new DiyData(6, "三级菜单1", 3),
new DiyData(7, "三级菜单2", 4)
);
// 不过滤
List<DiyData> diyData2 = Arrays.asList(
new DiyData(1, "二级菜单1", 0),
new DiyData(2, "二级菜单1", null),
new DiyData(3, "二级菜单1", 1),
new DiyData(4, "二级菜单2", 2),
new DiyData(5, "三级菜单1", 3),
new DiyData(6, "三级菜单1", 3),
new DiyData(7, "三级菜单2", 4)
);
// 普通树1
Collection<MenuItem> tree = TreeUtils.tree(menuList, MenuItem::getId, MenuItem::getParentId, (x, y) -> x.setChildren((List<MenuItem>) y));
System.out.println(tree);
// 普通树2
List<MenuItem> tree2 = TreeUtils.constructionTree(menuList2, MenuItem::getId, MenuItem::getParentId, null, MenuItem::setChildren);
System.out.println(tree2);
// 过滤
List<DiyData> tree3 = TreeUtils.constructionTree(diyData, DiyData::getId, DiyData::getParentId, null, DiyData::setChildren
, x -> (DiyData) new DiyData()
.setKey(x.getId())
.setId(x.getId())
.setTitle(x.getName())
.setParentId(x.getParentId()));
System.out.println(tree3);
// 不过滤
List<DiyData> tree4 = TreeUtils.constructionTree(diyData2, DiyData::getId, DiyData::getParentId, null, DiyData::setChildren);
System.out.println(tree4);
}
/**
* 测试普通 实体类
*/
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
static public class MenuItem {
private Integer id;
private String name;
private Integer parentId;
private List<MenuItem> children;
public MenuItem(Integer id, String name, Integer parentId) {
this.id = id;
this.name = name;
this.parentId = parentId;
}
}
/**
* 测试过滤 实体类
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
static public class DiyData extends TreeNode {
/**
* id 需映射 TreeNode 中的 key
*/
private Integer id;
/**
* id 需映射 TreeNode 中的 title
*/
private String name;
private Integer parentId;
}
/**
* 供前端组件使用,可自行修改
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public static class TreeNode {
/**
* 前端需要对应的id
*/
private Integer key;
/**
* 节点id
*/
private Integer id;
/**
* 父节点id
*/
private Integer parentId;
/**
* 前端需要对应公司的name
*/
private String title;
/**
* 子节点
*/
private List<? extends TreeNode> children;
}
}
七、配置类(config包下)
1、MyBatisPlusConfig类管理乐观锁注解和填充策略
@MapperScan("com.zhao.mapper")//扫描我们的mapper文件夹
@EnableTransactionManagement //事务控制
@Configuration //配置类
public class MyBatisPlusConfig implements MetaObjectHandler {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 注册乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
//插入时的填充策略
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("createTime", LocalDateTime.now(),metaObject);
this.setFieldValByName("updateTime", LocalDateTime.now(),metaObject);
}
//更新时的填充策略
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime", LocalDateTime.now(),metaObject);
}
}
2、RedisConfig
@Configuration
public class RedisConfig {
// 编写我们自己的 RedisTemplate,一般拿来用就行了
@Bean
@SuppressWarnings("all") //这tm是个坑,我现在不知道这个注释是干嘛的,但是下面这个注解和这串注解没有的话
// RedisConnectionFactory redisConnectionFactory 会爆红,下面这注解没有注释的话,就序列化不了
// @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
//我们为了自己开发方便,一般直接使用<String, Object>类型
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// Json序列化配置
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); 这个被弃用了,所以现在要用 activateDefaultTyping
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
jackson2JsonRedisSerializer.setObjectMapper(mapper);
// String 的序列化
RedisSerializer<String> stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
3、WebConfig(拦截器)
@Configuration
public class WebConfig implements WebMvcConfigurer{
@Autowired
VoteLoginInterceptor voteLoginInterceptor;
@Autowired
AdminLoginInterceptor adminLoginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自己的拦截器,并设置拦截的请求路径
//addPathPatterns为拦截此请求路径的请求
//excludePathPatterns为不拦截此路径的请求
registry.addInterceptor(voteLoginInterceptor).addPathPatterns("/vote/voting2/**","/vote/whetherVoting/**","/vote/index/voting/**").excludePathPatterns("/static/**");
registry.addInterceptor(adminLoginInterceptor).addPathPatterns("/index/**").excludePathPatterns("/","/static/**","/index");
}
}
3.5 用于token拦截的拦截器
/**
* Token验证配置
*/
@Configuration
public class TokenValidationConfig implements WebMvcConfigurer {
@Autowired
AdminLoginInterceptor adminLoginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自己的拦截器,并设置拦截的请求路径
//addPathPatterns为拦截此请求路径的请求
//excludePathPatterns为不拦截此路径的请求
registry.addInterceptor(adminLoginInterceptor).addPathPatterns("/**").excludePathPatterns("/login","/login/**");
}
}
@Slf4j
@Component
class AdminLoginInterceptor implements HandlerInterceptor {
/**
* 目标方法执行前
* 该方法在控制器处理请求方法前执行,其返回值表示是否中断后续操作
* 返回 true 表示继续向下执行,返回 false 表示中断后续操作
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = "";
// 如果是 OPTIONS 请求,我们就让他通过,不管他
if (request.getMethod().equals("OPTIONS")) {
response.setStatus(HttpServletResponse.SC_OK);
return true;
// 如果不是,我们就把token拿到,用来做判断
}else {
token = request.getHeader("token");
}
// Auth0JwtUtils.verify(token)&&!Auth0JwtUtils.isExpired(token) 这两个是我写的验证token的方法,用的jwt,封装了一个工具类
if (StringUtils.isNotBlank(token) && Auth0JwtUtils.verify(token) && !Auth0JwtUtils.isExpired(token)){
// 用于保存用户信息(可以搜索本章的ThreadLocalUtils工具类)
ThreadLocalUtils.setUserInfo(Auth0JwtUtils.getObject(token,"userInfo",UserInfo.class));
return true;
}else {
JSONObject json = EcpResponseUtil.failed("500","请重新登录");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(json.toString());
writer.flush();
writer.close();
return false;
}
}
/**
* 目标方法执行后
* 该方法在控制器处理请求方法调用之后、解析视图之前执行
* 可以通过此方法对请求域中的模型和视图做进一步修改
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// log.info("postHandle执行{}", modelAndView);
}
/**
* 页面渲染后
* 该方法在视图渲染结束后执行
* 可以通过此方法实现资源清理、记录日志信息等工作
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// log.info("afterCompletion执行异常{}", ex);
ThreadLocalUtils.remove();
}
}
4、DruidConfig
@Configuration
public class DruidConfig {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druidDataSource(){
return new DruidDataSource();
}
@Bean
//后台监控功能:相当于web.xml
//因为Springboot 内置了 servlet 容器,所以没有web.xml , 代替方法:ServletRegistrationBean
public ServletRegistrationBean StatViewServlet(){
ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
//后台需要有人登录,账号密码配置
HashMap<String, String> initParameters = new HashMap<>();
//增加配置
initParameters.put("loginUserName","admin");//登录的key是固定参数:loginUserName loginPassword
initParameters.put("loginPassword","123456");
//允许谁可以访问
// initParameters.put("allow","localhost");//仅允许本地登录
initParameters.put("allow","");//允许所有人登录
//禁止谁能访问
// initParameters.put("kuangshen","192.168.11.123");//禁止这个ip地址访问
bean.setInitParameters(initParameters);//设置初始化参数
return bean;
}
@Bean
//filter
public FilterRegistrationBean webStatFilter(){
FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
bean.setFilter(new WebStatFilter());
//可以过滤哪些请求
HashMap<String, String> initParameters = new HashMap<>();
//这些东西不进行统计
initParameters.put("exclusions","*.js,*.css,/druid/*");
bean.setInitParameters(initParameters);
return bean;
}
}
5、跨域问题
(1)、第一种
/**
* @Role 跨域请求
* @Author 赵平安
* @GMTCreate 2022年7月21日
* @Modifier
* @GMTModified
* @ModifyContent
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") //拦截所有请求
//是否发送Cookie,用于凭证请求, 默认不发送cookie。
.allowCredentials(true)
//出现的问题:当allowCredentials为真时,allowedorigin不能包含特殊值"*",因为不能在"访问-控制-起源“响应头中设置该值。要允许凭证到一组起源,显示地列出它们,或者考虑使用"allowedOriginPatterns”代替。
// .allowedOrigins("*") //可跨域的域名
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE") // 允许跨域的方法,可以单独配置
//配置预检请求的有效时间, 单位是秒,表示:在多长时间内,不需要发出第二次预检请求。
.maxAge(1800)
//配置允许的自定义请求头,用于 预检请求。
.allowedHeaders("*")
.exposedHeaders("*");
}
}
(1)、第二种(应对SpringBoot的版本比较低)
/**
* 跨域请求
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Resource
private CorsInterceptor corsInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 跨域拦截器需放在最上面
registry.addInterceptor(corsInterceptor).addPathPatterns("/**");
}
}
@Component
class CorsInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String origin = request.getHeader("origin");
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS");
response.setHeader("Access-Control-Max-Age", "86400");
//Headers值区分大小写
response.setHeader("Access-Control-Allow-Headers", "content-type,membertoken");
// 如果是OPTIONS则结束请求
if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
response.setStatus(HttpStatus.NO_CONTENT.value());
return false;
}
return true;
}
}
6、fastJson配置类
/**
* FastJson 配置类
* @author ZHAOPINGAN
*/
@Configuration
public class FJsonConfig {
@Bean
public HttpMessageConverter configureMessageConverters() {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
FastJsonConfig config = new FastJsonConfig();
config.setSerializerFeatures(
// 保留map空的字段
SerializerFeature.WriteMapNullValue,
// 将String类型的null转成""
SerializerFeature.WriteNullStringAsEmpty,
// 将Number类型的null转成0
// SerializerFeature.WriteNullNumberAsZero,
// 将List类型的null转成[]
SerializerFeature.WriteNullListAsEmpty,
// 将Boolean类型的null转成false
SerializerFeature.WriteNullBooleanAsFalse,
// 避免循环引用
SerializerFeature.DisableCircularReferenceDetect);
converter.setFastJsonConfig(config);
converter.setDefaultCharset(Charset.forName("UTF-8"));
List<MediaType> mediaTypeList = new ArrayList<>();
// 解决中文乱码问题,相当于在Controller上的@RequestMapping中加了个属性produces = "application/json"
mediaTypeList.add(MediaType.APPLICATION_JSON);
converter.setSupportedMediaTypes(mediaTypeList);
return converter;
}
}
八、配置拦截器(interceptor包下)
@Slf4j
@Component
public class AdminLoginInterceptor implements HandlerInterceptor {
/**
* 目标方法执行前
* 该方法在控制器处理请求方法前执行,其返回值表示是否中断后续操作
* 返回 true 表示继续向下执行,返回 false 表示中断后续操作
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Object loginUser = request.getSession().getAttribute("adminInfo");
if (loginUser == null) {
//未登录,返回登陆页
request.setAttribute("msg", "您没有权限进行此操作,请先登陆!");
request.getRequestDispatcher("/").forward(request, response);
return false;
} else {
//放行
return true;
}
}
/**
* 目标方法执行后
* 该方法在控制器处理请求方法调用之后、解析视图之前执行
* 可以通过此方法对请求域中的模型和视图做进一步修改
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// log.info("postHandle执行{}", modelAndView);
}
/**
* 页面渲染后
* 该方法在视图渲染结束后执行
* 可以通过此方法实现资源清理、记录日志信息等工作
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// log.info("afterCompletion执行异常{}", ex);
}
}
九、controller、service、pojo、mapper自动生成(测试类中)
/**
* <p>
* 代码生成器(快速版本)
* </p>
*
* @author 赵平安
* @since 2022-4-12 0022 16:51
*/
public class FastCodeGenerator {
// 基础信息配置
// 数据库连接字符
private static final String URL = "jdbc:mysql://127.0.0.1:3306/school_vote?useUnicode=true&serverTimezone=UTC&useSSL=false&characterEncoding=utf8";
// 数据库用户名
private static final String USERNAME = "root";
// 数据库密码
private static final String PASSWORD = "123456";
// 项目根路径。生成结果如:D:\MyProject\spring-boot
private static final String projectRootPath = System.getProperty("user.dir");
// 项目根路径(测试用,非通用)(此句是本项目测试用的。实际项目中,包括多模块项目,请注释掉此句,使用上句)
// private static final String projectRootPath = System.getProperty("user.dir") + "/study-mybatis-plus-fast-generator";
// 父包名。用于生成的java文件的import。
// private static final String parentPackageName = "com.cxhit.mybatisplus.generator";
private static final String parentPackageName = "com.zhao.projectname";
/**
* 执行此处
*/
public static void main(String[] args) {
// 简单示例,适用于单模块项目
simpleGenerator();
// 完整示例,适用于多模块项目
// completeGenerator();
}
/**
* 【单模块】简单的实现方案
*/
protected static void simpleGenerator() {
// 包路径
String packagePath = projectRootPath + "/src/main/java";
// XML文件的路径
String mapperXmlPath = projectRootPath + "/src/main/resources/Mybatis/mapper";
// 开始执行代码生成
FastAutoGenerator.create(URL, USERNAME, PASSWORD)
// 1. 全局配置
.globalConfig(builder -> builder
// 作者名称
.author("赵平安")
// 开启覆盖已生成的文件。注释掉则关闭覆盖。
// .fileOverride()
// 禁止打开输出目录。注释掉则生成完毕后,自动打开生成的文件目录。
.disableOpenDir()
// 指定输出目录。如果指定,Windows生成至D盘根目录下,Linux or MAC 生成至 /tmp 目录下。
.outputDir(packagePath)
// 开启swagger2.注释掉则默认关闭。
// .enableSwagger()
// 指定时间策略。
.dateType(DateType.TIME_PACK)
// 注释时间策略。
.commentDate("yyyy-MM-dd")
)
// 2. 包配置
.packageConfig((scanner, builder) -> builder
// 设置父表名
.parent(parentPackageName)
// .moduleName(scanner.apply("请输入模块名:"))
// mapper.xml 文件的路径。单模块下,其他文件路径默认即可。
.pathInfo(Collections.singletonMap(OutputFile.xml, mapperXmlPath))
//改实体类名字为pojo
.entity("pojo")
)
// 3. 策略配置
.strategyConfig((scanner, builder) -> builder.addInclude(getTables(scanner.apply("请输入表名,多个英文逗号分隔?生成所有表,请输入[all]:")))
// 阶段1:Entity实体类策略配置
.entityBuilder()
// 开启生成实体时生成字段注解。
// 会在实体类的属性前,添加[@TableField("nickname")]
.enableTableFieldAnnotation()
// 逻辑删除字段名(数据库)。
.logicDeleteColumnName("deleted")
// 逻辑删除属性名(实体)。
// 会在实体类的该字段属性前加注解[@TableLogic]
.logicDeletePropertyName("deleted")
// 乐观锁
.versionColumnName("version")
.versionPropertyName("version")
// 会在实体类的该字段上追加注解[@TableField(value = "create_time", fill = FieldFill.INSERT)]
.addTableFills(new Column("create_time", FieldFill.INSERT))
// 会在实体类的该字段上追加注解[@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)]
.addTableFills(new Column("update_time", FieldFill.INSERT_UPDATE))
// 阶段2:Mapper策略配置
.mapperBuilder()
// 开启 @Mapper 注解。
// 会在mapper接口上添加注解[@Mapper]
.enableMapperAnnotation()
// 启用 BaseResultMap 生成。
// 会在mapper.xml文件生成[通用查询映射结果]配置。
.enableBaseResultMap()
// 启用 BaseColumnList。
// 会在mapper.xml文件生成[通用查询结果列 ]配置
.enableBaseColumnList()
// 阶段4:Controller策略配置
.controllerBuilder()
// 会在控制类中加[@RestController]注解。
.enableRestStyle()
// 开启驼峰转连字符
.enableHyphenStyle()
.build()
)
// 4. 模板引擎配置,默认 Velocity 可选模板引擎 Beetl 或 Freemarker
//.templateEngine(new BeetlTemplateEngine())
.templateEngine(new FreemarkerTemplateEngine())
// 5. 执行
.execute();
}
/**
* 【多模块使用】完整的实现方案
*/
protected static void completeGenerator() {
//【1】基础信息配置
// 指定模块名,用于生成的java文件的import。
String moduleName = scanner("请输入模块名:");
// 六个文件的路径。多模块项目下,一般来说每个文件的路径都是不同的(根据项目实际,可能在不同的模块下)。
String entityPath = projectRootPath + "/project-entity/src/main/java/com/yourdomain/projectname/entity/" + moduleName;
String mapperPath = projectRootPath + "/project-dao/src/main/java/com/yourdomain/projectname/mapper/" + moduleName;
String mapperXmlPath = projectRootPath + "/project-dao/src/main/resources/mapper/" + moduleName;
String servicePath = projectRootPath + "/project-service/src/main/java/com/yourdomain/projectname/service/" + moduleName;
String serviceImplPath = projectRootPath + "/project-service/src/main/java/com/yourdomain/projectname/service/" + moduleName + "/impl";
String controllerPath = projectRootPath + "/project-controller/src/main/java/com/yourdomain/projectname/controller/" + moduleName;
// 关于以上写法的解释:
// --- 假设我们的项目有四个模块:project-entity、project-dao、project-service、project-controller
// --- project-entity 的包路径:com.yourdomain.projectname.eneity,
// ---则生成system模块下的sys_user表,生成的实体文件将放在:com.yourdomain.projectname.entity.system包下,SysUser.java。
// --- project-dao 的包路径:com.yourdomain.projectname.mapper,
// ---则生成system模块下的sys_user表,生成的mapper接口文件将放在:com.yourdomain.projectname.mapper.system包下,类名为:SysUserMapper.java。
// --- 其他文件以此类推,即每个模块放MVC结构中对应的类型文件。
// --- 注意:这里最后的文件路径修改了,下文配置中的【2 包配置】中的包路径也要同步修改!否则生成的java文件,首句import会报错。原因是路径错误。
// 【2】开始执行代码生成
FastAutoGenerator.create(URL, USERNAME, PASSWORD)
// 1. 全局配置
.globalConfig(builder -> builder
// 作者名称
.author("赵平安")
// 开启覆盖已生成的文件。注释掉则关闭覆盖。请谨慎开启此选项!
// .fileOverride()
// 禁止打开输出目录。注释掉则生成完毕后,自动打开生成的文件目录。
.disableOpenDir()
// 指定输出目录。多模块下,每个类型的文件输出目录不一致,在包配置阶段配置。
// .outputDir(packagePath)
// 开启swagger2。注释掉则默认关闭。
// .enableSwagger()
// 开启 kotlin 模式。注释掉则关闭此模式
// .enableKotlin()
// 指定时间策略。
.dateType(DateType.TIME_PACK)
// 注释时间策略。
.commentDate("yyyy-MM-dd")
)
// 2. 包配置
.packageConfig((scanner, builder) -> builder
// 阶段1:各个文件的包名设置,用来拼接每个java文件的第一句:package com.XXX.XXX.XXX.xxx;
// 父包名配置
.parent(parentPackageName)
// 输入模块名。此模块名会在下面的几个包名前加。多模块项目,请根据实际选择是否添加。
// .moduleName(moduleName)
.entity("entity." + moduleName)
.mapper("mapper." + moduleName)
.service("service." + moduleName)
.serviceImpl("service." + moduleName + ".impl")
.controller("controller." + moduleName)
.other("other")
// 阶段2:所有文件的生成路径配置
.pathInfo(
new HashMap<OutputFile, String>() {{
// 实体类的保存路径
put(OutputFile.entity, entityPath);
// mapper接口的保存路径
put(OutputFile.mapper, mapperPath);
// mapper.xml文件的保存路径
put(OutputFile.xml, mapperXmlPath);
// service层接口的保存路径
put(OutputFile.service, servicePath);
// service层接口实现类的保存路径
put(OutputFile.serviceImpl, serviceImplPath);
// 控制类的保存路径
put(OutputFile.controller, controllerPath);
}}
)
)
// 3. 策略配置【请仔细阅读每一行,根据项目实际项目需求,修改、增删!!!】
.strategyConfig((scanner, builder) -> builder.addInclude(getTables(scanner.apply("请输入表名,多个英文逗号分隔?生成所有表,请输入[all]:")))
// 阶段1:Entity实体类策略配置
.entityBuilder()
// 设置父类。会在生成的实体类名后:extends BaseEntity
// .superClass(BaseEntity.class)
// 禁用生成 serialVersionUID。(不推荐禁用)
// .disableSerialVersionUID()
// 开启生成字段常量。
// 会在实体类末尾生成一系列 [public static final String NICKNAME = "nickname";] 的语句。(一般在写wapper时,会用到)
// .enableColumnConstant()
// 开启链式模型。
// 会在实体类前添加 [@Accessors(chain = true)] 注解。用法如 [User user=new User().setAge(31).setName("snzl");](这是Lombok的注解,需要添加Lombok依赖)
// .enableChainModel()
// 开启 lombok 模型。
// 会在实体类前添加 [@Getter] 和 [@Setter] 注解。(这是Lombok的注解,需要添加Lombok依赖)
// .enableLombok()
// 开启 Boolean 类型字段移除 is 前缀。
// .enableRemoveIsPrefix()
// 开启生成实体时生成字段注解。
// 会在实体类的属性前,添加[@TableField("nickname")]
.enableTableFieldAnnotation()
// 逻辑删除字段名(数据库)。
.logicDeleteColumnName("deleted")
// 逻辑删除属性名(实体)。
// 会在实体类的该字段属性前加注解[@TableLogic]
.logicDeletePropertyName("deleted")
// 数据库表映射到实体的命名策略(默认下划线转驼峰)。一般不用设置
// .naming(NamingStrategy.underline_to_camel)
// 数据库表字段映射到实体的命名策略(默认为 null,未指定按照 naming 执行)。一般不用设置
// .columnNaming(NamingStrategy.underline_to_camel)
// 添加父类公共字段。
// 这些字段不会出现在新增的实体类中。
.addSuperEntityColumns("id", "delete_time")
// 添加忽略字段。
// 这些字段不会出现在新增的实体类中。
// .addIgnoreColumns("password")
// 添加表字段填充
// 会在实体类的该字段上追加注解[@TableField(value = "create_time", fill = FieldFill.INSERT)]
.addTableFills(new Column("create_time", FieldFill.INSERT))
// 会在实体类的该字段上追加注解[@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)]
.addTableFills(new Column("update_time", FieldFill.INSERT_UPDATE))
// 全局主键类型。如果MySQL主键设置为自增,则不需要设置此项。
// .idType(IdType.AUTO)
// 格式化文件名称。
// 如果不设置,如表[sys_user]的实体类名是[SysUser]。设置成下面这样,将是[SysUserEntity]
// .formatFileName("%sEntity")
// 阶段2:Mapper策略配置
.mapperBuilder()
// 设置父类
// 会在mapper接口方法集成[extends BaseMapper<XXXEntity>]
// .superClass(BaseMapper.class)
// 开启 @Mapper 注解。
// 会在mapper接口上添加注解[@Mapper]
.enableMapperAnnotation()
// 启用 BaseResultMap 生成。
// 会在mapper.xml文件生成[通用查询映射结果]配置。
.enableBaseResultMap()
// 启用 BaseColumnList。
// 会在mapper.xml文件生成[通用查询结果列 ]配置
.enableBaseColumnList()
// 设置缓存实现类
// .cache(MyMapperCache.class)
// 格式化 mapper 文件名称。
// 如果不设置,如表[sys_user],默认的文件名是[SysUserMapper]。写成下面这种形式后,将变成[SysUserDao]。
// .formatMapperFileName("%sDao")
// 格式化 xml 实现类文件名称。
// 如果不设置,如表[sys_user],默认的文件名是[SysUserMapper.xml],写成下面这种形式后,将变成[SysUserXml.xml]。
// .formatXmlFileName("%sXml")
// 阶段3:Service策略配置
// .serviceBuilder()
// 设置 service 接口父类
// .superServiceClass(BaseService.class)
// 设置 service 实现类父类
// .superServiceImplClass(BaseServiceImpl.class)
// 格式化 service 接口文件名称
// 如果不设置,如表[sys_user],默认的是[ISysUserService]。写成下面这种形式后,将变成[SysUserService]。
// .formatServiceFileName("%sService")
// 格式化 service 实现类文件名称
// 如果不设置,如表[sys_user],默认的是[SysUserServiceImpl]。
// .formatServiceImplFileName("%sServiceImpl")
// 阶段4:Controller策略配置
.controllerBuilder()
// 设置父类。
// 会集成此父类。
// .superClass(BaseController.class)
// 开启生成 @RestController 控制器
// 会在控制类中加[@RestController]注解。
.enableRestStyle()
// 开启驼峰转连字符
.enableHyphenStyle()
// 最后:构建
.build()
)
//模板引擎配置,默认 Velocity 可选模板引擎 Beetl 或 Freemarker
//.templateEngine(new BeetlTemplateEngine())
.templateEngine(new FreemarkerTemplateEngine())
// 执行
.execute();
}
// 处理 all 情况
protected static List<String> getTables(String tables) {
return "all".equals(tables) ? Collections.emptyList() : Arrays.asList(tables.split(","));
}
/**
* <p>
* 读取控制台内容
* </p>
*/
private static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入").append(tip).append(":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotBlank(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
}
十、上传Git
Git使用详解全图文(基于gitee),看这一篇就够了_我认不到你的博客-CSDN博客_git的使用
.gitignore文件
# Compiled class file
*.class
# Eclipse
.project
.classpath
.settings/
# Intellij
*.ipr
*.iml
*.iws
.idea/
# Maven
target/
# Gradle
build
.gradle
# Log file
*.log
log/
# out
**/out/
# Mac
.DS_Store
# others
*.jar
*.war
*.zip
*.tar
*.tar.gz
*.pid
*.orig
temp/
十一、Swagger+knife4j
Swagger3+knife4j的使用_我认不到你的博客-CSDN博客
十二、MinIO
十三、运维:Jenkins+Docker(Docker基本没怎么讲,主要是Jenkins)
Linux安装jenkins_我认不到你的博客-CSDN博客
jenkins使用docker安装保姆级教程(面向小白教程,最新最全,全图文)2022-8-1,不会docker也没关系
jenkins流水线(jenkinsfile)详解,保姆式教程_我认不到你的博客-CSDN博客
jenkins pipline 拉取git历史版本_我认不到你的博客-CSDN博客
十四、操作Excel
JAVA操作Excel(POI、easyPOI、easyExcel)
十五、Nginx
Windows安装Nginx并配置负载均衡_windows负载均衡软件_我认不到你的博客-CSDN博客
十六、JMeter
jmeter安装与使用,全图文讲解_我认不到你的博客-CSDN博客