核心价值就是把现实世界的业务操作搬到计算机上,通过计算机软件和网络进行业务和数据处理,但是时至今日,能用计算机软件提高效率的地方,几乎已经被全部发掘过了,必须能够发掘出用户自己都没有发现的需求,必须洞悉用户自己都不了解的自己。于是出现了:
大数据 + 机器学习”
本文致力于解决日常遇见的坑和一些个人经验,解决不必要的坑 耽误开发效
目录
1、 技巧:向spring注入对象:@configuration 的使用:
2、DB2分页查询
3、多线程的快捷写法
注意的点:实现多线程的两种方式:
线程涉及到的函数:
4、同样的项目,同样的数据库,但拷贝到另一个电脑,登录失败
5、上传文件、下载文件
6、关键词:ArrayList toString() 空格 容易踩的坑
7、Entity转换Vo对象 技巧。po转vo
8、sql查询玄学。关键词:sql limit hive
9、前端传后端数据。错误 400,排除技巧
情况一:前端传json格式的数据
情况二:前端传get请求携带的参数
10、排查工具的使用:Arthas、jvm总结
11、json转java对象 技巧
12、加空字符串的妙用 "对象"+""
13、输出数组 技巧。
14、界面点击按钮不生效,往往是js文件加载错,文件名写错导致的
15、项目本地访问页面正常,部署到服务器上访问不到。
16、应用部署 tomcat部署 和Liberty部署
17、数据库查询 和 hibernate查询的 条数一样,但部分行不一样。
18、linux上查找应用程序所在的 文件夹
19、Idea在debug模式下,停止当前函数(不执行断点后的代码)
20、相同项目代码,运行在两个环境,一个正常执行,一个等待很久才报错
21、tomcat部署后新war包后,重启后代码似乎没生效
22、java解析json中某个字段
23、SpringBoot的启动原理
24、mybatis#{}和${}的区别
两者的区别
25、编写单元测试,@Autowired注入的对象为null问题。
26、动态加载配置文件
27、接口开发 、其他系统的接口请求、postman请求可以,java请求有问题。
28、linux查看日志小技巧。
29、SpringBoot配置文件中的值读取、@values注解
30、开发心得、良好的习惯、易维护的代码。
31、部署jar包应用的偷懒技巧。
32、idea必备快捷键
33、guava学习和使用
34、不常见,但很有用的注解
35、PostMan使用技巧
36、mysql 分页需要oder by 吗?需要
36、java CURD mysql时 字段带"-",即短横问题
37、Git命令:
38、Collectors.toMap()、XX.stream().flatMap()的使用
39、myabtis报错:没有XX字段
40、mysql 表被锁
41、事务注解不生效:
42、jackson 对象转换成字符串,日期问题
43、ThreadLocal修饰符的作用:
44、sql为某字段设置默认值、修改字段类型、sql自动设置更新时间
45、java调用python模型、服务
46、springBoot配置主从数据的配置文件:
47、sprignboot 配置nacos
48、通过断点调试-查看数据源 (借助mybatis的mapper)
49、任务调度xxj-box:
步骤1:maven依赖:
步骤2:bootstrap.properties
步骤3:配置类bean注入
步骤4:使用样例:
步骤5:登录xxl-job的控制台去查看:
步骤5:在 任务调度中心 配置我们的这个执行器 上面时候执行,执行时携带什么参数:
50、springBoot加载配置文件相关问题
51、springboot 日志只输出 springboot和mybatis的的logo
52、观察接口报警的工具-可视化工具
53、工具-将大文件划分成小文件
54、springBoot记录日志
55、springboot 配置ncoas,指定配置不生效
56、谷歌API、aws亚马逊API
1、谷歌翻译
2、谷歌认证实现
aws亚马逊api调用技巧:
59、get请求,下划线请求参数封装进对象
57、查询日志es的kibaba
58、命令行查看本地Maven仓库地址 linux环境下:
59、springboot加载resource文件夹下 的自定义文件
60、jar 包在后台运行
61、架构师的素养
62、redis技巧、缩短key长度
63、对象集合list 存成csv文件
64、查询单表因数据量较大,导致sql耗时太久而失败。
65、如何生成优雅的listbug:编辑
66、JSON与对象转换的坑
67、列表分批处理小技巧
68、限流器 RateLimiter
69、在idea排查CPU、内存OOM的方法
70 、记录一次OOM事故
71、通用的简易网络客户端模板
(关键词:多数据源 jdbcTemplate @configuration)
场景:一个项目有多个数据源时,xml里配置了多个数据源。通过@configuration 的使用,其他类直接以 @Authoried方法注入到某类中,进行引用。
1、spring的配置xml文件(即是 web.xml中配置的spring配置文件的那个文件)如:
。。。。。
//一个数据库连接池 db2数据库
.....//略 具体的数据连接池的细节配置
//另一个一个数据库连接池 mysql数据库
.....//略 具体的数据连接池的细节配置
。。。。。
2、使用@configuration注解注入到spring容器中,这样在启动项目的时候,spring会自动将DB2JdbcTemplat、mysqlJdbcTemplate 注入到容器中,示例如下:
@Configuration
public class JdbcTemplateConfig {
/ *Bean 用于把当前方法的返回值作为对象存入spring的ioc容器中
属性:name 作用:用于指定baan的id,默认值为方法名 */
@Bean(name ="DB2JdbcTemplate")
public JdbcTemplate myBean(@Qualifier("dataSouce1")DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean(name ="mySqlJdbcTemplate")
public JdbcTemplate myBean(@Qualifier("dataSouce2")DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
3、使用注入的数据源 应用示例:
@Repository
public class myDaoImpl{
@Autowired
@Qualifier("mySqlJdbcTemplate")
privare JdbcTemplate mySqlJDBC;
@Autowired
@Qualifier("db2JdbcTemplate")
privare JdbcTemplate db2JDBC;
//查询示例
public List findUser(String name){
//此sql是原生的sql,即可以在数据库执行的sql
String sql = "select * from user where name=?"
// name处 那个参数 其实是可变参数,一般把参数多的情况下放入到list中,使用list.toArray()
List list =db2JDBC.query(sql,new BeanPropertyRowMapper<>(User.class),name);
}
//插入示例
public void create(final String name,final int age){
String sql="insert into user(id,name,age) value(?,?,?)";
db2JDBC.update(sql,new PreparedStatementSetter(){
@Override
public void setValues(PreparedStatement ps)throw SQLException{
ps.setString(1,UUIDUtil.genericUUID());
ps.setString(2,name);
ps.setInt(3,age);
}
})
//删除示例
public void deleteData(String id){
String sql="delete user where id=?";
db2JDBC.update(sql,id);
}
}
......
具体函数中 使用jdbcTemplate的方法即可:
常用的有:(技巧:可以 在sql对每个字段进行 参考 XXEntity属性 进行取别名 就可以直接映射进去了 如 select aa student_age from XX 解释: 数据库aa表示学生年龄的字段,实体属性为 studentAge)
1、query(将参数都拼接到字符串的sql语句, new BeanPropertyRowMapper(XXEntity.class))
2、query(?代替参数sql语句, new BeanPropertyRowMapper(XXEntity.class),(参数放入到)数组)
}
Spring @Configuration 注解介绍 - 简书
@Configuration
public class AppConfig {
@Bean
public MyBean myBean() {
// instantiate, configure and return bean ...
}
}
公司使用的数据库是db2,不像mysql那样 可以使用 limit 0,10 这样的语法。
所以需要使用:给查询出的结果加 一列“序号”,再对序号进行区间选取:
select * from (
select row_number() over( order by XXX) as rownum, a.*,b.* from tableA left join tableB on
tableA.id =tableB.id where 1=1 XXXXX拼接条件
)where rownum>? and rownum<=?
注意:
1、row_number() over(这里一定要排序否则每次查出来的顺序可能都在变化,导致每页可能存在重复的数据)。
2、 where rownum>? and rownum<=? 一定是单独存在的一个条件,切勿拼接到 条件组合的where中,否则会导致 有时候查出来的数据也正常,有时候不正常,why?因为对查询的结果进行排序,同时还对行号进行筛选,就会导致筛选出的结果的序号 可能不是连续的 。
3、记得,db2 分页是取 区间。而支持limit的数据库 limit 起始位置,每页取的个数。 别记混了。
4、经验:能在java处理的尽量用程序处理,数据库关联2张表,再多就不太好了,应有java小批量查询处理的思想。
使用lamba表达式:(不必再去单独写继承runable的线程类,再重写run方法,现在直接通过:向Thread类传入 ()->{ 要运行的程序},线程名称 即可)
package com.alipay.sofa.boot.examples.demo;
public class MultiThread1 {
public static void main(String[] args) {
// lamba表达式 ()->创建对象的意思
new Thread(() -> {
fun("线程11111");
},"线程11"
).start();
new Thread(() -> {
fun("线程2222222222");
},"线程22"
).start();
}
static void fun(String threadName){
for (int i = 0; i < 1000; i++) {
System.out.println("执行"+threadName);
}
}
}
原始的写法:
1、实现runable接口:
package com.alipay.sofa.boot.examples.demo;
public class MultiThread {
static class MyThread implements Runnable {
private String threadName;
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("执行" + threadName);
}
}
public MyThread(String threadName) {
this.threadName = threadName;
}
}
public static void main(String[] args) {
MyThread myThread1= new MyThread("线程111");
MyThread myThread2= new MyThread("线程22");
// 需要首先实例化一个 Thread,并传入自己的 MyThread
Thread thread1 = new Thread(myThread1);
Thread thread2 = new Thread(myThread2);
thread1.start();
thread2.strat();
}
}
2、使用继承Thread的方式实现:
package com.alipay.sofa.boot.examples.demo;
public class MultiThread2 {
static class MyThread extends Thread {
private String threadName;
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("执行" + threadName);
}
}
public MyThread(String threadName) {
this.threadName = threadName;
}
}
public static void main(String[] args) {
MyThread myThread1= new MyThread("线程111");
MyThread myThread2= new MyThread("线程22");
//继承Thread不用再放入到Thread中
myThread1.start();
myThread2.start();
}
}
①类A继承Thread类,重写run方法,调用时直接 new A().strat() 即可
②类A实现 Runable接口,实现run方法,调用时 需要把A传入Thread类中。如: new Thread(new A()).strat()
③lamba表达式: new Thread(()->{ 线程执行的语句 }).start()
Thread t1=new Thread(()->{
语句块;
}) ;
t1.join();
Thread t2=new Thread(()->{
语句块;
}).start() ;
在t2线程里调用t1,表示 t1执行完成 才执行t2
一、wait()和sleep()的区别
1、wait()是object类的方法,sleep()是Thread类的方法,
2、wait()使线程进入等待状态,并且释放了锁,使得其他线程能使用同步控制块或方法,需要使用notify()或notifyAll()方法来唤醒线程,进入就绪状态,再去抢占cpu资源;
sleep()方法是线程进入睡眠状态,不释放锁;在睡眠时间用完后或使用interrupt()方法中断,线程唤醒进入进入就绪状态;(sleep()方法只是让出CPU,并不会让出同步资源锁)
3、sleep()方法必须捕获异常,wait()、notify()、notifyAll()方法同样必须需要捕获异常;
扩展:FutureTask的使用
上面提到的都是 基本线程的用户,实际开发过程中都是使用线程池,一般使用如下:
public void startTask() {
List siteCampinArnList = this.getSiteCampinArn();
List> taskList = new ArrayList<>();
for (String site : siteCampinArnList) {
FutureTask task = new FutureTask(new Callable() {
@Override
public String call() throws Exception {
//todo 要运行的函数内容
return site+"成功";
}
});
executorService.execute(task); //这里是 线程池 通过bean注入
taskList.add(task);
}
}
///
线程池的使用
@Bean
public ExecutorService executorService(){
ThreadFactory springThreadFactory = new CustomizableThreadFactory("openserch-pool-");
return new ThreadPoolExecutor(8, 8, 1,
TimeUnit.MINUTES,
new java.util.concurrent.LinkedBlockingQueue(),
springThreadFactory);
}
扩展:如何让线程按顺序输出?
1、使用线程池,该线程池只有一个线程,每个任务用submit提交到线程池。
2、使用join() 每个线程,调用自己的join()函数。本质上是,主线程等待子线程完成,再顺序执行余下的线程。注意的是:
//这样是无效的, 调用start() 就意味着 这个线程可能已经在执行
thread1.start();
thread2.start();
thread3.start();
//再调用join() 已经无用了
thread1.join();
thread2.join();
thread3.join();
//正确的写法是:
thread1.start();
thread1.join();
thread2.start();
thread2.join();
thread3.start();
场景:新人入场,我拷贝了一份旧代码作为练习项目发给他,结果我的电脑可以运行,可以登录进系统。他的电脑却死活登录不进去。
原因:maven部分依赖的jar包未导入项目,导致登录失败。
注意:把自己的maven拷贝到新人电脑上,他的maven不一定能下载下来jar,原因是 可能你1年前下载的,公司私服上已经删除了 但是你本地仓库已经下载了,所以你的项目并不缺少,但是新人使用你的项目,pom文件依赖的jar 可能在远程仓库上不存在了。
扩展:maven知识:
maven的配置文件:setting.xml
1.profile
profiles下面可以配置多个repositories,用profile下不同的id进行区分。当不设置activeProfiles时,配置了多个profile时,默认是都有效,会依次进行尝试下载。
2.mirrors:要去下载jar包的地址
mirror是仓库的镜像备份,通过mirrorOf配置来拦截对应的repositories,想要拦截特定的repositories,就在mirrorOf配置上repository的id进行拦截,也可以配置*来拦截所有仓库,在不配置仓库时默认的仓库id为central。
3.server :认证时用,用于私服认证
当仓库需要认证时,需要配置,server的id需要与repositories保持一致生效。很多大佬说server可以和pom文件里的repositories可以一起使用,不清楚能否和settings里的repositories一起使用。
额外的:有的项目需要依赖自己的jar或者模块,所以运行项目时,先lifecycle中先mvn install一下。
前端:
Title
下载
上传文件:
package com.alipay.sofa.boot.examples.demo.controller;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@Controller
public class DownFile {
@RequestMapping("/index")
public String index(HttpServletResponse response) throws IOException {
return "index1.html";
}
@RequestMapping("/uploadFileTest")
public void uploadFileTest(Model model, @RequestParam(value = "file", required = false) MultipartFile file) throws IOException {
if (!file.isEmpty()) {
byte[] buffer = new byte[1024 * 1024];
int byteread = 0;
FileOutputStream fs = new FileOutputStream("C:/Users/wjw\\Documents\\资料\\fileName");
fs.write(buffer, 0, byteread);
} else {
model.addAttribute("result", "上传失败");
return;
}
}
}
下载文件所在的目录:
下载文件代码:
package com.alipay.sofa.boot.examples.demo.controller;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
@Controller
public class DownFile {
@RequestMapping("/donwFileTest")
public void downFile(HttpServletResponse response) throws IOException{
//该文件存在项目 resources文件夹下:
String fileName="file.txt";
//设置强制下载不打卡
response.setContentType("application/force-download");
response.addHeader("Content-Disposition","attachment;fileName="+fileName);
//读取文件
ClassPathResource cpr = new ClassPathResource(""+fileName); //路径从resources下开始
InputStream is=cpr.getInputStream();
IOUtils.copy(is,response.getOutputStream());
response.flushBuffer();
//关闭流
if(is!=null){
is.close();
}
}
}
场景:有时候想要使用获取list的元素拼接成字符串,于是:
list.toString().replace("[",").replace("]","");
问题:这种拼接 会在每个元素前有一个逗号和空格,后面再使用此字符串会出现问题。
解决:推荐使用原生自带的拼接
String str = String.join(",",myList);
后台查出实体对象,往往需要转换成Vo对象,向前台展示,实际开发中,很多时候 Vo的字段与实体对象的字段名是有重复的,如果遍历 List
//spring提供的工具类
for(User user:userList){
BeanUtils.copyproperties(user,new UserVo());
}
注: 需要实体的字段、类型 和Vo的字段、类型都一致 才能设置进去,否则设不进去。对应字段不一样的 可以手动set
经验:此方法对后续维护其实不是很方便
场景:查询hive表。使用limit n,m 进行分页。得到的数据不符合预期;具体原因:
user表中符合王二的数据有100+条,如下sql,但取出来的数据 只有18条。和预期20条不符合。
select * from user where name like '王二%' limit 0,20 //取前20条数据
原因:移除关键字查询 查询结果符合预期:查出来了20条数据
select * from user limit 0,20 //取前20条数据
分析:既然移除关键字的拼接 就能正常查询,不移除就不行。那应该是这个关键字有问题。
后来发现 hive 一般是数据文件或者解析导入的数据,存在某些字段的数据可能有空格(坑啊)。
所以对应hive表的关键字查询 需要对关键字段名加trim(). 如下:
select * from user where name like '王二%' limit 0,20 //取前20条数据 原先的如果user某行name列存在 “ 王二” 这时候就查不出来。
所以对应hive表 关键字查找的 均一股脑的:(只适用于字符串 类型的,日期类型的不可以加)
select * from user where trim(name) like '王二%' limit 0,20
注:trim(表字段XX),表示对该列 XX的数据均去空格处理
场景:前端向后台传参数,后台用对象接收,报错400。
原因:一般是因为前端传参不符合 接收对象的属性。即:不匹配。但是传参多时,一个一个核对很耗时,也不一定能核对出来原因。
技巧: 在后台参数列表里,多加一个:BindingResult ,可以查看具体不匹配的字段。
public String list(BindingResult b,Model model, User user){
}
后端:使用 @RequestBody注解 标记在 依据json对象转的java对象 即可
后端:使用@RequestParam注解 可以指定参数绑定到当前参数,并可设置默认值
@RequestParam 可以用来接收Get POST类型的请求
@RequestBody 可以用来接收POST请求json格式的参数
调优的目的:为了减少SWT,即:stop world time。 每次发生full gc 都会造成系统的卡顿,所以要尽量避免,full gc.
一般的性能调优都是jstack,但比较好用的是阿里巴巴开发的 Arthas 性能调优非常强大,
官方文档:快速入门 — Arthas 3.5.4 文档
使用方法:
1)、下载jar包
2)、 运行jar包
java -jar arthas-boot-jar
3、输入 你想要查看 某jar应用程序的 序号,再回车,会出现:
4、比如上图 发现 ID 为8的线程占用cpu 88%,所以现在查看该线程,使用命令 thread 8 即可打印出现移除的源代码:
2、常遇到的问题:系统应用频繁的full gc,导致用户体验差。
分析:性能调优,不得不先了解jvm的内存模块:
①堆:
标记整理算法:是老年代的
问题1:为什么堆内存要分为新生代和老年代?
答:因为JAVA对象90%以上的对象都是朝生夕死的,其中GC回收的成本很高,为了提高性能所以将新生成的对象放在Eden区,将扛过多次GC的“老家伙”放在老年代
问题2:那为什么新生代还需要继续细分?
答:因为Eden区的绝大部分对象寿命很短,那么Eden每次满了清理垃圾,存活的对象被迁移到老年区,老年区满了,就会触发Full GC,Full GC是非常耗时的,设立s区的一个目的就是在Eden区和老年代中增加一个缓冲池,放一些“年纪不够老”的对象,增加垃圾回收性能
问题3:触发GC的流程
答:GC分为 minor GC 和Full G
minor GC: 新生对象都会放在eden区,当eden区放满,触发minor gc,将eden区还存活的对象age+1放到s0区,然后清空 enden区和s1区,然后继续。。eden区再满时,将eden还存活的对象 和s1区还存活的对象age+1,然后放到s0区,再清空eden区和s`。。。(期间age>15的对象就会被放入到老年代,大对象(占from(to)区内存空间的一半以上都算大对象)也会放入到老年代)就这样周而复始,直至某一次 enden区的对象+ to/from区的对象移到 from/to区时,放不下了,就会把对象都放入到老年代。清空新生代,继续执行上述。
Full GC:最终老年代越放越多,直至放不下,就会触发full gc。
minor GC的S0和S1区的设置为了解决复制算法的碎片化
所以,通过上述会发现,频繁造成full gc的原因是因为:新生代区总是放不下,频繁地向老年代放对象导致的,所以如果我们增大 新生代区的内存空间,新生代区的空间 大到基本能够支撑 业务线程执行完后的下一个周期的时间间隔((因为线程执行完,局部变量会立刻被回收,而该线程中对象并不会立刻回收,而是要等到一次 gc,gc发现这些对象被root引用,就视为垃圾才回收)。该回收的对象也基本都会在minor gc 就给回收掉了,所以很少有对象能够放到 老年代,自然也会导致full gc的频率下降!!。
所以解决的办法是: 把 新生代 和老年代 默认的比例1:2 改大一点,根据业务线程评估会new对象,基本是按一个对象1kb累计,最后评估的结果再放大10-1000倍,就是一次线程执行完一次应该占用的 内存,在知道新生代和老年代占空间的大小下,就可以粗略算出minor gc 和 full gc 触发的频率。
具体场景:假如堆的空间有3G,则新生代1G,老年代1G,enden取800M,to 区100M,from区 100M,假如订单系统,每秒会有400个请求,一个订单系统线程可能创建10kb的空间占用,所以每秒会在eden区生成:400*10kb/1024~=4M, 为了避免没考虑到的对象,所以放大10倍,所以 每秒有 40M存放到enden区,所以大约 800M/40M=20s 就会造成一次 minor gc ,或者说20s后这些占40M空间的基本会被gc回收掉(因为一两秒之后 这40M很有可能变成垃圾了,因为线程(不是复杂的业务线程 基本都能很看执行结束)可能已经结束了,属于该线程的局部变量已经被回收了,但该线程的创建的对象也失去了root引用,就等gc执行时把他给回收了,所以 这40M的(存储对象的空间)就应该被回收了)但有可能个别线程因某个原因执行的稍稍慢一点点,eden回收了大部分的空间,如19个40M,但还有一个40M正在被线程持有,这个40M就该移动到to/from区了。但40M (可能大于to/from区的一半则)视为大对象,就会放入到了老年代。频繁导致full gc的原因就在这里!!
总结:几分钟、几小时就触发了full gc。原因是因为: 业务线程引用的对象所占的空间 大于或等于了 s0/s1区 的一半,容易被直接放入老年代。
解决方法: 调整 新生代、老年代的比例,以及 eden 、s0、s1区的比例!。
②方法区(元空间)
存放类编译后的信息,如静态变量、类的属性
③程序计算器
④虚拟机栈
详细可参考:虚拟机栈的栈帧都包含些什么?_yozzs的博客-CSDN博客_虚拟机栈存放什么数据
① 局部变量表(因为局部变量属于函数嘛,都是临时工 要在这里登机一下咯)
② 操作数栈(从局部变量表里取 某变量,在这里进行 加减乘除的运算)
③ 动态链接(理解不够深刻):将符号变量替换成直接引用
④ 方法出口.(计算完结果 把结果返回到 上一个压入栈的 帧的地址,也可能计算途中遇到异常,所以可能返回 异常出口的地址、或者 方法正常执行完成的 返回的地址)。
⑤本地方法栈
和虚拟机栈几乎一样,只不过这里调用的都是 native方法 都是执行c++或者c方法。
(135条消息) 五位卷王 | 总结的十道 JVM 面试真题!(建议收藏)_hzbooks的博客-CSDN博客
使用alibaba的fastjson 即可:
JSONbject.toJavaObject(XXX,XXXX.class);//参数1是JSON对象 参数2是要封装进去的对象类文件
//单独使用
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
List rootProducts = objectMapper.readValue(data, new TypeReference<>() {});
个人倾向使用fastJson但的确存在不少未知bug,踩坑到哭
使用jackjson的 工具类(推荐使用)
fastjson的坑:is开头接收boolean类型的会解析不到!
(对data好像也解析不到,未测试,但见有此现象)
@Slf4j
public class JsonUtil {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
static{
//对不存在的数据字段 解析不到不报错
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
public static String getValue(String content, String key) {
String value = "";
try {
JsonNode jsonNode = OBJECT_MAPPER.readTree(content);
jsonNode = jsonNode.get(key);
if (Objects.nonNull(jsonNode)) {
value = jsonNode.toString();
}
} catch (Exception e) {
log.error("get value from json failed! key[" + key + "].", e);
}
return value;
}
/**
* 对象转json字符串
*
* @param object
* @return
*/
public static String convertJson(Object object) {
try {
return OBJECT_MAPPER.writeValueAsString(object);
} catch (JsonProcessingException e) {
log.error("get json from value failed! value[" + object.toString() + "].");
return "";
}
}
/**
* @param content json字符串
* @param valueType class
* @param 泛型
* @return 该类型
*/
public static T convertObject(String content, Class valueType) {
try {
return OBJECT_MAPPER.readValue(content,valueType);
} catch (JsonProcessingException e) {
log.error("get object from json failed! json[" + content + "].",e);
return null;
}
}
}
使用方法:
json转对象
注:有时候要解析比较特别的json字符串:如下:(即:带转义的字符串型json)
"{\"platform_id\":\"10000\",\"site_id\":\"20000\",\"store_id\":30000,\"datas\":[{\"product\":{\"product_id\":\"5203225\",\"app_id\":\"10000\",\"product_code\":\"17288278\",\"name\":\"hbdtgsbhg\",\"description\":\"dgfsbhfds\",\"category_id\":\"3105\",\"front_category_id\":null,\"price\":\"24.99\",\"msrp\":\"66.99\",\"middle_east_price\":\"30.99\",\"id\":\"5203225\",\"operator_id\":0,\"icon\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993cf2ce6e2.jpg\",\"group_id\":2065087,\"image_id\":3683062},\"skus\":[{\"id\":19700443,\"product_id\":\"5203225\",\"code\":\"CN-1075-5203225-19700443\",\"sku_code\":1728827801,\"price\":\"24.99\",\"msrp\":\"66.99\",\"icon\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993cf2ce6e2.jpg\",\"image_id\":3683062,\"group_id\":2065087},{\"id\":19700444,\"product_id\":\"5203225\",\"code\":\"CN-1075-5203225-19700444\",\"sku_code\":1728827802,\"price\":\"24.99\",\"msrp\":\"66.99\",\"icon\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993cf2ce6e2.jpg\",\"image_id\":3683062,\"group_id\":2065087},{\"id\":19700445,\"product_id\":\"5203225\",\"code\":\"CN-1075-5203225-19700445\",\"sku_code\":1728827803,\"price\":\"24.99\",\"msrp\":\"66.99\",\"icon\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993d1ad6730.jpg\",\"image_id\":3683066,\"group_id\":2065087},{\"id\":19700446,\"product_id\":\"5203225\",\"code\":\"CN-1075-5203225-19700446\",\"sku_code\":1728827804,\"price\":\"24.99\",\"msrp\":\"66.99\",\"icon\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993d1ad6730.jpg\",\"image_id\":3683066,\"group_id\":2065087}],\"skuApp\":{\"1728827801\":{\"app_id\":\"10000\",\"sku_id\":19700443,\"sku_code\":1728827801,\"product_id\":\"5203225\",\"product_code\":\"17288278\",\"status\":2,\"color_scheme\":\"Beige\",\"created_at\":\"2020-10-28 11:24:53\"},\"1728827802\":{\"app_id\":\"10000\",\"sku_id\":19700444,\"sku_code\":1728827802,\"product_id\":\"5203225\",\"product_code\":\"17288278\",\"status\":2,\"color_scheme\":\"Beige\",\"created_at\":\"2020-10-28 11:24:53\"},\"1728827803\":{\"app_id\":\"10000\",\"sku_id\":19700445,\"sku_code\":1728827803,\"product_id\":\"5203225\",\"product_code\":\"17288278\",\"status\":2,\"color_scheme\":\"Grey\",\"created_at\":\"2020-10-28 11:24:53\"},\"1728827804\":{\"app_id\":\"10000\",\"sku_id\":19700446,\"sku_code\":1728827804,\"product_id\":\"5203225\",\"product_code\":\"17288278\",\"status\":2,\"color_scheme\":\"Grey\",\"created_at\":\"2020-10-28 11:24:53\"}},\"product_code\":\"17288278\",\"platform_id\":\"10000\",\"site_id\":\"20000\",\"store_id\":30000,\"translation\":[{\"id\":1244908,\"app_id\":10000,\"product_id\":5203225,\"product_code\":17288278,\"name\":\"hbdtgsbhg\",\"description\":\"dgfsbhfds\",\"language\":\"Russian\",\"status\":4,\"user_id\":0,\"created_at\":\"2020-10-28 11:25:16\",\"updated_at\":\"2020-10-28 11:25:16\"},{\"id\":1244907,\"app_id\":10000,\"product_id\":5203225,\"product_code\":17288278,\"name\":\"hbdtgsbhg\",\"description\":\"dgfsbhfds\",\"language\":\"Swedish\",\"status\":4,\"user_id\":0,\"created_at\":\"2020-10-28 11:25:13\",\"updated_at\":\"2020-10-28 11:25:13\"},{\"id\":1244906,\"app_id\":10000,\"product_id\":5203225,\"product_code\":17288278,\"name\":\"hbdtgsbhg\",\"description\":\"dgfsbhfds\",\"language\":\"Dutch\",\"status\":4,\"user_id\":0,\"created_at\":\"2020-10-28 11:25:11\",\"updated_at\":\"2020-10-28 11:25:11\"},{\"id\":1244905,\"app_id\":10000,\"product_id\":5203225,\"product_code\":17288278,\"name\":\"hbdtgsbhg\",\"description\":\"dgfsbhfds\",\"language\":\"Portugal\",\"status\":4,\"user_id\":0,\"created_at\":\"2020-10-28 11:25:09\",\"updated_at\":\"2020-10-28 11:25:09\"},{\"id\":1244904,\"app_id\":10000,\"product_id\":5203225,\"product_code\":17288278,\"name\":\"hbdtgsbhg\",\"description\":\"dgfsbhfds\",\"language\":\"ChineseTraditional\",\"status\":4,\"user_id\":0,\"created_at\":\"2020-10-28 11:25:07\",\"updated_at\":\"2020-10-28 11:25:07\"},{\"id\":1244903,\"app_id\":10000,\"product_id\":5203225,\"product_code\":17288278,\"name\":\"hbdtgsbhg\",\"description\":\"dgfsbhfds\",\"language\":\"German\",\"status\":4,\"user_id\":0,\"created_at\":\"2020-10-28 11:25:04\",\"updated_at\":\"2020-10-28 11:25:04\"},{\"id\":1244902,\"app_id\":10000,\"product_id\":5203225,\"product_code\":17288278,\"name\":\"hbdtgsbhg\",\"description\":\"dgfsbhfds\",\"language\":\"Italian\",\"status\":4,\"user_id\":0,\"created_at\":\"2020-10-28 11:25:02\",\"updated_at\":\"2020-10-28 11:25:02\"},{\"id\":1244901,\"app_id\":10000,\"product_id\":5203225,\"product_code\":17288278,\"name\":\"hbdtgsbhg\",\"description\":\"dgfsbhfds\",\"language\":\"French\",\"status\":4,\"user_id\":0,\"created_at\":\"2020-10-28 11:25:00\",\"updated_at\":\"2020-10-28 11:25:00\"},{\"id\":1244900,\"app_id\":10000,\"product_id\":5203225,\"product_code\":17288278,\"name\":\"hbdtgsbhg\",\"description\":\"dgfsbhfds\",\"language\":\"Spanish\",\"status\":4,\"user_id\":0,\"created_at\":\"2020-10-28 11:24:58\",\"updated_at\":\"2020-10-28 11:24:58\"},{\"id\":1244899,\"app_id\":10000,\"product_id\":5203225,\"product_code\":17288278,\"name\":\"hbdtgsbhg\",\"description\":\"dgfsbhfds\",\"language\":\"Arabic\",\"status\":4,\"user_id\":0,\"created_at\":\"2020-10-28 11:24:55\",\"updated_at\":\"2020-10-28 11:24:55\"}],\"operator_id\":0,\"id\":\"5203225\",\"sku_images\":[{\"product_id\":\"5203225\",\"sku_id\":19700443,\"src\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993cf087b8e.jpg\",\"sort\":0,\"template_no\":1,\"scale_type\":1,\"group_id\":2065087,\"image_id\":3683061},{\"product_id\":\"5203225\",\"sku_id\":19700443,\"src\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993cf2ce6e2.jpg\",\"sort\":1,\"template_no\":1,\"scale_type\":1,\"group_id\":2065087,\"image_id\":3683062},{\"product_id\":\"5203225\",\"sku_id\":19700443,\"src\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993cf394927.jpg\",\"sort\":2,\"template_no\":1,\"scale_type\":1,\"group_id\":2065087,\"image_id\":3683063},{\"product_id\":\"5203225\",\"sku_id\":19700443,\"src\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993cf523de2.jpg\",\"sort\":3,\"template_no\":1,\"scale_type\":1,\"group_id\":2065087,\"image_id\":3683064},{\"product_id\":\"5203225\",\"sku_id\":19700444,\"src\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993cf087b8e.jpg\",\"sort\":0,\"template_no\":1,\"scale_type\":1,\"group_id\":2065087,\"image_id\":3683061},{\"product_id\":\"5203225\",\"sku_id\":19700444,\"src\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993cf2ce6e2.jpg\",\"sort\":1,\"template_no\":1,\"scale_type\":1,\"group_id\":2065087,\"image_id\":3683062},{\"product_id\":\"5203225\",\"sku_id\":19700444,\"src\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993cf394927.jpg\",\"sort\":2,\"template_no\":1,\"scale_type\":1,\"group_id\":2065087,\"image_id\":3683063},{\"product_id\":\"5203225\",\"sku_id\":19700444,\"src\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993cf523de2.jpg\",\"sort\":3,\"template_no\":1,\"scale_type\":1,\"group_id\":2065087,\"image_id\":3683064},{\"product_id\":\"5203225\",\"sku_id\":19700445,\"src\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993d1999cc5.jpg\",\"sort\":0,\"template_no\":1,\"scale_type\":1,\"group_id\":2065087,\"image_id\":3683065},{\"product_id\":\"5203225\",\"sku_id\":19700445,\"src\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993d1ad6730.jpg\",\"sort\":1,\"template_no\":1,\"scale_type\":1,\"group_id\":2065087,\"image_id\":3683066},{\"product_id\":\"5203225\",\"sku_id\":19700445,\"src\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993d1d3a24b.jpg\",\"sort\":2,\"template_no\":1,\"scale_type\":1,\"group_id\":2065087,\"image_id\":3683067},{\"product_id\":\"5203225\",\"sku_id\":19700445,\"src\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993d1e3dc97.jpg\",\"sort\":3,\"template_no\":1,\"scale_type\":1,\"group_id\":2065087,\"image_id\":3683068},{\"product_id\":\"5203225\",\"sku_id\":19700446,\"src\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993d1999cc5.jpg\",\"sort\":0,\"template_no\":1,\"scale_type\":1,\"group_id\":2065087,\"image_id\":3683065},{\"product_id\":\"5203225\",\"sku_id\":19700446,\"src\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993d1ad6730.jpg\",\"sort\":1,\"template_no\":1,\"scale_type\":1,\"group_id\":2065087,\"image_id\":3683066},{\"product_id\":\"5203225\",\"sku_id\":19700446,\"src\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993d1d3a24b.jpg\",\"sort\":2,\"template_no\":1,\"scale_type\":1,\"group_id\":2065087,\"image_id\":3683067},{\"product_id\":\"5203225\",\"sku_id\":19700446,\"src\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993d1e3dc97.jpg\",\"sort\":3,\"template_no\":1,\"scale_type\":1,\"group_id\":2065087,\"image_id\":3683068}],\"product_icons\":[{\"product_id\":\"5203225\",\"scale_type\":1,\"template_no\":1,\"icon\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993cf2ce6e2.jpg\",\"group_id\":2065087,\"image_id\":3683062}],\"product_sku_icons\":[{\"sku_id\":19700443,\"sku_code\":1728827801,\"product_id\":\"5203225\",\"template_no\":1,\"icon\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993cf2ce6e2.jpg\",\"group_id\":2065087,\"image_id\":3683062},{\"sku_id\":19700444,\"sku_code\":1728827802,\"product_id\":\"5203225\",\"template_no\":1,\"icon\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993cf2ce6e2.jpg\",\"group_id\":2065087,\"image_id\":3683062},{\"sku_id\":19700445,\"sku_code\":1728827803,\"product_id\":\"5203225\",\"template_no\":1,\"icon\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993d1ad6730.jpg\",\"group_id\":2065087,\"image_id\":3683066},{\"sku_id\":19700446,\"sku_code\":1728827804,\"product_id\":\"5203225\",\"template_no\":1,\"icon\":\"http:\\\/\\\/aaawebstatic.s3.us-west-2.amazonaws.com\\\/origin\\\/product\\\/000000000000\\\/5f993d1ad6730.jpg\",\"group_id\":2065087,\"image_id\":3683066}]}]}"
这种不好剔除\(容易bug),但可以使用 readTree()
ObjectMapper mapper= new ObjectMapper();
//使用 readTree(String XX)方法 转换map 再转jsonNode 就不会带转义了
JsonNode dataNode= mapper.readTree(receiveMsg);//把数据receiveMsg转换成 jsonNode
JsonNode msgDataNode = dataNode.get("msg_data");//获取receiveMsg中的msg_data也作为节点
if (Objects.nonNull(msgDataNode) && msgDataNode.isTextual()) {
msgData = msgDataNode.asText(); //可以转换为 字符串了!!
}
try {
jsonNode = OBJECT_MAPPER.readTree(msgData);
} catch (Exception e) {
log.error("json read tree failed! json[" + msgData + "].", e);
return;
}
当你使用 对象.toString() 方法时,最好使用 +"" 方式 代替,因为 对象有可能是null,执行时会导致空指针异常。但是null对象+字符串=“null” 所以还要replaeAll("null","");
把数组放入到 ArrayList中
String data={1,2,3,4};
Arrays.toString(data);
虽然这个问题很low 但是使用idea,有时候使用Refaotr导致改文件名会不小心改动了其他的地方。
浏览器控制台:Failed to load resouce:the XXX.js server responesd with a status of 404 即战斗资源。
场景:使用springmvc查询数据并返回页面。本地运行正常,但在服务器部署出现如下错误:
报错日志:Could not resolve view with name 'XXXX' in servlet with name 'springmvc'
思路:发现 return "XXXX" 是 XXXX和实际的页面文件名称不一样,自然是找不到页面的了,可是为什么 本地可以 而服务器却不可以?难道是运行在tomcat上的项目 spring的视图解析器不区分大小写,而运行在liberty上区分大小写?
原因:只是指定servlet相对路径下的视图文件。所以是否敏感取决于servlet对文件名大小写是否敏感,或者说归根到底,取决于操作系统对文件名大小写是否敏感。
总结:无论是什么,最好return的页面和实际页面名称保持一致。莫粗心,慎用idea的查找替换。
1、tomcat部署 ,老生常谈,把应用程序的war包放到tomcat下的 webapps,然后去上级目录的bin目录下,运行 shutdown.sh 或startup.sh即可对应用进行停、启操作
2、(IBM公司开发的)liberty部署。特点:轻量级(占内存少)、动态(更新配置文件重启项目,自己会更新) 将war包放到 XXXXX/dropins 目录下即可,然后去 XXX/servers 下执行:(发现在任意目录下 也可以执行 下面语句,可能公司的服务器配了全局变量)
server start war包名称 //不带后缀 启动应用
server stop war包名称 //不带后缀 关闭应用
参考文章:liberty | 在IDEA整合Springboot与IBM liberty_Eshare分享-CSDN博客
场景:测试测试查询条件,我在数据库里造了几条不同的数据,只改动了部分列的值。
现象:数据库查的条数 和开发的系统查的条数一致,但内容却不一致。
原因:查询的该表未设置主键,而hibernate的实体类上指定了主键。于是在查询时候,hibernate会认为相同 id值的就是同一个对象,造成 行覆盖,出现了:明明数据库有这条数据,但在java查询查出来的还是 却没有这条数据.
基本思路:找到应用程序的pid,再根据pid找到文件夹
方法1:根据应用端口号查找pid,在根据pid查找位置
//步骤1: 根据端口号找到pid
netstat -nptl |grep 9088 //快速记忆法 nptl 你怕他了
会输出:tcp6 0 0 0::9088 :::* LISTEN 29998/java
//发现是 9088的端口 是由 pid为 29998的 进程在占用
//步骤2:根据pid找到 进程所在的目录 linux会为每个进程创建一个文件夹 在/proc目录下 pid作为文件夹名
cd /proc/29998
ls -al
会输出: 其中有:“cwd -> /webapps/wlp/usr/servers/ofp "
所以进程是在 /webapps/wlp/usr/servers/ofp 目录下
方法二:根据 ps命令查找pid,再根据pid查找位置。
ps -ef |grep ofp
//输出 几行有 ofp的 进行信息, 第一列就是pid
方法三:top -c命令
top -c // -c是显示 路径名称
场景:断点调试查看某个功能时,有时候只是为了看流程,但当其执行到操作数据库时,并不想真的去提交数据到数据库,这时候要么终止项目,再重启项目(效率低),要么使用这个方法:
参考博客:
Idea在debug模式下,直接停止程序(不执行断点后的代码)_云别-CSDN博客_idea结束debug
关键词:断点调试执行要等很久
场景:拷贝项目到本地,运行发现断点执行某句语句时,要等很久。
原因:一般情况是最后报 超时错误。(多翻一下报错日志,看看含time、socket字样),发现是 请求超时等问题,查看我这项目也没什么请求的啊。报错是执行查询hbase时报错的。
解决:项目使用的数据库地址是 映射的。即在host文件中配置了 具体的ip地址,而我的电脑host没有那个映射 自然 查询不到数据库导致的报错。坑爹项目!
(关键字:部署war包,像没部署一样、tomcat)
场景:新打的war包,修复了bug。部署到测试环境下,但修复bug仍就出现,本地却是好的。
原因:此刻tomcat正在运行旧的war包,这时新的war包放到tomcat时,我们会以为覆盖旧的war包。再重启tomcat就可以运行最新的war包了。但事实上是:当你把新war包覆盖旧war包的时候,这时tomcat会解压旧的war包,生成 rcp文件夹(我的是rcp.war)。每次启动tomcat都运行的是 rcp文件夹的代码,即 :运行的旧的代码。
解决:删除 tomcat解压生成的 同名项目 文件夹,即删除 rcp文件夹(我的项目名叫rcp)。再重启tomcat即可。
总结:先停服务(shutdown.sh),再更新war包,再启动服务(start.sh).
文章参考:Tomcat 何时解压war包 - fatsnake - 博客园
方法1:
json字符串: {"age":"10","name":"李四","address":{"area1":"萧山区","area2":"杭州市"} }
JSONObject jsonParam =JSONObject.parseObject(json的字符串);
//getString()方法 只解析 当前一层的 key:value。
String age = jsonParm.getString("age"); //age=10
String address= jsonParm.getString("address"); //address={"area1":"萧山区","area2":"杭州市"}
T object = JSONobject.toHavaObject(json字符串,T.class); //解析出T类 参数自动匹对
注:这些方法依赖fastjson包
方法2:(推荐)但需要引入
cn.hutool
hutool-all
5.7.4
import cn.hutool.json.JSONObject;
。。。。
public static void main(String[] args) {
JSONObject jsonObject = new JSONObject("{\"name\":\"李四\"}");
Object name = jsonObject.get("name");
System.out.println(name); //输出李四
}
知识点学习:
1、为什么使用springBoot?(以web项目为例)
①简化配置。否则要在web.xml中注册SpringMVC的DispatcherServlet
,拦截器、过滤器等
②统一管理各个组件依赖的jar包。手动维护免费、费事。
所以SpringBoot在此基础上,整合了一套快速开发的工具包。开箱即用,一行代码就能启动。
2、SpringBoot场景启动器starter。不同场景有对应的启动器,那么他的作用是什么呢?
starter的实现逻辑主要由两个基本部分组成:
xxxAutoConfiguration
:自动配置类,不同场景下,自动配置类的内容就不一样。对某个场景下需要使用到的一些组件进行自动注入,并利用xxxProperties类来进行组件相关配置。 如:spring-boot-starter-模块名
xxxProperties
:某个场景下所有可配置属性的集成,在配置文件中配置可以进行属性值的覆盖。
所以,引入starter后,springBoot就会在启动的时候帮我们完成相关的:自动配置、自动导入
3、SpringBoot的启动原理?
首先,查看SpringBootApplication注解结构:
SpringBoot在启动的时候从类路径下的(所有的)META-INF/spring.factories
中获取EnableAutoConfiguration指定的所有自动配置类的全限定类名
将这些自动配置类导入容器,自动配置类就生效,帮我们进行自动配置工作;
整个J2EE的整体解决方案和自动配置都在spring-boot-autoconfigure
的jar包中;
它会给容器中导入非常多的自动配置类 (xxxAutoConfiguration), 就是给容器中导入这个场景需要的所有组件,并配置好这些组件 ;
有了自动配置类,免去了我们手动编写配置注入功能组件等的工作
文章参考:图文并茂,Spring Boot Starter 万字详解!还有谁不会?
精通Spring?请吃我一狗腿!【文末送书】
扩展:@import注解,顾名思义,导入,即把类加入Spring IOC容器。
有多种方式能让类加IOC容器管理,如@Bean、@Component等,@Import是另外一种方式,更加快捷。
@Import支持 三种方式
1.带有@Configuration的配置类(4.2 版本之前只可以导入配置类,4.2版本之后 也可以导入 普通类) 加上@Configuration是为了能让Spring 扫描到这个类
2.ImportSelector 的实现
3.ImportBeanDefinitionRegistrar 的实现
扩展:
坑:有时候 偷懒 直接传参,如:String ids ="1,2,3" 字符串作为参数 在xml中拼接xsql时
select * from XXX_table in (#{ids}) 而没使用foreach进行拼接。
结果:不报错,但返回的数据只有一条
总结:偷懒的写法,日志打印 只有一个? 常规的写法,日志打印 ??? 3个占位符
文章参考:#{}和${}的区别_lt_zl的博客-CSDN博客
场景:自己编写了一个查询的query()函数(该函数所在类名:A),想在测试类中调用 该函数,因此,我写的测试类代码如下:
@RunWitg(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(class= 启动类.class)
public class MyTest{
//能注入成功,未运行时,一般会提示报错说找不到bean,不用管,运行后就不报错了
@Autowired
private QueryUser queryUser;
@Test
public void test(){
/*getUser方法 中 使用了注入sqlsession对象,这个是有值的*/
User user = queryUser.getUser();
}
@Test
public void test1(){
QueryUser queryUser1= new QueryUser();
/*getUser方法 中 使用了注入sqlsession对象,这个是有null的*/
User user = queryUser1.getUser();
}
}
对比 test和test1 可以发现,自己创建的对象queryUser1中注入其他对象,但事实上并没注入成功?为什么呢?
个人觉得,自己创建的对象 不受spring容器控制,和spring容器是两回事,所该对象引用的一些spring的对象,自然注入不进来。(该观点可能错误,未看源码的猜测)。
解决方法:使用自动注入的对象,可以使用spring容器中的主动注入对象。(即上述代码中test方法的使用)。
后续更新:
步骤1:创建一个基类。子类去继承这个基类,就可以了
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@SpringBootTest
@RunWith(SpringRunner.class)
public class BaseTests {
}
步骤2:
package com.11.search.service;
import com.11.search.BaseTests;
import com.11.search.entity.ProductIndexEs;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
//继承基类 顺便写
public class myServiceTest extends BaseTests {
@Autowired
ElasticsearchRestTemplate elasticsearchRestTemplate;
@Test
public void ss(){
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.withQuery(QueryBuilders.matchAllQuery());
NativeSearchQuery build = builder.build();
SearchHits search = elasticsearchRestTemplate.search(build, ProductIndexEs.class);
System.out.println("");
}
}
如果想让单元测试插入数据库则需要加上@Roolback(false),如下:
@Test
@Transactional(rollbackFor = Exception.class)
@Rollback(false) //不写的话,单元测试成功后也默认回滚
public void test() {
insertAll();
quertList();
System.out.println("hello");
}
运行中的项目,更新了配置文件,又不想重启,如何让程序自动更新加载这个文件呢?
首先,想到的是写个死循环,让这个线程一直去查看文件是否改变,改变则重新读取。但是浪费cpu资源啊。
个人想法(未实践):使用观察者模式,A改变状态,通知B去读取配置文件。即:运行的程序比如预留了一个controller的requestMapping,如:htttp://ip/item/myselfequest。当我们更新完服务器上的配置文件后,在服务器上请求一下预留的那根请求。curl "htttp://ip/item/myselfeques",通知B去重新读取配置文件。也就是手动触发。
场景:开发稍大的系统,都会遇到 A服务器上的项目,调用B服务器上的项目。A如何像postman一样去调用呢?
解决:
方法1: 使用 RestTemplate. 。常规方法
RestTemplate 用法详解_Lzc的博客-CSDN博客_resttemplate
源码中的默认设置的值ResrTemplate的默认请求头是:application/json
此方法:一般没什么问题,很方便,但有时候会因为请求头、协议一些问题 卡壳。。。
package com.接口请求;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class MyRestTemplete {
public static void main(String[] args) throws IOException {
RestTemplate restTemplate = new RestTemplate();
//
String url = "http://localhost:8080/test1";
Map map = new HashMap<>();
map.put("name","张三");
// Object object= restTemplate.getForEntity(url,Object.class,map);
Object object1= restTemplate.postForObject(url,map,Object.class);
}
}
文章参考:RestTemplate发送HTTP、HTTPS请求_JustryDeng-CSDN博客_resttemplate调用https接口
RestTemplate HttpMessageConverter报错的解决方案no suitable HttpMessageConverter - 未月廿三 - 博客园
此次记录一次使用RestTemplate发送 form-data的post请求:(其他几种方式没能成功)亲测可用。
public static XXX sendPostByFormData() {
//1 创建请求对象
RestTemplate restTemplate = new RestTemplate();
//2 设置请求头信息
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
//3 设置请求的参数
MultiValueMap params = new LinkedMultiValueMap<>(); //这里是导入 import org.springframework.util.LinkedMultiValueMap包下的
params.add("name","张三");
//4 将参数设置进 请求实体内
HttpEntity> httpEntity= new HttpEntity<>(params,headers);
//5 发起请求,拿到请求的结果 注:此次 最好使用String.class接收,之后再使用fastJson进行解析。因为如果 返回的json对象中含 数组类型的,即使我们使用List
失败的案例记录1://请求返回200 ok,但参数解析失败
public Object ss(){
Map paramMap = new HashMap<>();
paramMap.put("name","张三");
RestTemplate restTemplate = new RestTemplate();
String response =restTemplate.patchForObject("http://localhost:8080/test1",paramMap,String.class);
System.out.println( response);//请求返回200 ok,但参数解析失败
return response;
}
//失败案例2
好文参考学习 SpringBoot的HttpMessageConverter使用(1)RestTemplate中的应用 - 简书
方法2: 用postman生成代码,贴到代码中,会像在postman上测的那样。
使用postman自动生成java代码。理论上postman能够请求成功的接口,通过自动生成就能拷贝在代码中请求成功。
1/3点击code按钮,
2/3选择编程语言:
3/3 自己的项目依赖okhttp的jar包
com.squareup.okhttp3
okhttp
3.8.1
把代码贴到自己的项目中 就ok了! (注:导包时,都选okhttp的包)
此方法,目测是凡是 postman可以做的,都可以使用java来做。
请求接口中踩的坑:
1、要确认别人提供的接口版本是否最新,别人家修改了url,你还在傻傻用上一个版本的url,出现postman请求没问题,自己却请求接口不存在、或者解析参数错误 的这种。low坑
高效的查找命令,有助于快速有效定位问题。
grep命令
小技巧:有时候输出的日志密密麻麻,希望用颜色标识日志中匹配到的内容,方便聚焦。于是:
grep --color "要查找的内容" 文件名 //注:而不是 -color
默认匹配到的是棕红色
全局配置,避免每次输入--color参数,自动给grep加颜色:
[root@home]#vim ~/.bashrc //打开配置文件,添加 下面的语句 alias grep='grep --color' // 以后输入grep 就相当于输入了 grep --color //保存bashrc文件后,更新系统配置 [root@home]#source ~/.bashrc
惯例使用的 :
//查找 XXX.log的日志文件 只找 421723199606258699 的字符串
cat XXX.log | grep 421723199606258699
比较好用的写法是:
//grep 要查找的内容 文件名
//如: 查找当前 logs目录下的 所有的log中含 421723199606258699 的日志
grep 421723199606258699 ./logs/*.log
有时候我们输入一个关键字出现好多信息,比如把前几天的日志也给查找出来了。这时候我们可以只要后面最新的几条就好:
grep 张三 my.log | tail -n 2 //找到张三 并打印最后出现2次张三的行
awk命令
场景:想要查看某条日志B,前面 离A最近的某个特定日志A: 即 A日志,和B 日志 两个字符串中间还隔着 好多不需要的日志。
形如:(因为很多时候 B报错了,现在想看A传入的参数,所以B在错误的位置很好定位,但是A可能要翻很久)
//现在想获取 B 记录前的 A记录
log.info("A log....");
///中间还有很多 log.info
log.info("XXXX log....");
log.info("B log....");
那么可以使用命令:(基本思想,使用 先入后出的思想,使用tac反向查找达到找 最近出现的一行)
grep "B" /XXX/XXX/pathFile/XX.log //定位B
//步骤1:定位B, 向前查找10000行(最好看一下处理一次的流程大概要输出多少日志,这个值就取多少,一般100000行应该能包含了) 并把这部分日志 导入 到 temp.txt 文件中
grep -B 10000 "B" /XXX/XXX/pathFile/XX.log > temp.txt
//步骤2: 使用tac命令,从某文件的最后一行开始输出
tac temp.txt | grep "A"
场景:一般java取值从配置文件中取我们配好的值,但是避免有人误删了配置文件中的值,所以会在代码中也有默认值,如下写法:
即使配置文件和java中的值不一样,会先读取nacos里的,其次是配置文件里的,最后是默认值,如果没有则会报错。
因为其他地方想调时,对象类型限制了,且因为函数内部有使用 XXX= 对象.属性 这样的赋值,导致函数不能通用,所以如果参数不是很多的话,除了 controller层其他层传参最好使用 基本类型的方式。
此方式会很大程度上解决,很多函数功能相似,但又无法适用当前开发需求 的问题
执行jar命令:sh run.sh 即可启动
#脚本解释: 文件名可叫 run.sh
// 功能:自动将当前目录下的jar包 执行 java -jar XX.jar 并且吧 标准输出 和错误输出 重定向到黑洞
#!/bin/sh
# 获取当前目录
DIR='dirname $0'
#打开当前目录
cd $DIR
#执行java -jar XXX.jar 解释:把标准输出重定向到“黑洞”,还把错误输出2重定向到标准输出1
nohup java -jar 'ls | grep jar'> /dev/null 2>&1 &
#将该应用的pid记录到tpid文件中 方便一会停止运行
echo $!>tpid
#提示用户启动成功
echo Start Success!
结束jar命令: sh stop.sh
#!/bin/bash
#cat tpid | xargs kill -9
pid='cat tpid'
while :
do
rt='ps -ef | grep java | grep $pid'
if[[ $rt =~$pid ]]
then
kill $pid
else
echo $pid is killed
break;
fi
done
Ctrl+Alt+V 自动创建返回值及变量
Ctrl+Alt+ <-- 调回上次调用该函数的位置 特别有效防止看代码时跳的晕头转向
guava技巧_飞花落雨的博客-CSDN博客
@JsonInclude(JsonInclude.Include.NON_NULL) //标注在类头上,过滤json字段中为null的字段。
@Data //注解在类上, 为类提供读写属性, 此外还提供了 equals()、hashCode()、toString() 方法
@JsonProperty //此注解用于属性上,作用是把该属性的名称序列化为另外一个名称,1.前端传参数过来的时候,使用这个注解,可以获取到前端与注解中同名的属性 2。后端处理好结果后,返回给前端的属性名也不以实体类属性名为准,而以注解中的属性名为准
@Valid //会去验证后面对象里面的每个属性,每个属性看是否符合要求,不符合时返回message
如:void myFunctuon(@Valid User user) {//todo}
而User中:
@Data
public class User {
@NullNot(message = "用户名不能为空啊")
private String username;
}
/**关键字,不引主,插入会报错,group在sql里是关键字,所以执行时会报错,
看到日期打印的结果,框架本就提示出此错误可能是因为使用关键字导致的,
起因: 数据库建表一般都是用引号引住的字段,所以可以见表成功,但是你的mybatis使用的时候没有引,
导致使用关键字报错,故,需要对有问题的字段进行 引用即可
*/
@TableField("`group`")
private String group;
注:如果前端传 http://XX/?name=1,2,3,4 后端类属性使用。private List
扩展:
注解Autowaired与 Resource的区别
@Autowired//默认按type注入(spring的注解)
@Qualifier("cusInfoService")//一般作为@Autowired()的修饰用
@Resource(name="cusInfoService")//默认按name注入,可以通过name和type属性进行选择性注入(javaee的注解)推荐使用:@Resource,可以使代码更优雅,避免警告提示
@Autowired 与@Resource的区别(详细)_raymond_2580的博客-CSDN博客_@resource @autowired
场景:每次本地部署应用和远程部署一样,都要手动各创建一份请求,除了ip地址不同,其他路径和参数一模一样,如下:(原先自己都是在postman中创建两个文件夹,老粘贴复制,烦死。)
//本地请求
127.0.0.1:8080/user/operators
//远程请求
192.168.11.1:8080/user/operators
所以,有没有方法:把ip地址变成变量,切换“环境”就自动改变变量的值?postMan提供了!!
如果想创建环境,就选 No Environment 再点击小眼睛,就会弹出新窗口,添加,如下:
如果下修改环境,就选 该环境,再点小眼睛。
详情参考:(65条消息) postman初级-1-环境变量:增、删、改、切换_花测试-CSDN博客_postman怎么删除环境变量
场景:按照mysql的数据结果,默认排序结果是 id列从小到大的排列。
select * from user limit 0,5 取 前5条数据 这是没有什么问题的,不用加order by
如果在sql语句中不指定order by排序条件,那么得到的结果集的排序顺序是与查询列有关的。因为不同的查询列可能会用到不同的索引,从而导致顺序不同。所以无论什么情况下,还是加上为好。
(65条消息) MySQL查询默认排序与order by排序_韩某的博客-CSDN博客_mysql查询结果默认排序
场景:java插入msyql时,失败,发现某字段带"-"
解决方法:加引号
原来是:
@ApiModelProperty("繁体中文翻译")
@TableField("zh-tw_name")
public String zhtwName;
解决后是:加 ` //查询插入都不报错
@ApiModelProperty("繁体中文翻译")
@TableField("`zh-tw_name`")
public String zhtwName;
切记 不是 加 ' //查询不报错了 插入报错
@ApiModelProperty("繁体中文翻译")
@TableField("'zh-tw_name'")
public String zhtwName;
解决思路:发现打印的sql执行出错,看了没问题,去msql中查询却发现的确报错。所以加单引号
场景:开发时,一般分配的任务,自己在远程新开一个My分支,每次都在My分支上进行修改和提交,到任务都做完汇总的时候,再合并的主分支上。
或者自己先在本地创建本地分支,进行开发和修改,然后将本地推送到远程 同名分支,也即是自己的这个分支推送到远程,开发完成再和主分支合并。
命令:
1,git clone ssh://git@XXXX 克隆项目分支
2,git checkout master 切换到master分支
3,git checkout -b playBackQueue 创建playBackQueue分支并切换至这个分支,这个是本地分支
4,git branch 查看当前分支,*应该在playBackQueue分支上
5,git add . 添加修改
6,git status 查看工作区状态,即修改的文件有哪些
7,git commit -m"注释" 提交修改到当前分支
8,git push origin playBackQueue:playBackQueue把本地新建的分支推到远程分支,冒号前面是远程分支名,也可以与本地分支名不同
//9,10步骤 是将 playBackQueue 合并到master分支上
9,git checkout master 切换回master分支
10,git merge playBackQueue 合并自己的分支到master
11,浏览器查看是否提交上去了读代码
12,git push origin :playBackQueue 删除远程分支
或者git push origin --delete playBackQueue都可以实现删除远程分支
12,git branch -d playBackQueue 删除playBackQueue本地分支
13、将当前分支关联到指定分支
git checkout -b gpf origin/gpf # 新建本地分支gpf与远程gpf分支相关联
(68条消息) gitLab新建分支给远程分支提交代码_yana_balabala的博客-CSDN博客_gitlab提交新分支
上述命令遇到冲突就不好搞了,对应融合最好使用idea自带的。如果想将opensearch分支融合到masrer分支,则
①切换分支到master(如果本地已经有master分支,最好也切远程的,因为可以更新一下本地的master分支)
②git->merge
③执行融合,弹出来的下拉选项,如果没有opensearch 说明opensearch最新的代码已经融合到master分支了。如果有,那说明确实要融合一下最新的。
注:以上融合都是本地分支的融合,远程的分支还没融合这时候,虽然我并没有对mater分支进行修改,使用不了(因为提交也是没文件让你提交)。但是你这时候应该push一下到远程,push后才是真的推送到远程。
Collectors.toMap()该函数的参数key、value均不能为空,否则空指针异常
codeMap = ValuesList.stream().filter(item->Objects.nonNull( item.getCode())).collect(Collectors.toMap(Values::getId,Values::getCode,(v1,v2) -> v1));
扩展:想要获取 列表A中的列表B,将B中的所有集合放到C列表中
//存在空指针异常 i.getB()可能为空
List c= products.stream().flatMap(i ->i.getB().stream()).collect(Collectors.toList());
//使用Optional.ofNullable(可能为空的对象) 防止A中的B 没有对象导致空指针异常
List c= A.stream().flatMap(i ->Optional.ofNullable( i.getB()).orElseGet(ArrayList::new).stream()).collect(Collectors.toList());
注:toMap() 使用时,最好 考虑到 key重复的情况。如:
//身份证号为key:value为name
XXX.toMap("persion身份证号",persion.姓名,(v1,v2)->v2) //如果遇到重复的key 那就保留最新的value
代替:
XXX.toMap("persion身份证号",persion.姓名)
场景:使用mybatis查询或插入时,myabatis报错,查无此列,核对了 表结构、以及对应的实体 发现都没问题 也都没这个字段,那么这个字段哪里来的??
解决:发现 对应的实体 继承了 其他类, 该类的私有字段也给继承回来了!!
所以:子类继承父类 是继承了父类的所有!! 包括private修饰的属性!!
场景:一般发生在更新或者删除sql操作时。出现的情况是应用程序 被kill -9 pid死的 (正确操作:应该kill pid),更新sql语句时,结果java代码就卡在这里不动,好半天后 报Lock wait timeout exceeded try restarting transaction。
java应用程序结束,已经开启的事务不一定结束!(未验证)
解释1:
原因:
锁表,是因为有个上一个事务A没有提交,现在来了事务B 要操作 该资源(指:行 表),导致B没法操作该资源,事务A又迟迟没了“动静”,出现 锁表。 该资源(指:行(如果是innoDB:可锁行、表), 表(如果是MyIsAm,只可锁表))
1.无索引情况下更新数据
begin;-- 开启事务
update tb_user set phone=‘15167891234’ where name=‘小花’;-- 修改,先别commit事务
再开一个窗口,直接运行命令:
update tb_user set phone=‘15167891234’ where name=“小明”;-- 发现一直卡着不能执行
但将第一个窗口的sql COMMIT之后,第二个窗口的更新语句就能执行了,说明在where条件后没索引的情况下锁表。
2.有索引情况下更新数据
create index index_name on tb_user(name);
加完索引之后继续按照1的步骤去执行,发现窗口2不会卡着了,立马执行了,说明没有锁表了,然后将相同的update语句在打开的2个窗口内执行(即:更新用一条),发现第2个窗口会一直卡着,说明在where条件后有索引的情况下锁行。总结
在update/delete情况下,如果没有索引,会锁表,如果加了索引,就会锁行。但是其中过滤条件最好在主键索引情况下执行,因为过滤条件在非主键索引情况下,mysql会先锁住非主键索引,再锁定主键索引,如果此时恰好该行记录又根据主键索引更新,有可能也会发生冲突。
ps:数据库锁表时间一般为50s。
解释2:
//查看mysql自带的表中有哪些 事务
SELECT * FROM information_schema.INNODB_TRX;
//找到 trx_sql_thread_id 杀掉即可
kill XXXX
如果怕以后再出现类似情况,在kill -9 XXX 时,让java持续知道自己被杀了,然后赶快回滚本次事务就行了。
解决办法:kill XXX 少用kill -9 XXX 前者会"有序退出程序",后者直接 嗝屁退出程序
kill 和 kill -9 是常用的命令,都可以用来杀死进程。 那 kill 与 kill -9 有什么区别呢?
kill命令默认的信号就是15,也就是 kill -15 ,被称为优雅的退出。 当使用kill -15时,系统会发送一个SIGTERM的信号给对应的程序。 当程序接收到该信号后,具体要如何处理是自己可以决定的
注:其他博客都是 trx_state 是lock 才kil,而我遇到的是running状态,kill后发现也好了。
场景:由于要数据解析MQ拉取的参数,我在service中创建了 A函数进行解析,然后调用带事务注解的B函数,B中有对数据库操作的 C,D函数。 结果在B中执行完C,执行D时抛出异常,但数据没回滚。伪代码如下:
class AAAA{
/**A 调用带事务注解的B,此刻事务没开启,原因:事务注解基于动态代理,
那么如果在类内部调用类内部的事务方法,这个调用事务方法的过程并不是通过代理对象来调用的,
而是直接通过this对象来调用方法,绕过的代理对象,肯定就是没有代理逻辑了
*/
public void A(){
//业务逻辑
this.B();
}
@Transactional(rollbackFor = Exception.class)
public void B(){
this.C();//插入更新操作
1/0;
this.D();//插入更新操作
}
private void C(){//todo}
private void D(){//todo}
}
解决: 将注解移到 函数A头上即可。
扩展:
事务注解失效的原因:
①事务注解标注的当前函数的访问修饰符不是public
②在t同一个类中,函数A调用 带有@Transactional标注的函数B方法。B是不会开启事务的(我的情况就是这个)
③捕获异常抛出事务却不回滚。
(我在 类头上加事务注解,发现开启了事务,但是B中执行C后发生异常,数据并没有被回滚,原因是:如果方法中捕获异常后手动抛出异常,事务并没有回滚。)
解决办法:不要用Exception.class 取捕获,如,int i=1/0; 运行时的异常,要用 RuntimeException.class捕获。,
@Transactional(rollbackFor = RuntimeException.class)
可是Exception是包含了 RuntimeException的啊,不知道为什么!
但是,下面的代码标记在方法上,是可以拦截 检查和不可检查 的异常的!!
@Transactional(rollbackFor = Exception.class)
总结:最好加在方法上,加类头上好像拦截不了 运行时的异常(不可检查的异常)
小扩充:Exception分:可检查的(如I/O异常)、和不可检查的(如空指针异常,即根据运行的结果有不可预期的可能)。
(73条消息) @Transactional注解不起作用解决办法及原理分析_一撸向北的博客-CSDN博客_transactional注解失效
扩展:全局异常
springBoot中提供了全局异常的拦截处理。定义全局拦截方法,在类头上标记注解:
@RestControllerAdvice
代码如下:
import com.XX.product.config.MybatisPlusConfig;
import com.XX.product.model.common.ResponseDTO;
import com.XX.product.exception.ProductServiceException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import java.util.List;
import java.util.Set;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.OK)
protected ResponseDTO handleInternalException(Exception e) {
String errCode;
StringBuilder message = new StringBuilder();
if (e instanceof ValidationException) {
// 参数校验报错
errCode = 50001;
ConstraintViolationException exs = (ConstraintViolationException) e;
Set> violations = exs.getConstraintViolations();
for (ConstraintViolation> item : violations) {
message.append(item.getMessage()).append(";");
}
} else if (e instanceof BindException) {
// 参数绑定报错
errCode = 5002;
List fieldErrors = ((BindException) e).getFieldErrors();
for (FieldError fieldErr : fieldErrors) {
message.append(fieldErr.getDefaultMessage()).append(";");
}
} else if (e instanceof MethodArgumentNotValidException) {
// 参数不合法报错
errCode = 50003;
MethodArgumentNotValidException methodArgumentNotValidException = (MethodArgumentNotValidException) e;
List fieldErrors = methodArgumentNotValidException.getBindingResult().getFieldErrors();
for (FieldError fieldErr : fieldErrors) {
message.append(fieldErr.getDefaultMessage()).append(";");
}
} else if (e instanceof ProductServiceException) {
// 业务报错
ProductServiceException productServiceException = (ProductServiceException) e;
message = new StringBuilder(productServiceException.getMessage());
errCode = productServiceException.getErrCode();
} else {
// 其他报错
errCode = 500;
message = new StringBuilder(ErrorCodeEnum.UNKNOWN_ERR.getDesc() + ":" + e.getMessage());
}
log.error("系统全局异常定位:{}", Arrays.stream(e.getStackTrace()).limit(3).collect(Collectors.toList()));
log.error(e.getMessage(), e);
ResponseDTO responseBean = new ResponseDTO<>();
responseBean.setMessage(message.toString());
responseBean.setCode(errCode);
responseBean.setStatusCode(500);
return responseBean;
}
}
异常什么时候抛出,什么时候捕获呢?
程序上:捕获,是程序继续还会执行,抛出,当前程序会中断
个人业务需求上:某个字段的、或者众多批次中有个别失败,在不影响大局的情况下捕获。
基本原则:能抛则抛
场景:公司使用jackson转换成字符串时,发现日期对象变成了时间戳的方式。一般使用jackSon会处理很多不同类型的对象,而有的对象的日期对象是:Date ,有的又是 LoaclDateTime 对象。。。
解决: Data使用 SimpleDateFormat格式化,LoaclDateTime使用JavaTimeModule格式化,且可以同时使用。
方法:
public String dddd(Object object){
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addDeserializer(LocalDateTime.class,
new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addSerializer(LocalDateTime.class,
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
objectMapper.registerModule(javaTimeModule);
String contentJson = null;
try {
contentJson = objectMapper.writeValueAsString(content);
} catch (JsonProcessingException e) {
log.error("get json from value failed! value[" + content + "].", e);
}
returen contentJson ;
}
多个线程各自拷贝一份 ThreadLocal修饰的变量 到自己本地,即:不会对原本的变量修改,只修改副本。
注:ThreadLocal变量通常设置为static的原因:避免每次使用该类就创建该变量,节省空间。不然,每次创建 一个值相等的对象,但这些对象的地址又不同,造成浪费
一个ThreadLocal实例对应当前线程中的一个TSO实例。因此,如果把ThreadLocal声明为某个类的实例变量(而不是静态变量),那么每创建一个该类的实例就会导致一个新的TSO实例被创建。显然,这些被创建的TSO实例是同一个类的实例。于是,同一个线程可能会访问到同一个TSO(指类)的不同实例,这即便不会导致错误,也会导致浪费(重复创建等同的对象)!因此,一般我们将ThreadLocal使用static修饰即可。
原文链接:https://blog.csdn.net/u013543848/article/details/102980066
(74条消息) 深入学习java源码之ThreadLocal.get()()与ThreadLocal.initialValue()_wespten的博客-CSDN博客_java threadlocal.get
注:其实这是 覆盖操作,所以记得 可能需要 补上一些额外的 限制语句,如下:unsingned not null
//添加默认值、并且修改备注
alter table tableXXXXX modify column product_status tinyint(3) unsigned NOT NULL default 1 comment '1 草稿,4下架,11上架';
让sql维护更新时间、创建时间
//修改tableXXXXX 表的created_at 字段设置为timestamp 类型,并设置默认值为 创建该记录的时间
ALTER TABLE tableXXXXX MODIFY created_at timestamp DEFAULT CURRENT_TIMESTAMP;
//修改tableXXXXX 表的created_at 字段设置为timestamp 类型,并设置默认值为 更新该条记录的时间
ALTER TABLE tableXXXXX MODIFY updated_at timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
好处: java代码中,就不用再写设置 这两个字段的业务逻辑了。
思路:http
python服务开启代码:
from flask import Flask,request
app = Flask(__name__)
@app.route("/", methods=["POST"])
def hello():
print(request.form["name"])
return "Hello World!"
if __name__ == "__main__":
app.run(port='8080')
java调用代码:
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
public class TestRcp {
public static void main(String[] args) {
RestTemplate restTemplate = new RestTemplate();
String url="http://127.0.0.1:8080/";
MultiValueMap params = new LinkedMultiValueMap<>();
params.add("name","张三");
Object object1= restTemplate.postForObject(url,params,String.class);
System.out.println();
}
}
关键词:主从复制,多数据源
步骤1:maven依赖:
org.apache.shardingsphere
sharding-jdbc-spring-boot-starter
4.1.1
org.codehaus.groovy
groovy
步骤2:application.properies文件
#配置主从数据源,要基于MySQL主从架构。
spring.shardingsphere.datasource.names=m0,s0
spring.shardingsphere.datasource.m0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.m0.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.m0.jdbc-url=jdbc:mysql://192.168.11.131:3306/product-service?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8
spring.shardingsphere.datasource.m0.username=root
spring.shardingsphere.datasource.m0.password=11
spring.shardingsphere.datasource.s0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.s0.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.s0.jdbc-url=jdbc:mysql://192.168.11.132:3306/product-service?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8
spring.shardingsphere.datasource.s0.username=root
spring.shardingsphere.datasource.s0.password=11
#读写分离规则, m0 主库,s0 从库
spring.shardingsphere.sharding.master-slave-rules.ds0.master-data-source-name=m0
spring.shardingsphere.sharding.master-slave-rules.ds0.slave-data-source-names=s0
spring.shardingsphere.props.sql.show=false
spring.shardingsphere.props.check.table.metadata.enabled=false
spring.shardingsphere.props.max.connections.size.per.query=100
此处m0库负责增删改,s0负责查询,两处数据源可以配置不同,也可以配置为同一个数据源
1、maven依赖
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
${nacos.config.version}
guava
com.google.guava
2、配置文件:
bootstrap.properties内容:
spring.cloud.nacos.config.group=DEFAULT_GROUP //指定nacos上的组
spring.cloud.nacos.config.file-extension=properties
spring.cloud.nacos.config.server-addr=nacos-server.dev.interfocus.tech:443
spring.cloud.nacos.config.namespace=771917b3-7305-4be2-XXXX-efd6c9860a5f //每个环境naocs上的唯一id
spring.cloud.nacos.config.refresh-enabled=true
spring.cloud.nacos.config.enabled=true
优势,可以通过界面去管理或触发 任务执行,比@schedule 注解强多了,后者是死的、定时的,一般用于单机上的执行。
使用:
com.xuxueli
xxl-job-core
${xxljob.version}
xxl.job.admin.addresses=https://xxl-job.dev.interfocus11.tech/
xxl.job.accessToken=
xxl.job.executor.appname=11-community-service
xxl.job.executor.address=
xxl.job.executor.ip=
xxl.job.executor.port=9081
xxl.job.executor.logpath=/var/log/java/xxl-community
xxl.job.executor.logretentiondays=30
package com.11.community.config;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class XxlJobConfig {
private final Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.accessToken:}")
private String accessToken;
@Value("${xxl.job.executor.appname}")
private String appname;
@Value("${xxl.job.executor.address:}")
private String address;
@Value("${xxl.job.executor.ip:}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
/**
* 执行器
*
* @return XxlJobSpringExecutor
*/
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
logger.info(">>>>>>>>>>> xxl-job adminAddresses=" + adminAddresses + " appname=" + appname + " port=" + port);
return xxlJobSpringExecutor;
}
}
package com.11.community.controller.xxljob;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.11.community.bo.PostFactorBo;
import com.11.community.bo.PostScoreTempBo;
import com.11.community.bo.es.PostProductIdBo;
import com.11.community.bo.es.PostProductIdTempBo;
import com.11.community.bo.es.ProductIndexEs;
import com.11.community.constant.BaseConstant;
import com.11.community.entity.PostScore;
import com.11.community.query.post.PostsWithProductQuery;
import com.11.community.service.IPostScoreService;
import com.11.community.service.IPostService;
import com.11.community.utils.DateUtils;
import com.11.community.utils.DecimalUtils;
import com.xxl.job.core.handler.annotation.XxlJob;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.index.query.QueryBuilders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.text.ParseException;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
*
* xxl job定时任务
*
* @Author mcj
* @Date 2022/2/15
*/
@Slf4j
@Component
public class PostSortJobHandler {
@Autowired
private IOpenSearchCommonService commonService;
/**
* xxl job 调用
*/
@ApiOperation("同步数据到openSearch信息")
@XxlJob(value = "openSearchPut") //我们的 任务调度的执行器 就叫 openSearchPut
public void queryAndPut() {
String productIds = XxlJobHelper.getJobParam();//获取 xxl-job的参数,如果不传,则是空字符串
try {
commonService.startTask(productIds);
}catch (Exception e){
log.error(e.getMessage());
e.printStackTrace();
throw e;
}finally {
MybatisPlusConfig.myTableName.set("");
}
}
idea控制台也可以看见是否注册成功
xxl-job的控任务调度执行也可以核对是否注册上来:
关键词:@value赋值失败、 指定加载配置文件失败、no active profile set
场景:使用配置类时,注入bean,该bean依赖@valu("${XXX}"),发现报错没解析,如下:
(注:截图中 port的值的value实际写错了,导致项目一直卡住,不报错也没信息输出)
发现:编译后的target目录没有配置文件,那么需要在pom中设置如下:
src/main/resources
**/*.xml
**/*.properties
true
待续.....
场景:项目启动。没有详细的信息,只有框架logo输入 ,如下:
原因:和日志输出有关。应该是日志路径或者日志配置有问题。自己创建了新环境的配置文件而日志配置文件中没有配该环境下的:
扩展:
日志模板:
application.properties的日志文件配置:
#指定日志文件的配置文件
logging.config=classpath:logback-spring.xml
#日志的输出等级,可以指定具体某个目录下的才输出,如果 设置 logging.level.root=debug 则整个项目含jar包中的也输出的 贼多
logging.level.com.example=debug
logging.file.path=/log
logback-spring.xml的日志模板配置:
${CONSOLE_LOG_PATTERN}
${log_path}/${project_name}.%d{yyyy-MM-dd}.%i.log
100MB
30
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
(可能配置麻烦)
grafana 是一款采用 go 语言编写的开源应用,是一个跨平台的开源的度量分析和可视化工具,可以通过将采集的数据查询然后可视化的展示,并及时通知
配置样例:
钉钉 自定义机器人接入:自动发到群消息:
https://blog.csdn.net/u013372493/article/details/124819854
//inputFile 文件路径
//outputFile 输出路径
// gb 拆分文件的大小, 单位gb
public static void fileToSamll(String inputFile, String outputFile,int gb) {
try {
int hash = gb * 1024 * 1024 * 1024;
FileReader read = new FileReader(inputFile);
BufferedReader br = new BufferedReader(read,10*1024 * 1024);
String row;
int num = 0;
int fileNo = 1;
FileWriter fw = new FileWriter(outputFile+fileNo +".csv");
while ((row = br.readLine()) != null) {
num =row.length() +num;
fw.append(row + "\r\n");
if((num / hash) > (fileNo - 1)){
fw.close();
fileNo ++ ;
fw = new FileWriter(outputFile+fileNo +".csv");
}
}
fw.close();
System.out.println("fileNo="+fileNo);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
1、配置文件 logback-spring.xml
${CONSOLE_LOG_PATTERN}
${log_path}/${project_name}.%d{yyyy-MM-dd}.%i.log
100MB
30
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
注:如果控制台有日志打印,但是日志文件没有日志打印,请查看springboot的启动环境,不指定环境,则是 default,如下:
那么去检查,springProfile name="default" 是否存在。(上文的配置有给出,所以存在)
场景:开发项目,引入nacos。死活连接不上远程的nocas配置。配置如下:
maven依赖:
//springboot版本是2.3.12.RELEASE
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
2.2.7.RELEASE
配置文件:application.properties
spring.application.name=11-searchTemp
spring.profiles.active=dev
spring.cloud.nacos.config.group=DEFAULT_GROUP
spring.cloud.nacos.config.file-extension=properties
spring.cloud.nacos.config.namespace=771917b3-7305-4be2-9c0b-efd6c9860a5f
spring.cloud.nacos.config.server-addr=XXXXXXX:443
spring.cloud.nacos.config.refresh-enabled=true
spring.cloud.nacos.config.enabled=true
问题:配置了这些配置和没配一样,问题排查方法 :
发现NacosPropertySourceBuilder 类输出的这个 Igrore the empty nacos XXX,所以全局查找(idea 中,按两次sheift) r如下:
找到日志的代码:
考虑到,是其他调用此方法输出的这些,那么断点调试,找到调用此方法的函数,看看传入的数据 对不对就行了!
这样跳出函数几次,很有可能就找到我们配置的数据,开始没加载我们指定的数据,这里都是默认的localhost:8848 等其他默认。
原因找到了,是没加载配置文件,为什么没加载我们指定的配置呢?
扩展知识:bootstrap优先级高于 appliaction文件 ,yml优先级高于properties。
经验建议:提早配置、引导配置 尽量配置到 bootstrap中
application.properties和 bootstrap.yml 区别 - 简书
问题解决:将项目中的 application.properties 文件名修改为 bootstrap.properties
读取远程的配置文件验证:
个人心得:application 更注重的是 应用本身的上下文引导,bootstrap是程序启动时的引导,包含了应用上下文的引导
前提是公司已经给你申请了 秘钥key
详情参考:
官方参考:快速入门 | Cloud Translation | Google Cloud谷歌翻译SDK (Google Translate SDK)的使用_nicolelili1的博客-CSDN博客_谷歌翻译开放平台
步骤开始:未安装或者配置 上述博客的的任何东西。
步骤1:引入依赖
com.google.cloud
google-cloud-translate
2.1.13
步骤2:接口调用
package com.search.service.impl;
import com.google.cloud.translate.Translate;
import com.google.cloud.translate.TranslateOptions;
import com.google.cloud.translate.Translation;
import com.google.cloud.translate.v3.*;
import com.11.search.service.IGoogleTranslationService;
import org.springframework.stereotype.Service;
import java.io.IOException;
/**
* @author wwwV_Jb0
* * @date 2022/5/13
*/
@Service
public class GoogleTranslationServiceImpl implements IGoogleTranslationService {
@Override
public String translateToEn(String keyword) throws IOException {
//该秘钥是 付费的,一般是AIz开头的
String key="xxxxxxxxxxxxxxxxxxx";
Translate translate = TranslateOptions.newBuilder().setApiKey(key).build().getService();
String targetLanguage = "en";
String sourceLanguage = "zh-CN";//可以传空 会自动检测原语种
Translate.TranslateOption srcLang = Translate.TranslateOption.sourceLanguage(sourceLanguage);
Translate.TranslateOption tgtLang = Translate.TranslateOption.targetLanguage(targetLanguage);
// Use translate `model` parameter with `base` and `nmt` options.
Translate.TranslateOption model = Translate.TranslateOption.model("base");
String sourceText = "裙子 ";
Translation translate1 = translate.translate(sourceText, srcLang, tgtLang, model);
String translatedText = translate1.getTranslatedText();
System.out.println(translatedText);
return null;
}
}
。。。。。
com.google.api.client.googleapis.json.GoogleJsonResponseException: 400 Bad Request
GET https://translation.googleapis.com/language/translate/v2?key=AIzaSyBmdOdgjKSPKDfiUTW1EAwe5bVLPzfXXXX&model=base&q=%E8%A3%99%E5%AD%90%20&source=zh-CN&target=en
{
"code" : 400,
"details" : [ {
"@type" : "type.googleapis.com/google.rpc.ErrorInfo",
。。。。。
可以看出,调用api也是发送请求,只不过参数 进行了拼接,所以 直接通过 restTemplate发送get请求 应该也是可以。如下,验证也是成功的的。
主要是如何拼接url的get请求:
格式如下:
//共4个参数: key、model、q(即关键字) 、source(原始语言)、target(目标语言)
https://translation.googleapis.com/language/translate/v2?key=AIzaSyBmdOdgjKSPKDfiUTW1EAhe5bVXXXXX&model=base&q=长裙&source=zh-CN&target=en
方式一: 比较原始的request请求
(谷歌文档的风格)
)
首先,为了避免多次创建客户端,多次读取密钥,所以将创建的客户端通过java的配置类 注入spring中
认证代码:(基本原理使用谷歌的对象,对request对象设置一些认证的操作
使用上:
//方法一认证: 其中, google-credentials是谷歌授权账号去“凭证”下载的json密钥
@Bean
public GoogleCredential googleAuthorize() throws IOException {
ClassPathResource resource = new ClassPathResource("google-credentials.json");
return GoogleCredential.fromStream(resource.getInputStream()).createScoped(Collections.singleton("https://www.googleapis.com/auth/cloud-platform")).setExpirationTimeMilliseconds(new Long(3600000L));
}
@Bean
public HttpRequestFactory httpRequestFactory() throws GeneralSecurityException, IOException {
HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
return httpTransport.createRequestFactory();
}
-------------------------------------------------------使用方面------------------------
public void sss(){
//其中map是我们的请求体,即依据json字符串,构造的map。如:{“name”:"李四"} 则map.put("name","李四")
//调用谷歌的buildPostRequest获取request
HttpRequest request = httpRequestFactory.buildPostRequest(new GenericUrl(谷歌文档某API的URL), new JsonHttpContent(new GsonFactory(), map));
//给谷歌的HttpRequest 设置密钥
googleAuthorize.initialize(request);
HttpResponse response = request.execute();
System.out.println(response.parseAsString());
}
注:如果报错40X,请依据返回的报错结果,调整json,一般是构造的map和json对不上,另一个是 value的类型不对(常规字段基本均为字符串)
方式二:谷歌对象的认证
方法二:谷歌的风格都是 XXXServiceClient客户端,想要对其设置 传入xxxServiceSettings 对象即可
@Bean
public Credentials getCredentials() throws IOException {
ClassPathResource resource = new ClassPathResource("google-credentials.json");
return ServiceAccountCredentials.fromStream(resource.getInputStream());
}
@Bean
public XXXServiceClient XXXServiceClient() throws IOException {
Credentials creds = this.getCredentials();
XXXServiceSettings xxxServiceSettings = XXXServiceSettings.newBuilder().setCredentialsProvider(FixedCredentialsProvider.create(creds))).build();
return XXXServiceClient.create(xxxServiceSettings);
}
---------------------------------使用方面----------------------------------------------
基本和XXXServiceClient的方法有关,不太灵活只能调用该客户端提供的一些方法,但是方便和标准
注:谷歌提供的对象、或者要传入函数的对象 一般都不可new,
而是 XXXX对象.newBuilder().setXXX()......bulid();
方式三:有时候可能业务安全方面,本地不让保存 google-credentials.json,需要配置在配置中心去管理,所以我们可以先把 google-credentials.json内容变成字符串(可通过map一个一个属性put再转成字符串)。
@Value("${google.search.key}")
private String googleSearchKey;
@Bean
public Credentials getCredentials() throws IOException {
//map变成输入流
// InputStream in = new ByteArrayInputStream(JSON.toJSONString(map).getBytes());
// ClassPathResource resource = new ClassPathResource("google-credentials.json");
return ServiceAccountCredentials.fromStream(new ByteArrayInputStream(googleSearchKey.getBytes()));
}
身份认证:
@Configuration
public class AWSConfig {
@Value("${aws.accessKeyId}")
private String awsAccessKeyId;
@Value("${aws.seretACCessKey}")
private String awsAccessKeySecet;
@Bean
public PersonalizeClient personalizeClient() {
AwsBasicCredentials awsCreds = AwsBasicCredentials.create(awsAccessKeyId, awsAccessKeySecet);
return PersonalizeClient.builder().credentialsProvider(StaticCredentialsProvider.create(awsCreds)).region(Region.US_WEST_2).build();
}
}
java文档参考:aws-doc-sdk-examples/javav2 at main · awsdocs/aws-doc-sdk-examples (github.com)
该github项目的所有示例代码,除了没有按照其配置认证环境,而是需要提供代码实现认证,
惯用模板套路: 通过获取 XXXCredentials.create() 获取证书对象,然后把这个对象丢到 某目标客户端:
XXXClient.builder().credentialsProvider(XXXXCredentialsProvider.create(awsCreds)).region(Region.US_WEST_2).build();
其实很多时候,第三方认证让配置系统环境变量,即在项目启动前先用代码或者指定jvm参数进行设置,如:
文章参考:【SpringBoot】启动前执行的几种方式_可耳(keer)的博客-CSDN博客
场景:get请求模式不准变化的需求,且众多参数,接收参数的对象,如果属性和get请求携带的参数 一模一样,自然会接收成功,如下:
get 请求: XXX/XX/site_id=100¶m1=&....
接收该请求要封装的对象:
@Data
public QO{
private Integer site_id;
private String param1;
}
这样,调用时,可能是 qo.getSite_id(); 非常丑。
所以,通过以下实现:
和上述方法一样,但是 生成set、get方法后,将方法名修改去掉 下划线, 并删除 set方法,如下:
@Data
public QO{
private Integer site_id;
private String param1;
//切记 不能要 get_siet_id的方法
public get siteId(){ return site_id;}
public get param1(){ return param1;}
}
查询思路:先理解你公司所在的索引字段,一般是某个模块 会应了一个项目名称字段:如project_name,想要查看日志是否发送错误,可以看 level字段:于是写出的查询语句为:
product_name:"模型名称" AND level "error"
其中,level是你公司在同步到es上 log.error的信息一般通过脚本同步解析,赋值该字段,
一般的 log.info 日志信息会放到 message字段(我公司是这样设计的表结构,每个公司不一样哈)
例如:下面想查询: product-service 模块的日志 且 log.info输出 query:default;
技巧:当一个项目很大时,线程很多,依据日志排错,日志相关时间段内打印的日志是各个线程掺杂着出现的,这时候我们可以评估 单位时间内(如5分钟、1分钟,主要看时间段内的日志数量来确定)按线程聚合 ,甚至抛出异常的都不知道是哪个接口抛出的(如IOException:Broken pipe)
使用dev tool的方式查询示例:
GET java-log-2022-06-21/_search
{
"query": {
"bool": {
"must": [
{
"match_phrase": {
"project_name": {
"query": "product-service"
}
}
},
{
"match_phrase": {
"message": {
"query": "query:default:"
}
}
}
]
}
},
"_source": [
"project_name",
"message"
],
"from": 0,
"size": 10000
}
扩展:IOException:broken pipe
原因:A请求B,B还在处理A的请求,A这时候就不等待结果取消了请求
①在linucx系统上报错为: IOException:broken pipe
②在window系统上报错为:java.io.IOException: 远程主机强迫关闭了一个现有的连接。
1. 如果是只需要查看本地仓库的话可以使用如下命令:(会下载,然后打印路径)
mvn help:evaluate -Dexpression=settings.localRepository | grep -v '\[INFO\]'
默认路径:/home/用户/.m2/repository
2. 在运行maven命令时,添加-X 或者 -Debug参数
mvn -X
会打印出相关结果
...
[DEBUG] Reading global settings from C:\Maven\conf\settings.xml
[DEBUG] Reading user settings from C:\segphault\.m2\settings.xml
[DEBUG] Using local repository at C:\Repo
文章参考:命令行查看本地Maven仓库地址_xinglu31的博客-CSDN博客_linux maven 默认仓库地址
方式1:
String path = this.getClass().getClassLoader().getResource("my.json").getPath();
方式2:(推荐)
ClassPathResource resource = new ClassPathResource("my.json");
nohup java -jar babyshark-0.0.1-SNAPSHOT.jar > log.file 2>&1 &
上面的2 和 1 的意思如下:
0 标准输入(一般是键盘)
1 标准输出(一般是显示屏,是用户终端控制台)
2 标准错误(错误信息输出)
参考:(89条消息) Java -jar 如何在后台运行项目_刘信坚的博客的博客-CSDN博客_jar如何在后台运行
架构设计的主要目的是为了解决软件系统复杂度带来的问题,解决:高性能、高可用、可扩展
网友感悟是:架构即(重要)决策,是在一个有约束的盒子里去求解或接近最合适的解。这个有约束的盒子是团队经验、成本、资源、进度、业务所处阶段等所编织、掺杂在一起的综合体(人,财,物,时间,事情等)。
架构无优劣,但是存在恰当的架构用在合适的软件系统中,而这些就是决策的结果。 需求驱动架构。
在分析设计阶段,需要考虑一定的人力与时间去"跳出代码,总揽全局",为业务和IT技术之间搭建一座"桥梁"。
至少应该拿出3种架构方案,避免个人局限性。
对方案的选择可以通过:
(质量属性:如:性能、复杂度、人力、硬件成本、可靠性、可维护性)
今日得到:
1 架构是为了应对软件系统复杂度而提出的一个解决方案。
2 架构即(重要)决策
3 需求驱动架构,架起分析与设计实现的桥梁
4 架构与开发成本的关系
依据原则:合适原则、简单原则、演化原则
场景:搜索服务,调用第三方服务,为了降低成本,故做缓存,降低调用第三方服务的频率,用户体验也不错。但面对 用户搜索请求,可能拼接很多条件,所以key应该如何设计呢?
思路:利用:相同的字符串经过md5算法,得到的 32位是一样的。因此,将 参数对象 变成字符串,通过 (md5算法)映射 成一个32位的 字符串
总结:有时候,可以把当前问题转换等价成 另一种方式。要有维度映射的思维
关键词:list转csv
场景:虽然可以借助 csv依赖 ,一个字段一个字段的塞进去,但是效率慢
思路:根据csv是用 逗号,作为单元格进行区分,那么我们利用反射,可以一劳永逸
解决方法:
package com.XX.product.utils;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class ListToCsvUtil {
/**
* @param list 对象列表
* @param clazz 对象.class
* @param header 是否需要表头
* @return 对象属性值字符串
*/
public String list2Csv(List list, Class clazz,Boolean header) {
//创建
List allFields = new ArrayList<>(100);
// 获取当前对象的所有属性字段
// clazz.getFields():获取public修饰的字段
// clazz.getDeclaredFields(): 获取所有的字段包括private修饰的字段
allFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
// 获取所有父类的字段, 父类中的字段需要逐级获取
Class clazzSuper = clazz.getSuperclass();
// 如果父类不是object,表明其继承的有其他类。 逐级获取所有父类的字段
while (clazzSuper != Object.class) {
allFields.addAll(Arrays.asList(clazzSuper.getDeclaredFields()));
clazzSuper = clazzSuper.getSuperclass();
}
StringBuilder stringBuilder = new StringBuilder();
if(header) {
List fields = allFields.stream().map(Field::getName).map(String::valueOf).collect(Collectors.toList());
String headerString = String.join(",", fields);
stringBuilder.append(headerString).append("\n");
}
// 设置字段可访问, 否则无法访问private修饰的变量值
for (Field field : allFields) {
field.setAccessible(true);
}
for (T item : list) {
List objectList = new ArrayList<>();
for (Field field : allFields) {
try {
// 获取字段名称
// String fieldName = field.getName();
// 获取指定对象的当前字段的值
String fieldVal = field.get(item)+"";
objectList.add(fieldVal);
// System.out.println(fieldName + "=" + fieldVal);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
String oneObject = String.join(",", objectList).replaceAll("\r\n"," ");
stringBuilder.append(oneObject).append("\n");
}
for (Field field : allFields) {
field.setAccessible(false);
}
return stringBuilder.toString();
}
}
测试:
关键词 :mybatis分页、慢sql、耗时久
场景:没有表关联、纯粹因为查询表数据较大,导致查询失败,一般是数据导出类的场景遇见
思路:分批查询。最后再用 list.addAll(部分list) 得到最终结果。如果结果过大,则考虑 批次设置稍大一点,文件分块传输。
分页代码:
ArrayList resultList = new ArrayList<>();
Long productCount = saleProductsService.selectCount(List.of(ProductConst.ONLINE,
for (int pageIndex = 0; pageIndex < (productCount / pageSize) + 1; pageIndex++) {
List awsTrueFitBySize = getProductsByStatus(tableSuffix, pageIndex + 1, pageSize);
if(!CollectionUtils.isEmpty(awsTrueFitBySize)) {
resultList.addAll(awsTrueFitBySize);
}
}
return resultList;
public List getProductsByStatus(List status, Integer start, Integer pageSize) {
//如果使用XXXMapper调用 则为 XXMapper.selectPage(xx,xx);//可看源码 很清晰
IPage page = new Page<>(start, pageSize);
page.setRecords(new ArrayList());
return page(page, new LambdaQueryWrapper()
.in(SaleProducts::getProductStatus,status)
.isNull(SaleProducts::getDeletedAt)
.orderByAsc(SaleProducts::getId))
.getRecords();
}
上面其实就是一个元素,实际应该为 List.of("1","2","5","7","8","15")
实际执行的sql日志如下:(原本期望,应该有 )
使用技巧:
List.of(数组)
Arrays.asList(数组)
场景:放入缓存的对象,再取出时,发现缺少下划线
原因分析:发现缺少下划线的字段,都是用了 @JsonProperty注解进行标注,而放入缓存时,使用的是JSON.toJSONString() 方法转换成字符串进行存储的。经过实验测试,发现 JSON.toJSONString()方法打印的原始的属性名称。
解决办法:使用:@JSONField代替@JsonProperty
对象上使用注解
JSON.toJSONString(实例对象)//方法,会打印 原始对象 ,如:打印的是json中的key是将是 minPrice
扩展:@JSONField代替@JsonProperty的区别
①所属包不一样,@JSONField是fastjosn包里的,@JsonProperty是json包里的
②转换对象,调用配合的方法不一样。
@JsonProperty 搭配ObjectMapper().writeValueAsString(实体类)方法使用,将实体类转换成字符串。搭配ObjectMapper().readValue(字符串)方法使用,将字符串转换成实体类
@JSONField 搭配JSON.toJSONString(实体类)方法使用,将实体类转换成json字符串。搭配JSON.parseObject(字符串,实体类.class)方法使用,将字符串转换成实体类。
③ @JSONField 是转换成JSON字符串时,使用JSON.toJSONString()别名生效。
@ JsonProperty是转换成 JSON字符串时,使用JSON.toJSONString()别名不效。
如果既想 转对象也别名生效、又想转字符串也生效,那么就两个一块加!如:
扩展:
1、将json字符串转换成对象时,使用@JsonProperty是无效的,需要使用@JSONField
2、JSON字符串中含有不同key但都是同级,需求又是想把它们作为同级 看做是一个list时,定义成Map即可。
如:A,B.C同级 其中B是列表:
{
"A": "1",
"B": [
"21",
"22",
"23"
],
"C": 3
}
这定义的JAVA对象是:
class Obejct{
private String A;
private List B;
private String C;
}
JSON是如下情况时,应该使用map转换
{
"A":"1",
"B":{
"B1":"21",
"B2":"22",
"B3":"23"
},
"C":3
}
class Obejct{
private String A;
private Map B;
private String C;
}
测试结果
总结:对象本质就是Map,只不过的每个属性可以定义成不同的类型
场景:使用fastjson解析json时,有些环境下,用boolean类型解析不到 is开头的属性
解决办法:尽量不使用is开头的属性,或者使用字符串去接收该json的值
场景:有时候,我们业务需要处理 较大的列表,可能会拿着这个列表去执行sql,导致查询慢、查询sql的结果大,甚至内存溢出。这时候就需要我们 把 大任务,分小点去处理。
分析:按正常操作,使用 写个循环,算区间值,取区间XXList.subList(start,end) ,能做 但是麻烦
更好的解决办法:使用java自带的 Lists.partion(列表,分批大小),遍历块,处理块!
//创建20个元素的list
List list = new ArrayList<>(22);
for (int i = 0; i < 22; i++) {
list.add(i);
}
//按5个一组进行处理(输出)
List> partition = Lists.partition(list, 5);
for (List integers : partition) {
System.out.println(integers);
}
输出:
[0, 1, 2, 3, 4]
[5, 6, 7, 8, 9]
[10, 11, 12, 13, 14]
[15, 16, 17, 18, 19]
[20, 21]
场景:上传数据过程中,调用第三方接口,每分钟不能超过100次请求,超过后将被直接抛出异常,多线程情况下,没法确保 单位时间内的请求是否过于密集
分析:能不能参考缓存的涉及,生产这随便生产,但我消费者按固定速度去取?
解决办法:使用限流器
google开源工具包guava提供了限流工具类RateLimiter,该类基于“令牌桶算法”,非常方便使用。
代码使用:
import com.alibaba.nacos.shaded.com.google.common.util.concurrent.RateLimiter;
import org.springframework.util.StopWatch;
。。。。
public static void main(String[] args) {
//1分钟内调test的次数不超过10次
//限流器
RateLimiter rateLimiter = RateLimiter.create(10);//其实参数就是QPS的值
for (int i = 0; i < 1000; i++) {
//计时器
StopWatch stopwatch = new StopWatch();
stopwatch.start();
//执行目标函数
rateLimiter.acquire();
test();
//打印执行多久
stopwatch.stop();
System.out.println( stopwatch.getTotalTimeMillis());
}
}
public static void test(){
System.out.println("test");
}
执行结果:基本是100ms执行一次,实现了1秒执行10下的机制
多线程下使用:无论创建1个线程还是20个线程,下列输出只会输出每秒打印一次:
执行了 16669386XX035
import com.alibaba.nacos.shaded.com.google.common.util.concurrent.RateLimiter;
public class JsonTest {
public static void main(String[] args) {
//1分钟内调test的次数不超过100次
//限流器
RateLimiter rateLimiter = RateLimiter.create(1);
for (int i = 0; i < 20; i++) { //101执行
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if (rateLimiter.tryAcquire()) {
//执行
tagrget();
}
}
}
}).start();
}
}
public static void tagrget() {
System.out.println("执行了"+" "+System.currentTimeMillis());
}
}
另外,多线程使用同一个加锁的函数,让函数进行睡眠,也能起到限制调用
实现限流器的算法一般:
场景:在本地运行调试重新,寻找哪些方法比较占cpu、内存
工具:使用 profiler
步骤:
1、选取要观察的应用
2、右键可以看该应用的cpu、head memory 、threads如下:
3、最nb的功能是:(将会监听一段时间,然后你手动停止监听,就可以查看结果了。类似于医生听诊后,得出结论,该结论能查看 每个函数占用的内存和cpu)
听诊结果:
蓝色部分,类似于堆栈信息,下一层级的是上一层级的总和,如
场景:。应用部署一段时间后就停止了停止,日志有出现OOM。新增了3个任务,A任务每10分钟执行一次(2分钟左右能执行完成),B任务有一天执行一次(1小时左右能够执行完成)。AB每次执行均使用同一个线程池,且每个任务需要35个线程执行,C任务每次也需要有35个线程。执行4个小时左右。线程池设置如下:
原因分析:考虑到服务的的限制,A任务执行的又快,所以设置如上。事实上,单独运行A任务时的执行情况是:35个任务到提交到线程池,其中1-8个任务直接拿去现有的线程池开始执行,剩下的35-8=27个任务在linkedBockingQueue阻塞队里里等待,由于没有指定阻塞队里大小(默认:Ineger.max)因此,虽然没有达到 maximumPoolSize,但也不会再创建新线程,直至1-8个任务逐个完成,空闲的线程才去处理被堵塞的。
当B任务开始后。B占用1-8个线程,队列堵塞27个,后来的A的任务每隔10分钟提交35个任务,由于B任务持续60分钟左右才执行完成,则 队列会堵塞,27+60/10 *35=117个任务
同类C任务开始后,会阻塞 117+240/10 *35=957个,最终导致OOM
解决方法:一个项目中应该对不同的任务创建不同的线程池,避免多个任务共用一个线程池
注意: 由此可知,创建线程池时,LinkedBlockingQueue 必须指定阻塞队列的大小!否则线程池往往 maxnumPooLSize没气作用,是个摆设!
阻塞队列有6个种,常见3种如下:
场景:经常调用第三方服务、无非是url、请求头设置(有时候需要将密码设置进请求头)、请求参数 共3个组成,自己编写总是随心所欲没有多少规范,使用的也比较原生,因此想找应该通用的工具类,代码如下:
构建请求函数:
public static HttpRequest httpRequest(String uri, String key, String method, String contents) {
contents = contents == null ? "" : contents;
var builder = HttpRequest.newBuilder();
builder.uri(URI.create(url));
builder.setHeader("content-type", "application/json");
builder.setHeader("api-key", key);
switch (method) {
case "GET":
builder = builder.GET();
break;
case "HEAD":
builder = builder.GET();
break;
case "DELETE":
builder = builder.DELETE();
break;
case "PUT":
builder = builder.PUT(HttpRequest.BodyPublishers.ofString(contents));
break;
case "POST":
builder = builder.POST(HttpRequest.BodyPublishers.ofString(contents));
break;
default:
throw new IllegalArgumentException(String.format("Can't create request for method '%s'", method));
}
return builder.build();
}
发起请求:
private final static HttpClient client = HttpClient.newHttpClient();//全局变量
private static HttpResponse sendRequest(HttpRequest request) throws IOException, InterruptedException {
log.info(String.format("%s: %s", request.method(), request.uri()));
return client.send(request, HttpResponse.BodyHandlers.ofString());
}
响应情况:
public static boolean isSuccessResponse(HttpResponse response) {
try {
int responseCode = response.statusCode();
log.info("\n Response code = " + responseCode);
if (responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_ACCEPTED
|| responseCode == HttpURLConnection.HTTP_NO_CONTENT || responseCode == HttpURLConnection.HTTP_CREATED) {
return true;
}
// We got an error
var msg = response.body();
if (msg != null) {
log.error(String.format("\n MESSAGE: %s", msg));
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
//最终调用示例
场景: 多个线程操作同一个全局变量list时,需要使用线程安全的list
线程安全的三种办法:
其中,CopyOnWriteArrayList效率最高,其原理是 对Arraylist操作前,先拷贝,再进行增删,增删后,再把原指针指向最新的数组对象。因为涉及到创建、拷贝对象,所以适合 读多写少的情况
文章参考:(129条消息) 三种线程安全的List_橙不甜橘不酸的博客-CSDN博客_线程安全list
关键字:集合比较、对象比较
场景:新老脚本改造,需要对比两个数据库的记录是否相等,因此需要分别连接两个数据库,查询出后,对记录进行比较。部分字段又不想参与比较,如id、create_time这些无意义的比较想忽略
思路:集合中的元素,原本可以重写每个类的compare()方法,但是只能通用一直类型的,既然需要大范围的调用,排除该方法。考虑到 toSting()方法,比较每个对象的toString即可
解决:思想对象转map,移除不参与比较的字段,再用toString(),进行比较
package com.XXX.comparedata.utils;
import com.baomidou.mybatisplus.core.toolkit.BeanUtils;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class CompareListData {
public boolean compare(List list1, List list2) {
if (list1.size() != list2.size()) {
return false;
}
//list中的元素转换成map
List
关键词:工具,数据提取
解决:
private Set getInfoByReg(String s, String regex) {
Set imagesIdList = new HashSet<>();
Pattern pattern = Pattern.compile(regex);
java.util.regex.Matcher matcher = pattern.matcher(s);
while (matcher.find()) {
System.out.println(matcher.group(1));
imagesIdList.add(Integer.parseInt(matcher.group(1).replace("\"", "")));
}
return imagesIdList;
}
关键字:技巧、效率
rabitMq的概念关系:
虚拟机上有多个交换机、每个交换机上有多个队列.,虚拟机类似于命名空间的作用
rabbitMq交换机的3种类型:topic、fanout、direct,这么多类型是为了各种匹配队列
当交换机类型是topic时 使用注解监听:直接指名队列即可
@RabbitListener(queues = {"队列名称"}, containerFactory = "occContainerFactory", concurrency = "1")
当交换机类型是direct时,使用注解监听:
@RabbitListener(containerFactory = "XXXContainerFactory",
bindings = {@QueueBinding(
value = @Queue("queueName"),
exchange = @Exchange(value = "XXXexchange"),
key = {"路由key"}
)})
扩展:假如 AAExchange 已经绑定了队列A,现在想让消息也复制一份发送到队列B的做法:
①新建队列B②将队列别 绑定到交换机AAExchange,并绑定和队列A的路由key一样的key
场景:项目越来越大代码越来越难维护,虽然一直本着 3层的结构进行(如下:)开发,但每个人开发人员理解和接触的层名称不太一样。
controller---->service----->dao
解释:早期使用mybatis的xml编写放在了 mapper中或使用hibernate,查数据的定义函数放在了dao层中,每个表在dao中都唯一对应,service层却与数据库中的表不在一一对应,可能会注入多个dao,整合好后再返回数据即可。
controller--->service---->mapper
解释:随着mybatis-plus的使用,越来越多的sql不再写在mapper.xml中,使用代码生成器会生成每个数据库表对应的一个service类,其实这个虽然叫service 但本质上就是 版本1 中的dao层,于是有出现概念模糊的,有时候会把 一个复杂的业务写在 某个表对应的service中,导致后期维护越来越难!!,这也是项目维护的痛点
鉴于版本2,于是出现
controller----->bus----->service----->mapper
解释:bus层用来放 复杂的业务(涉及到2+张表以上的查询,因为该层某类要注入2+个以上的 service类),这样就只需要在sevice层中 进行lamba的简单查询语句即可,以便后期需求复用。
总结:随着mybatis-plus和jdk的升级,淡化掉了dao层或mapper层,本质一直都是三层结构,尽量本着:操作数据库的类职能单一 ,不易复用的代码才是真的的业务层
形如以下mapper.selectLisy()查询就不应该出现在逻辑代码中(因为下下面还有map的操作等),
这样写,以后有人使用查询还要再写一遍!
使用某个表对应的service.XXXX();该XXX方法的实现在,该表对应的service中:
关键词:开关、naocs刷新
场景:项目代码部署后,不想来回部署项目,而实现if esle之类的代码生效,这时候可以使用nacos配置中心的功能
方案1实现:
①naocs的该项目的配置文件夹中 配置了 AA=true
②类头上标记刷新注解 :@RefreshScope
③引用naocs的变量 :@Value("${AA:true}") //可以设置默认值 如下:
import org.springframework.cloud.context.config.annotation.RefreshScope;
@RefreshScope //标记这个文件的值会刷新
@Compent
class aaa{
//引用naocs中的变量
@Value("${AA}")
pribate Boolean aaaa;
public void sss(){
//业务代码
if(aaaa){
//todo1
}esle{
//todo2
}
}
}
方案2实现:推荐(遇到一次方案1不生效,即使改了naocs的变量值,控制台打印了AA change了 但值就是没变化)
新创建配置类,该类只有各种naocs上的变量,如下:
使用值的地方用 bean注入的方式 :
@Auowire
priavte NacosConfig naocos;
。。。。。
Boolean = nacos.getBasic();
。。。。
79、idea必装插件:
为了提高开发效率,工具的使用贼方便
①界面主题:Material Theme Ui 选Moonlight主题
②MyBatisX
③GitToolBox 点击某行代码,显示代码谁某时写的
④Grep Console 方便看控制台日志,可以自定义设置颜色
⑤Maven Helper 能够查看maven的jar依赖、冲突
⑥ SequenceDiahram 对函数右键 选择后,生成 调用链,方便理解和阅读
⑦Translation 翻译
⑧Github Copilot (Ai代码机器人,收费,淘宝50认证学生一年免费)
由于文字太大:分2篇
(146条消息) 开发中遇到的问题和经验 记录 ------- 后端篇(2)_飞花落雨的博客-CSDN博客