单体项目开发
客户端发出请求的几种方式
地址栏中输入请求地址后回车发出请求 同步
通过超链接发出请求 同步
通过form表单发出请求 同步
通过前端框架发出异步请求
服务器端接收请求参数的几种方式
通过HttpServletRequest对象获取参数 (以后基本不用)
通过在处理请求的方法参数列表处声明的方式接收参数
通过在处理请求的方法参数列表处声明自定义对象的方式接收参数
V:View视图 指前端页面相关代码 ,前端MVC是将此部分代码再划分为三部分
C:Controller控制器, 对应Controller代码
M:Model模型 指数据模型相关, 指Mapper相关代码
1.文件上传
Vue中代码
<!-- 修改头像
action:上传头像的地址
name:上传头像的名称
limit:设置上传头像的数量
on-remove 文件列表移除文件时的钩子
on-preview 点击文件列表中已上传的文件时的钩子
-->
<el-upload
class="upload-demo"
name="file"
action="http://localhost:9080/upload"
:on-success="success"
:on-preview="handlePreview"
:on-remove="handleRemove"
:limit="1"
:on-exceed="handleExceed"
:file-list="fileList">
<el-button size="small" type="primary">修改头像</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
</el-upload>
methods: {
//头像上传 B
handleRemove(file, fileList) {
console.log(file, fileList);
},
handlePreview(file) {
console.log(file);
},
handleExceed(files, fileList) {
this.$message.warning(`当前限制选择 3 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`);
},
// 头像上传成功的钩子
success(response, file, fileLis){
this.url=response;
console.log(response)
}
// 头像上传 End
},
@RequestMapping("/upload")
public String update(MultipartFile file) throws IOException {
//file 代表客户端上传的文件参数
System.out.println("上传文件成功"+file);
String filename = file.getOriginalFilename();
System.out.println("原始文件名"+filename);
// 得到原始文件名的后缀
String substring = filename.substring(filename.lastIndexOf("."));
filename = UUID.randomUUID()+substring;
System.out.println(filename);
//准备保存文件的路径
String filePath = "d:/image";
File dirfile = new File(filePath);
if (!dirfile.exists()){
dirfile.mkdirs();//创建文件夹
}
//得到完整路径 ../avatar/xxx.jpg
String filePathto = dirfile + "/" + filename;
//把图片保存到上面的路径中 异常抛出
file.transferTo(new File(filePathto));
String result="http://localhost:9080/"+filename;
System.out.println(result);
return result;
}
# 配置静态资源文件夹classpath:static 指的是原来的Static文件夹
spring:
web:
resources:
static-locations: file:d:/image
2.获取ip地址
public static void main(String[] args) {
try {
InetAddress address = InetAddress.getLocalHost();
//获取的是本地的IP地址 //PC-20140317PXKX/192.168.0.121
String hostAddress = address.getHostAddress());//192.168.0.121
InetAddress address1 = InetAddress.getByName("www.wodexiangce.cn");//获取的是该网站的ip地址,比如我们所有的请求都通过nginx的,所以这里获取到的其实是nginx服务器的IP地
String hostAddress1 = address1.getHostAddress());//124.237.121.122
InetAddress[] addresses = InetAddress.getAllByName("www.baidu.com");//根据主机名返回其可能的所有InetAddress对象
for(InetAddress addr:addresses){
System.out.println(addr);//www.baidu.com/14.215.177.38
//www.baidu.com/14.215.177.37
}
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
获取本地IP(已经测试)
// 获取服务器ip的方法
@Test
public String getServerIP() {
String ip = null;
try {
//获取当前服务器ip
ip = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
log.error("获取当前服务器ip报错", e);
}
return ip;
}
获取客户端Ip地址
//传入request对象,获得客户端ip
//注意,本地不行,本地会获取到0:0:0:0:0:0:0:1;服务器上是正常的
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
//本地会获取到0:0:0:0:0:0:0:1
ip = request.getRemoteAddr();
}
if (ip.contains(",")) {
return ip.split(",")[0];
} else {
return ip;
}
}
3.邮箱发送验证码
添加依赖
<!-- 邮件服务 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- Thymeleaf 模版,用于发送模版邮件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
配置文件
#关于邮箱的配置
spring:
mail:
default-encoding: UTF-8
host: smtp.163.com #邮箱类型
password: RJGWVJDLDPUSDWPB # 邮箱授权码
properties:
mail:
smtp:
ssl:
enable: true
username: yanpeng01012022@163.com # 发送邮箱的账号
添加工具配置类:SendEmailCode
package com.example.ones.util;
import com.example.ones.ex.ServiceCode;
import com.example.ones.ex.ax.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.util.Random;
/**
* 邮箱验证码发送
*/
@Slf4j
public class SendEmailCode {
@Autowired
private JavaMailSenderImpl mailSender;
public String sentCode(String email) {
String codeNum = "";
try {
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true);
int [] code = new int[3];
Random random = new Random();
//自动生成验证码
for (int i = 0; i < 6; i++) {
int num = random.nextInt(10) + 48;
int uppercase = random.nextInt(26) + 65;
int lowercase = random.nextInt(26) + 97;
code[0] = num;
code[1] = uppercase;
code[2] = lowercase;
codeNum+=(char)code[random.nextInt(3)];
}
//标题
helper.setSubject("您的验证码为:"+codeNum);
//内容
helper.setText("有效时间为10分钟!请尽快使用!");
helper.setText("您好!,感谢您的支持。" +
"您的验证码为:"+"<h2>"+codeNum+"</h2>"
+"千万不能告诉别人哦!",true);
//邮件接收者
helper.setTo("1372065855@qq.com");
//邮件发送者,必须和配置文件里的一样,不然授权码匹配不上
helper.setFrom("yanpeng01012022@163.com");
log.info("验证码为"+codeNum);
mailSender.send(mimeMessage);
log.info("邮件发送成功!");
}catch ( Exception e ) {
throw new ServiceException(ServiceCode.Email,"抱歉!验证码发送失败!请稍后再试,或联系管理员");
}
return codeNum;
}
}
使用
//发送验证码
SendEmailCode sendEmailCode=new SendEmailCode();
String sentCode = sendEmailCode.sentCode(email);
4.随机生成昵称
工具类RandomName
package com.example.ones.util;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Random;
/**
* @Title: randomName
* @Description: 随机取名字
* @param simple 是否单姓
* @param len 生成姓名长度
* @return String 名字
*/
public class RandomName {
/**方法2*/
public static String randomName(boolean simple, int len) {
String surName[] = {
"赵","钱","孙","李","周","吴","郑","王","冯","陈","楮","卫","蒋","沈","韩","杨",
"朱","秦","尤","许","何","吕","施","张","孔","曹","严","华","金","魏","陶","姜",
"戚","谢","邹","喻","柏","水","窦","章","云","苏","潘","葛","奚","范","彭","郎",
"鲁","韦","昌","马","苗","凤","花","方","俞","任","袁","柳","酆","鲍","史","唐",
"费","廉","岑","薛","雷","贺","倪","汤","滕","殷","罗","毕","郝","邬","安","常",
"乐","于","时","傅","皮","卞","齐","康","伍","余","元","卜","顾","孟","平","黄",
"和","穆","萧","尹","姚","邵","湛","汪","祁","毛","禹","狄","米","贝","明","臧",
"计","伏","成","戴","谈","宋","茅","庞","熊","纪","舒","屈","项","祝","董","梁",
"杜","阮","蓝","闽","席","季","麻","强","贾","路","娄","危","江","童","颜","郭",
"梅","盛","林","刁","锺","徐","丘","骆","高","夏","蔡","田","樊","胡","凌","霍",
"虞","万","支","柯","昝","管","卢","莫","经","房","裘","缪","干","解","应","宗",
"丁","宣","贲","邓","郁","单","杭","洪","包","诸","左","石","崔","吉","钮","龚",
"程","嵇","邢","滑","裴","陆","荣","翁","荀","羊","於","惠","甄","麹","家","封",
"芮","羿","储","靳","汲","邴","糜","松","井","段","富","巫","乌","焦","巴","弓",
"牧","隗","山","谷","车","侯","宓","蓬","全","郗","班","仰","秋","仲","伊","宫",
"宁","仇","栾","暴","甘","斜","厉","戎","祖","武","符","刘","景","詹","束","龙",
"叶","幸","司","韶","郜","黎","蓟","薄","印","宿","白","怀","蒲","邰","从","鄂",
"索","咸","籍","赖","卓","蔺","屠","蒙","池","乔","阴","郁","胥","能","苍","双",
"闻","莘","党","翟","谭","贡","劳","逄","姬","申","扶","堵","冉","宰","郦","雍",
"郤","璩","桑","桂","濮","牛","寿","通","边","扈","燕","冀","郏","浦","尚","农",
"温","别","庄","晏","柴","瞿","阎","充","慕","连","茹","习","宦","艾","鱼","容",
"向","古","易","慎","戈","廖","庾","终","暨","居","衡","步","都","耿","满","弘",
"匡","国","文","寇","广","禄","阙","东","欧","殳","沃","利","蔚","越","夔","隆",
"师","巩","厍","聂","晁","勾","敖","融","冷","訾","辛","阚","那","简","饶","空",
"曾","毋","沙","乜","养","鞠","须","丰","巢","关","蒯","相","查","后","荆","红",
"游","竺","权","逑","盖","益","桓","公","晋","楚","阎","法","汝","鄢","涂","钦",
"岳","帅","缑","亢","况","后","有","琴","商","牟","佘","佴","伯","赏","墨","哈",
"谯","笪","年","爱","阳","佟"};
String doubleSurName[] = {"万俟","司马","上官","欧阳","夏侯","诸葛","闻人","东方",
"赫连","皇甫","尉迟","公羊","澹台","公冶","宗政","濮阳","淳于","单于","太叔","申屠",
"公孙","仲孙","轩辕","令狐","锺离","宇文","长孙","慕容","鲜于","闾丘","司徒","司空",
"丌官","司寇","仉","督","子车","颛孙","端木","巫马","公西","漆雕","乐正","壤驷","公良",
"拓拔","夹谷","宰父","谷梁","段干","百里","东郭","南门","呼延","归","海","羊舌","微生",
"梁丘","左丘","东门","西门","南宫"};
String[] word = {"一","乙","二","十","丁","厂","七","卜","人","入","八","九","几","儿","了","力","乃","刀","又",
"三","于","干","亏","士","工","土","才","寸","下","大","丈","与","万","上","小","口","巾","山",
"千","乞","川","亿","个","勺","久","凡","及","夕","丸","么","广","亡","门","义","之","尸","弓",
"己","已","子","卫","也","女","飞","刃","习","叉","马","乡","丰","王","井","开","夫","天","无",
"元","专","云","扎","艺","木","五","支","厅","不","太","犬","区","历","尤","友","匹","车","巨",
"牙","屯","比","互","切","瓦","止","少","日","中","冈","贝","内","水","见","午","牛","手","毛",
"气","升","长","仁","什","片","仆","化","仇","币","仍","仅","斤","爪","反","介","父","从","今",
"凶","分","乏","公","仓","月","氏","勿","欠","风","丹","匀","乌","凤","勾","文","六","方","火",
"为","斗","忆","订","计","户","认","心","尺","引","丑","巴","孔","队","办","以","允","予","劝",
"双","书","幻","玉","刊","示","末","未","击","打","巧","正","扑","扒","功","扔","去","甘","世",
"古","节","本","术","可","丙","左","厉","右","石","布","龙","平","灭","轧","东","卡","北","占",
"业","旧","帅","归","且","旦","目","叶","甲","申","叮","电","号","田","由","史","只","央","兄",
"叼","叫","另","叨","叹","四","生","失","禾","丘","付","仗","代","仙","们","仪","白","仔","他",
"斥","瓜","乎","丛","令","用","甩","印","乐","句","匆","册","犯","外","处","冬","鸟","务","包",
"饥","主","市","立","闪","兰","半","汁","汇","头","汉","宁","穴","它","讨","写","让","礼","训",
"必","议","讯","记","永","司","尼","民","出","辽","奶","奴","加","召","皮","边","发","孕","圣",
"对","台","矛","纠","母","幼","丝","式","刑","动","扛","寺","吉","扣","考","托","老","执","巩",
"圾","扩","扫","地","扬","场","耳","共","芒","亚","芝","朽","朴","机","权","过","臣","再","协",
"西","压","厌","在","有","百","存","而","页","匠","夸","夺","灰","达","列","死","成","夹","轨",
"邪","划","迈","毕","至","此","贞","师","尘","尖","劣","光","当","早","吐","吓","虫","曲","团",
"同","吊","吃","因","吸","吗","屿","帆","岁","回","岂","刚","则","肉","网","年","朱","先","丢",
"舌","竹","迁","乔","伟","传","乒","乓","休","伍","伏","优","伐","延","件","任","伤","价","份",
"华","仰","仿","伙","伪","自","血","向","似","后","行","舟","全","会","杀","合","兆","企","众",
"爷","伞","创","肌","朵","杂","危","旬","旨","负","各","名","多","争","色","壮","冲","冰","庄",
"庆","亦","刘","齐","交","次","衣","产","决","充","妄","闭","问","闯","羊","并","关","米","灯",
"州","汗","污","江","池","汤","忙","兴","宇","守","宅","字","安","讲","军","许","论","农","讽",
"设","访","寻","那","迅","尽","导","异","孙","阵","阳","收","阶","阴","防","奸","如","妇","好",
"她","妈","戏","羽","观","欢","买","红","纤","级","约","纪","驰","巡","寿","弄","麦","形","进",
"戒","吞","远","违","运","扶","抚","坛","技","坏","扰","拒","找","批","扯","址","走","抄","坝",
"贡","攻","赤","折","抓","扮","抢","孝","均","抛","投","坟","抗","坑","坊","抖","护","壳","志",
"扭","块","声","把","报","却","劫","芽","花","芹","芬","苍","芳","严","芦","劳","克","苏","杆",
"杠","杜","材","村","杏","极","李","杨","求","更","束","豆","两","丽","医","辰","励","否","还",
"歼","来","连","步","坚","旱","盯","呈","时","吴","助","县","里","呆","园","旷","围","呀","吨",
"足","邮","男","困","吵","串","员","听","吩","吹","呜","吧","吼","别","岗","帐","财","针","钉",
"告","我","乱","利","秃","秀","私","每","兵","估","体","何","但","伸","作","伯","伶","佣","低",
"你","住","位","伴","身","皂","佛","近","彻","役","返","余","希","坐","谷","妥","含","邻","岔",
"肝","肚","肠","龟","免","狂","犹","角","删","条","卵","岛","迎","饭","饮","系","言","冻","状",
"亩","况","床","库","疗","应","冷","这","序","辛","弃","冶","忘","闲","间","闷","判","灶","灿",
"弟","汪","沙","汽","沃","泛","沟","没","沈","沉","怀","忧","快","完","宋","宏","牢","究","穷",
"灾","良","证","启","评","补","初","社","识","诉","诊","词","译","君","灵","即","层","尿","尾",
"迟","局","改","张","忌","际","陆","阿","陈","阻","附","妙","妖","妨","努","忍","劲","鸡","驱",
"纯","纱","纳","纲","驳","纵","纷","纸","纹","纺","驴","纽","奉","玩","环","武","青","责","现",
"表","规","抹","拢","拔","拣","担","坦","押","抽","拐","拖","拍","者","顶","拆","拥","抵","拘",
"势","抱","垃","拉","拦","拌","幸","招","坡","披","拨","择","抬","其","取","苦","若","茂","苹",
"苗","英","范","直","茄","茎","茅","林","枝","杯","柜","析","板","松","枪","构","杰","述","枕",
"丧","或","画","卧","事","刺","枣","雨","卖","矿","码","厕","奔","奇","奋","态","欧","垄","妻",
"轰","顷","转","斩","轮","软","到","非","叔","肯","齿","些","虎","虏","肾","贤","尚","旺","具",
"果","味","昆","国","昌","畅","明","易","昂","典","固","忠","咐","呼","鸣","咏","呢","岸","岩",
"帖","罗","帜","岭","凯","败","贩","购","图","钓","制","知","垂","牧","物","乖","刮","秆","和",
"季","委","佳","侍","供","使","例","版","侄","侦","侧","凭","侨","佩","货","依","的","迫","质",
"欣","征","往","爬","彼","径","所","舍","金","命","斧","爸","采","受","乳","贪","念","贫","肤",
"肺","肢","肿","胀","朋","股","肥","服","胁","周","昏","鱼","兔","狐","忽","狗","备","饰","饱",
"饲","变","京","享","店","夜","庙","府","底","剂","郊","废","净","盲","放","刻","育","闸","闹",
"郑","券","卷","单","炒","炊","炕","炎","炉","沫","浅","法","泄","河","沾","泪","油","泊","沿",
"泡","注","泻","泳","泥","沸","波","泼","泽","治","怖","性","怕","怜","怪","学","宝","宗","定",
"宜","审","宙","官","空","帘","实","试","郎","诗","肩","房","诚","衬","衫","视","话","诞","询",
"该","详","建","肃","录","隶","居","届","刷","屈","弦","承","孟","孤","陕","降","限","妹","姑",
"姐","姓","始","驾","参","艰","线","练","组","细","驶","织","终","驻","驼","绍","经","贯","奏",
"春","帮","珍","玻","毒","型","挂","封","持","项","垮","挎","城","挠","政","赴","赵","挡","挺",
"括","拴","拾","挑","指","垫","挣","挤","拼","挖","按","挥","挪","某","甚","革","荐","巷","带",
"草","茧","茶","荒","茫","荡","荣","故","胡","南","药","标","枯","柄","栋","相","查","柏","柳",
"柱","柿","栏","树","要","咸","威","歪","研","砖","厘","厚","砌","砍","面","耐","耍","牵","残",
"殃","轻","鸦","皆","背","战","点","临","览","竖","省","削","尝","是","盼","眨","哄","显","哑",
"冒","映","星","昨","畏","趴","胃","贵","界","虹","虾","蚁","思","蚂","虽","品","咽","骂","哗",
"咱","响","哈","咬","咳","哪","炭","峡","罚","贱","贴","骨","钞","钟","钢","钥","钩","卸","缸",
"拜","看","矩","怎","牲","选","适","秒","香","种","秋","科","重","复","竿","段","便","俩","贷",
"顺","修","保","促","侮","俭","俗","俘","信","皇","泉","鬼","侵","追","俊","盾","待","律","很",
"须","叙","剑","逃","食","盆","胆","胜","胞","胖","脉","勉","狭","狮","独","狡","狱","狠","贸",
"怨","急","饶","蚀","饺","饼","弯","将","奖","哀","亭","亮","度","迹","庭","疮","疯","疫","疤",
"姿","亲","音","帝","施","闻","阀","阁","差","养","美","姜","叛","送","类","迷","前","首","逆",
"总","炼","炸","炮","烂","剃","洁","洪","洒","浇","浊","洞","测","洗","活","派","洽","染","济",
"洋","洲","浑","浓","津","恒","恢","恰","恼","恨","举","觉","宣","室","宫","宪","突","穿","窃",
"客","冠","语","扁","袄","祖","神","祝","误","诱","说","诵","垦","退","既","屋","昼","费","陡",
"眉","孩","除","险","院","娃","姥","姨","姻","娇","怒","架","贺","盈","勇","怠","柔","垒","绑",
"绒","结","绕","骄","绘","给","络","骆","绝","绞","统","耕","耗","艳","泰","珠","班","素","蚕",
"顽","盏","匪","捞","栽","捕","振","载","赶","起","盐","捎","捏","埋","捉","捆","捐","损","都",
"哲","逝","捡","换","挽","热","恐","壶","挨","耻","耽","恭","莲","莫","荷","获","晋","恶","真",
"框","桂","档","桐","株","桥","桃","格","校","核","样","根","索","哥","速","逗","栗","配","翅",
"辱","唇","夏","础","破","原","套","逐","烈","殊","顾","轿","较","顿","毙","致","柴","桌","虑",
"监","紧","党","晒","眠","晓","鸭","晃","晌","晕","蚊","哨","哭","恩","唤","啊","唉","罢","峰",
"圆","贼","贿","钱","钳","钻","铁","铃","铅","缺","氧","特","牺","造","乘","敌","秤","租","积",
"秧","秩","称","秘","透","笔","笑","笋","债","借","值","倚","倾","倒","倘","俱","倡","候","俯",
"倍","倦","健","臭","射","躬","息","徒","徐","舰","舱","般","航","途","拿","爹","爱","颂","翁",
"脆","脂","胸","胳","脏","胶","脑","狸","狼","逢","留","皱","饿","恋","桨","浆","衰","高","席",
"准","座","脊","症","病","疾","疼","疲","效","离","唐","资","凉","站","剖","竞","部","旁","旅",
"畜","阅","羞","瓶","拳","粉","料","益","兼","烤","烘","烦","烧","烛","烟","递","涛","浙","涝",
"酒","涉","消","浩","海","涂","浴","浮","流","润","浪","浸","涨","烫","涌","悟","悄","悔","悦",
"害","宽","家","宵","宴","宾","窄","容","宰","案","请","朗","诸","读","扇","袜","袖","袍","被",
"祥","课","谁","调","冤","谅","谈","谊","剥","恳","展","剧","屑","弱","陵","陶","陷","陪","娱",
"娘","通","能","难","预","桑","绢","绣","验","继","球","理","捧","堵","描","域","掩","捷","排",
"掉","堆","推","掀","授","教","掏","掠","培","接","控","探","据","掘","职","基","著","勒","黄",
"萌","萝","菌","菜","萄","菊","萍","菠","营","械","梦","梢","梅","检","梳","梯","桶","救","副",
"票","戚","爽","聋","袭","盛","雪","辅","辆","虚","雀","堂","常","匙","晨","睁","眯","眼","悬",
"野","啦","晚","啄","距","跃","略","蛇","累","唱","患","唯","崖","崭","崇","圈","铜","铲","银",
"甜","梨","犁","移","笨","笼","笛","符","第","敏","做","袋","悠","偿","偶","偷","您","售","停",
"偏","假","得","衔","盘","船","斜","盒","鸽","悉","欲","彩","领","脚","脖","脸","脱","象","够",
"猜","猪","猎","猫","猛","馅","馆","凑","减","毫","麻","痒","痕","廊","康","庸","鹿","盗","章",
"竟","商","族","旋","望","率","着","盖","粘","粗","粒","断","剪","兽","清","添","淋","淹","渠",
"渐","混","渔","淘","液","淡","深","婆","梁","渗","情","惜","惭","悼","惧","惕","惊","惨","惯",
"寇","寄","宿","窑","密","谋","谎","祸","谜","逮","敢","屠","弹","随","蛋","隆","隐","婚","婶",
"颈","绩","绪","续","骑","绳","维","绵","绸","绿","琴","斑","替","款","堪","搭","塔","越","趁",
"趋","超","提","堤","博","揭","喜","插","揪","搜","煮","援","裁","搁","搂","搅","握","揉","斯",
"期","欺","联","散","惹","葬","葛","董","葡","敬","葱","落","朝","辜","葵","棒","棋","植","森",
"椅","椒","棵","棍","棉","棚","棕","惠","惑","逼","厨","厦","硬","确","雁","殖","裂","雄","暂",
"雅","辈","悲","紫","辉","敞","赏","掌","晴","暑","最","量","喷","晶","喇","遇","喊","景","践",
"跌","跑","遗","蛙","蛛","蜓","喝","喂","喘","喉","幅","帽","赌","赔","黑","铸","铺","链","销",
"锁","锄","锅","锈","锋","锐","短","智","毯","鹅","剩","稍","程","稀","税","筐","等","筑","策",
"筛","筒","答","筋","筝","傲","傅","牌","堡","集","焦","傍","储","奥","街","惩","御","循","艇",
"舒","番","释","禽","腊","脾","腔","鲁","猾","猴","然","馋","装","蛮","就","痛","童","阔","善",
"羡","普","粪","尊","道","曾","焰","港","湖","渣","湿","温","渴","滑","湾","渡","游","滋","溉",
"愤","慌","惰","愧","愉","慨","割","寒","富","窜","窝","窗","遍","裕","裤","裙","谢","谣","谦",
"属","屡","强","粥","疏","隔","隙","絮","嫂","登","缎","缓","编","骗","缘","瑞","魂","肆","摄",
"摸","填","搏","塌","鼓","摆","携","搬","摇","搞","塘","摊","蒜","勤","鹊","蓝","墓","幕","蓬",
"蓄","蒙","蒸","献","禁","楚","想","槐","榆","楼","概","赖","酬","感","碍","碑","碎","碰","碗",
"碌","雷","零","雾","雹","输","督","龄","鉴","睛","睡","睬","鄙","愚","暖","盟","歇","暗","照",
"跨","跳","跪","路","跟","遣","蛾","蜂","嗓","置","罪","罩","错","锡","锣","锤","锦","键","锯",
"矮","辞","稠","愁","筹","签","简","毁","舅","鼠","催","傻","像","躲","微","愈","遥","腰","腥",
"腹","腾","腿","触","解","酱","痰","廉","新","韵","意","粮","数","煎","塑","慈","煤","煌","满",
"漠","源","滤","滥","滔","溪","溜","滚","滨","粱","滩","慎","誉","塞","谨","福","群","殿","辟",
"障","嫌","嫁","叠","缝","缠","静","碧","璃","墙","撇","嘉","摧","截","誓","境","摘","摔","聚",
"蔽","慕","暮","蔑","模","榴","榜","榨","歌","遭","酷","酿","酸","磁","愿","需","弊","裳","颗",
"嗽","蜻","蜡","蝇","蜘","赚","锹","锻","舞","稳","算","箩","管","僚","鼻","魄","貌","膜","膊",
"膀","鲜","疑","馒","裹","敲","豪","膏","遮","腐","瘦","辣","竭","端","旗","精","歉","熄","熔",
"漆","漂","漫","滴","演","漏","慢","寨","赛","察","蜜","谱","嫩","翠","熊","凳","骡","缩","慧",
"撕","撒","趣","趟","撑","播","撞","撤","增","聪","鞋","蕉","蔬","横","槽","樱","橡","飘","醋",
"醉","震","霉","瞒","题","暴","瞎","影","踢","踏","踩","踪","蝶","蝴","嘱","墨","镇","靠","稻",
"黎","稿","稼","箱","箭","篇","僵","躺","僻","德","艘","膝","膛","熟","摩","颜","毅","糊","遵",
"潜","潮","懂","额","慰","劈","操","燕","薯","薪","薄","颠","橘","整","融","醒","餐","嘴","蹄",
"器","赠","默","镜","赞","篮","邀","衡","膨","雕","磨","凝","辨","辩","糖","糕","燃","澡","激",
"懒","壁","避","缴","戴","擦","鞠","藏","霜","霞","瞧","蹈","螺","穗","繁","辫","赢","糟","糠",
"燥","臂","翼","骤","鞭","覆","蹦","镰","翻","鹰","警","攀","蹲","颤","瓣","爆","疆","壤","耀",
"躁","嚼","嚷","籍","魔","灌","蠢","霸","露","囊","罐"};
int surNameLen = surName.length;
int doubleSurNameLen = doubleSurName.length;
int wordLen = word.length;
StringBuffer sb = new StringBuffer();
Random random = new Random();
if(simple){
sb.append(surName[random.nextInt(surNameLen)]);
int surLen = sb.toString().length();
for (int i = 0; i < len - surLen; i++) {
if(sb.toString().length() <= len){
sb.append(word[random.nextInt(wordLen)]);
}
}
}else{
sb.append(doubleSurName[random.nextInt(doubleSurNameLen)]);
int doubleSurLen = sb.toString().length();
for (int i = 0; i < len - doubleSurLen; i++) {
if(sb.toString().length() <= len){
sb.append(word[random.nextInt(wordLen)]);
}
}
}
return sb.toString();
}
}
使用方法
/*
* @param simple 是否单姓
* @param len 生成姓名长度
*/
String s = RandomName.randomName(simple,len);
SpringBoot 项目
1.Mybatis-MySql
1. 数据库编程的依赖
<!-- Mybatis整合Spring Boot的依赖项 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!-- MySQL的依赖项 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
application.properties添加配置如下
# 连接数据库的配置参数
spring.datasource.url=jdbc:mysql://localhost:3306/mall_pms?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
检查配置是否正确在test文件测试 没报错即通过
@Autowired
DataSource dataSource;
@Test
void getConnection() throws Throwable {
dataSource.getConnection();
}
2. Mybatis实现数据库编程
在项目的根包下创建config.MybatisConfiguration配置类(添加了@Configuration注解的类),在此类上配置@MapperScan注解 指定接口文件所在的根包
package cn.tedu.csmall.product.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@MapperScan("cn.tedu.csmall.product.mapper")
@Configuration
public class MybatisConfiguration {
}
在application.properties中添加配置:
# Mybatis相关配置
mybatis.mapper-locations=classpath:mapper/*.xml
# 支持驼峰命名法-yml
configuration:
mapUnderscoreToCamelCase: true
并且,在src/main/resources下创建名为mapper的文件夹。
编写POJO类,先在项目中添加依赖:
<!-- Lombok的依赖项,主要用于简化POJO类的编写 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>provided</scope>
</dependency>
在项目的根包下创建`pojo.entity.xxxxx中声明与数据表对应的各属性
package cn.tedu.csmall.product.pojo.entity;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* @author java@tedu.cn
* @version 0.0.1
*/
@Data
public class Album implements Serializable {
/**
* 记录id
*/
private Long id;
/**
* 相册名称
*/
private String name;
}
在项目的根包下创建mapper.AlbumMapper接口,并在接口中添加“插入1条数据”的抽象方法
在各Mapper接口上添加@Repository注解(防止自动装配报错)
- 与添加
@Mapper注解的本质不同,添加@Mapper注解是为了标识此接口是Mybatis框架应该处理的接口,添加@Repository注解是为了引导IntelliJ IDEA作出正确的判断
package cn.tedu.csmall.product.mapper;
import cn.tedu.csmall.product.pojo.entity.Album;
/**
* 处理相册数据的Mapper接口
*
* @author java@tedu.cn
* @version 0.0.1
*/
@Repository
public interface AlbumMapper {
/**
* 插入相册数据
*
* @param album 相册数据
* @return 受影响的行数
*/
int insert(Album album);
}
在src/main/resource/mapper下,并在此文件中配置以上接口
抽象方法的对应代码:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.tedu.csmall.product.mapper.AlbumMapper">←这里需要对应mapper中的文件
<!-- int insert(Album album); -->
<insert id="insert">
INSERT INTO pms_album (
name, description, sort
) VALUES (
#{name}, #{description}, #{sort}
)
</insert>
</mapper>
mapper数据库语句编写规范
插入数据时获取自动编号的id
在<insert>标签上,可以配置useGeneratedKeys="true"和keyProperty="属性名",将可以获取自动编号的id值,并由Mybatis自动赋值到参数对象的属性(keyProperty配置的值)上
<!-- int insert(Album album); -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO pms_album (
name, description, sort
) VALUES (
#{name}, #{description}, #{sort}
)
</insert>
提示:如果表的id不是自动编号的,则插入数据时必须由方法的调用者给出id值,所以,对于方法的调用者而言,id值是已知的,则不需要配置这2个属性。
mapper中增删改查语句示例
批量插入数据
int insertBatch(List<Album> albums);
<!-- int insertBatch(List<Album> albums); -->
<insert id="insertBatch" useGeneratedKeys="true" keyProperty="id">
INSERT INTO pms_album (
name, description, sort
) VALUES
<foreach collection="list" item="album" separator=",">
(#{album.name}, #{album.description}, #{album.sort})
</foreach>
</insert>
根据id删除数据
int deleteById(Long id);
<!-- int deleteById(Long id); -->
<delete id="deleteById">
DELETE FROM pms_album WHERE id=#{id}
</delete>
根据若干个id批量删除数据
int deleteByIds(Long[] ids);
<!-- int deleteByIds(Long ids); -->
<delete id="deleteByIds">
DELETE FROM pms_album WHERE id IN (
<foreach collection="array" item="id" separator=",">
#{id}
</foreach>
)
</delete>
修改数据
int update(Album album);
<!-- int update(Album album); -->
<update id="update">
UPDATE pms_album
<set>
<if test="name != null">
name=#{name},
</if>
<if test="description != null">
description=#{description},
</if>
<if test="sort != null">
sort=#{sort},
</if>
</set>
WHERE id=#{id}
</update>
统计数据的数量
int count();
在设计“查询”的抽象方法时,关于返回值类型,只需要保证所设计的返回值类型足够“装得下”所需的查询结果即可。
<!-- int count(); -->
<select id="count" resultType="int">
SELECT count(*) FROM pms_album
</select>
注意:每个<select>标签必须配置resultType或resultMap这2个属性中的其中1个
根据id查询数据的详情
创建pojo.vo.AlbumStandardVO类型,在此类型中设计与查询的字段列表匹配的属性
AlbumStandardVO getStandardById(Long id);
<!-- AlbumStandardVO getStandardById(Long id); -->
<select id="getStandardById" resultType="cn.tedu.csmall.product.pojo.vo.AlbumStandardVO">
SELECT id, name, description, sort FROM pms_album WHERE id=#{id}
</select>
注意:每个<select>标签必须配置resultType或resultMap这2个属性中的其中1个。
查询数据列表
创建pojo.vo.AlbumListItemVO类型,在此类型中设计与查询的字段列表匹配的属性
List<AlbumListItemVO> list();
<!-- List<AlbumListItemVO> list(); -->
<select id="list" resultType="cn.tedu.csmall.product.pojo.vo.AlbumListItemVO">
SELECT id, name, description, sort FROM pms_album ORDER BY sort DESC, id DESC
</select>
注意:每个<select>标签必须配置resultType或resultMap这2个属性中的其中1个。
<resultMap>与<sql>标签
<!-- CategoryStandardVO getStandardById(Long id); -->
<select id="getStandardById" resultMap="StandardResultMap">
SELECT
<include refid="StandardQueryFields"/>
FROM
pms_category
WHERE
id=#{id}
</select>
<sql id="StandardQueryFields">
<if test="true">
id, name, parent_id, depth, keywords, sort, icon, enable, is_parent, is_display
</if>
</sql>
<resultMap id="StandardResultMap" type="cn.tedu.csmall.product.pojo.vo.CategoryStandardVO">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="parent_id" property="parentId"/>
<result column="depth" property="depth"/>
<result column="keywords" property="keywords"/>
<result column="sort" property="sort"/>
<result column="icon" property="icon"/>
<result column="enable" property="enable"/>
<result column="is_parent" property="isParent"/>
<result column="is_display" property="isDisplay"/>
</resultMap>
MySQL数据类型与Java类中的数据类型的对应关系
| MySQL数据类型 | Java类中的数据类型 |
|---|---|
tinyint / smallint / int | Integer |
bigint | Long |
char / varchar / text系列 | String |
datetime | LocalDateTime |
Mybatis的占位符中使用的名称
在使用Mybatis框架时,配置的SQL语句中的参数可以使用占位符来表示,例如:
<!-- int deleteById(Long id); -->
<delete id="deleteById">
DELETE FROM ams_admin WHERE id=#{id}
</delete>
以上SQL语句中的#{id}就是占位符。
事实上,当抽象方法的参数只有1个时,在占位符中的名称是完全自由编写的,因为Mybatis框架会自动的查找那唯一参数值代入到SQL执行过程中!当抽象方法的参数超过1个时,在占位符中名称不可以是随意的名称,如果使用的名称错误,则可能出现类似以下错误:
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.binding.BindingException: Parameter 'passwordxxx' not found. Available parameters are [password, id, param1, param2]
在错误提示信息中,已经明确指出:Available parameters are [password, id, param1, param2],即“可用的参数名称是[password, id, param1, param2]”,则配置的SQL语句可以是:
<update id="updatePasswordById">
update ams_admin set password=#{password} where id=#{id}
</update>
或者:
<update id="updatePasswordById">
update ams_admin set password=#{param2} where id=#{param1}
</update>
在较低版本的框架中,为了保证能够使用#{id}、#{password}这种名称的占位符,需要在抽象方法的各参数前添加@Param注解,以配置参数的名称,例如:
int updatePasswordById(@Param("id") Long id, @Param("password") String password);
Mybatis的占位符的格式
在Mybatis中,配置SQL语句时,参数可以使用#{}格式的占位符表示,也可以使用${}格式的占位符来表示!
对于以下查询:
<select id="getStandardById" resultMap="StandardResultMap">
SELECT
<include refid="StandardQueryFields" />
FROM
ams_admin
WHERE
id=#{id}
</select>
无论是使用#{}还是使用${}格式的占位符,执行效果是相同的!
对于以下查询:
<select id="getLoginInfoByUsername" resultMap="LoginResultMap">
SELECT
<include refid="LoginQueryFields" />
FROM
ams_admin
WHERE
username=#{username}
</select>
使用#{}格式的占位符时,是可以正常使用的,使用${}格式的占位符,会出现如下错误:
Caused by: java.sql.SQLSyntaxErrorException: Unknown column 'root' in 'where clause'
2.Lombok框架
Lombok框架的主要作用是通过注解可以在编译期生成某些代码,例如Setters & Getters、hashCode()与equals()、toString()方法等,可以简化开发。
<!-- Lombok的依赖项,主要用于简化POJO类的编写 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>provided</scope>
</dependency>
Lombok的常用注解有:
@Data:添加在类上,可在编译期生成全部属性对应的Setters & Getters、hashCode()与equals()、toString(),使用此注解时,必须保证当前类的父类存在无参数构造方法@Setter:可以添加在属性上,将仅作用于当前属性,也可以添加在类上,将作用于类中所有属性,用于生成对应的Setter方法@Getter:同上,用于生成对应的Getter方法@EqualsAndHashCode:添加在类上,用于生成规范的equals()和hashCode(),关于equals()方法,如果2个对象的所有属性的值完全相同,则返回true,否则返回false,关于hashCode()也是如此,如果2个对象的所有属性的值完全相同,则生成的HashCode值相同,否则,不应该相同@ToString:添加在类上,用于生成全属性对应的toString()方法@Slf4j:添加在类上,用于日志输出 log.info(“---------”)
日志显示级别
trace:跟踪信息,可能包含不一定关注,但是包含了程序执行流程的信息debug:调试信息,可能包含一些敏感内容,比如关键数据的值info:一般信息warn:警告信息error:错误信息
日志的显示级别配置
logging.level.cn.tedu.csmall=error
日志占位符的使用方法
int x = 1;
int y = 2;
log.info("{}+{}={}", x, y, x + y);
3.Knife4j框架
官网地址:快速开始 | Knife4j
添加依赖
<!-- 添加 Knife4j-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.9</version>
</dependency>
添加配置文件Knife4jConfiguration
package com.example.sap.config;
import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
import java.net.InetAddress;
import java.net.UnknownHostException;
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfiguration {
@Autowired
private MyConfig myConfig;
@Bean(value = "defaultApi2")
public Docket defaultApi2() {
String groupName="SAP远程调用接口";
Docket docket= null;
try {
docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder()
.title("VWED_SAP_SECRET_API")
.description("描述")
.termsOfServiceUrl("http://www.vwed.com/")
.contact("programmer3.cc.vwed.com")
.version("1.0")
.build())
//分组名称
.groupName(groupName)
.select()
//这里指定Controller扫描包路径
.apis(RequestHandlerSelectors.basePackage("com.example.sap.controller"))
.paths(PathSelectors.any())
.build();
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
return docket;
}
}
增强与加密配置
knife4j:
production: false
enable: true
basic:
enable: true
username: admin
password: 123456
接口使用示例
/**
* @author Yanpeng
* @version 1.0
* @Title: SAP 通用接口
* @date 2022/2/25 9:00
*/
@Api(tags = "通用接口")
@ApiSupport(author = "YanPeng", order = 1)
@RestController
@RequestMapping("/api/sap")
public class CommonController {
/**
* SAP通用接口
* @return
* @throws JCoException
*/
@ApiOperationSupport(order = 1)
@ApiOperation(value = "SAP通用接口:400", notes = "传入对应参数")
@PostMapping("/400/handleGeneralRequest")
public Res handleGeneralRequestPart(@RequestBody FunctionInfoFrom functionInfoFrom){
JSONArray objects = commonService.handleGeneralRequestPart(functionInfoFrom);
return Res.ok(objects);
}
}
@ApiModel(value = "通用参数")
public class FunctionInfoFrom implements Serializable {
/**
* 函数名称
*/
@ApiModelProperty(value = "函数名称", required = true)
private String functionName;
/**
* 输入参数
*/
@ApiModelProperty(value = "输入参数", required = false)
private List<InputParameter> inputParameters;
/**
* 输出参数
*/
@ApiModelProperty(value = "输出参数", required = false)
private List<OutputParameter> outputParameters;
}
4.关于跨域访问
跨域访问:客户端与服务器端不在同一台服务器上。
在默认情况下,不允许发送跨域访问请求,如果发送,在浏览器的控制台中则会提示如下错误:
Access to XMLHttpRequest at 'http://localhost:9080/albums/add-new' from origin 'http://localhost:9000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
如果使用的是FireFox浏览器,提示信息如下所示:
已拦截跨源请求:同源策略禁止读取位于 http://localhost:9080/albums/add-new 的远程资源。(原因:CORS 头缺少 'Access-Control-Allow-Origin')。状态码:200。
当服务器端允许来自跨域的客户端发送请求时,在Spring Boot项目中,需要使用配置类实现WebMvcConfigurer接口,并重写addCorsMappings()方法进行配置。
则在csmall-product项目中,在根包下创建config.WebMvcConfiguration类,在类上添加@Configuration注解,并实现WebMvcConfigurer接口,重写addCorsMappings()方法:
package cn.tedu.csmall.product.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Spring MVC配置类
*
* @author java@tedu.cn
* @version 0.0.1
*/
@Slf4j
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
public WebMvcConfiguration() {
log.debug("创建配置类对象:WebMvcConfiguration");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedHeaders("*")
.allowedMethods("*")
.allowCredentials(true)
.maxAge(3600);
}
}
5. Validation框架
添加依赖
在pom.xml中添加spring-boot-starter-validation依赖项:
<!-- Spring Boot Validation的依赖项,用于检查请求参数的基本格式 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
检查封装的请求参数
在控制器中,对于封装类型的请求参数,应该先在请求参数之前添加@Valid或@Validated注解,表示将需要对此请求参数的格式进行检查,例如:
@ApiOperation("添加相册")
@ApiOperationSupport(order = 100)
@PostMapping("/add-new")
// ↓↓↓↓↓↓ 以下是新添加的注解
public JsonResult addNew(@Valid AlbumAddNewDTO albumAddNewDTO) {
log.debug("开始处理【添加相册】的请求,参数:{}", albumAddNewDTO);
albumService.addNew(albumAddNewDTO);
log.debug("添加相册成功!");
return JsonResult.ok();
}
然后,在此封装类型中,在需要检查的属性上,添加检查注解,例如可以添加@NotNull注解,此注解表示“不允许为null值”,例如:
@Data
public class AlbumAddNewDTO implements Serializable {
/**
* 相册名称
*/
@ApiModelProperty(value = "相册名称", required = true)
@NotNull // 新添加的注解
private String name;
// 暂不关心其它代码
}
重启项目,如果客户端提交请求时,未提交name请求参数,就会响应400错误,并且,在服务器端的控制台会提示错误:
2022-11-01 11:27:45.398 WARN 15104 --- [nio-9080-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors<EOL>Field error in object 'albumAddNewDTO' on field 'name': rejected value [null]; codes [NotNull.albumAddNewDTO.name,NotNull.name,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [albumAddNewDTO.name,name]; arguments []; default message [name]]; default message [不能为null]]
处理检查不通过时的异常
由于检查未通过时会抛出org.springframework.validation.BindException异常,则可以在全局异常处理器中,添加对此异常的处理,以避免响应400错误到客户端,而是改为响应一段JSON数据:
@ExceptionHandler
public JsonResult handleBindException(BindException e) {
log.debug("开始处理BindException");
StringJoiner stringJoiner = new StringJoiner(",", "请求参数格式错误,", "!!!");
List<FieldError> fieldErrors = e.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
String defaultMessage = fieldError.getDefaultMessage();
stringJoiner.add(defaultMessage);
}
return JsonResult.fail(ServiceCode.ERR_BAD_REQUEST, stringJoiner.toString());
}
当请求参数可能出现多种错误时,也可以选择“快速失败”的机制,它会使得框架只要发现错误,就停止检查其它规则,这需要在配置类中进行配置,则在项目的根包下创建config.ValidationConfiguration类并配置:
package cn.tedu.csmall.product.config;
import cn.tedu.csmall.product.controller.AlbumController;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.validation.Validation;
/**
* Validation配置类
* @author java@tedu.cn
* @version 0.0.1
*/
@Slf4j
@Configuration
public class ValidationConfiguration {
public ValidationConfiguration() {
log.debug("创建配置类对象:ValidationConfiguration");
}
@Bean
public javax.validation.Validator validator() {
return Validation.byProvider(HibernateValidator.class)
.configure() // 开始配置
.failFast(true) // 快速失败,即检查请求参数时,一旦发现某个参数不符合规则,则视为失败,并停止检查(剩余未检查的部分将不会被检查)
.buildValidatorFactory()
.getValidator();
}
}
使用这种做法,当客户端提交的请求参数格式错误时,最多只会发现1种错误,则处理异常的代码可以调整为:
@ExceptionHandler
public JsonResult handleBindException(BindException e) {
log.debug("开始处理BindException");
String defaultMessage = e.getFieldError().getDefaultMessage();
return JsonResult.fail(ServiceCode.ERR_BAD_REQUEST, defaultMessage);
}
检查未封装的请求参数
当处理请求的方法的参数是未封装的(例如Long id等),检查时,需要:
- 在当前控制器类上添加
@Validated注解 - 在需要检查的请求参数上添加检查注解
@Slf4j
@Validated // 新添加的注解
@RestController
@RequestMapping("/albums")
@Api(tags = "04. 相册管理模块")
public class AlbumController {
// http://localhost:8080/albums/9527/delete
@ApiOperation("根据id删除相册")
@ApiOperationSupport(order = 200)
@ApiImplicitParam(name = "id", value = "相册id", required = true, dataType = "long")
@PostMapping("/{id:[0-9]+}/delete")
// 新添加的注解 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
public String delete(@Range(min = 1, message = "删除相册失败,尝试删除的相册的ID无效!")
@PathVariable Long id) {
String message = "尝试删除id值为【" + id + "】的相册";
log.debug(message);
return message;
}
}
当请求参数不符合以上@Range(min =1)的规则时(例如请求参数值为0或负数),默认情况下会出现500错误,在服务器端控制台可以看到以下异常信息:
javax.validation.ConstraintViolationException: delete.id: 删除相册失败,尝试删除的相册的ID无效!
则需要在全局异常处理器中,添加新的处理异常的方法,用于处理以上异常:
@ExceptionHandler
public JsonResult handleConstraintViolationException(ConstraintViolationException e) {
log.debug("开始处理ConstraintViolationException");
StringJoiner stringJoiner = new StringJoiner(",");
Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
for (ConstraintViolation<?> constraintViolation : constraintViolations) {
stringJoiner.add(constraintViolation.getMessage());
}
return JsonResult.fail(ServiceCode.ERR_BAD_REQUEST, stringJoiner.toString());
}
关于检查注解
在javax.validation.constraints和org.hibernate.validator.constraints都有大量的检查注解,常用的检查注解有:
@NotNull:不允许为null@Range:此注解有min和max属性,分别通过@Min和@Max实现,且min的默认值为0,max的默认值为long类型的最大值,此注解只能添加在整型的数值类型上,用于设置取值区间@NotEmpty:不允许为空字符串,即长度为0的字符串,此注解只能添加在字符串类型的参数上@NotBlank:不允许为空白,即不允许是仅由空格、TAB制表位、换行符等空白字符组成的值,此注解只能添加在字符串类型的参数上@Pattern:此注解有regexp属性,可通过此属性配置正则表达式,在检查时将根据正则表达式所配置的规则进行检查,此注解只能添加在字符串类型的参数上// 判断字符串空用这个 @NotBlank(message="姓名必须输入!") private String name; @NotBlank @Length(min=18,max=19,message="身份证长度必须在18-19之间") private String card; @NotNull @Past(message="日期必须必须是当天之前") //@Future // 前台传递日期字符,自动转换成日期对象 @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") // 日期对象输出到前台,自动格式化展示 @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date date; // 判断基本类型空用这个 @NotNull(message="年龄必须输入!") @Max(message="最大年龄28岁!",value=28) @Min(message="最小年龄18岁!",value=18) private Integer age; @NotBlank // string,numeric大小判断 @Range(min=1,max=100,message="weight只能在1-100之间") // 数组,集合大小判断 // @Size(max=100, min=1,message="size只能在1-100之间") @Digits(integer=3,fraction=2,message="weight只能为数字,整数3位以下,小数保留2位") private String weight; @NotNull @AssertTrue(message="性别只能填男!") //@AssertFalse private Boolean sex; // 判断集合空用这个 @NotEmpty(message="集合不能为空!") List<String> list; @Null(message="该字段不能设值!") //@NotNull private Object tmp; @NotBlank @Pattern(regexp="^[150[0-9]+]{11}",message="电话格式有问题!") private String phone; @NotBlank @Email(message="email格式不正确!") private String email; @DecimalMin(value="18",message="dicimal不能小于18!") @DecimalMax(value="20",message="dicimal不能大于20!") private BigDecimal dicimal; *************************************************************************************************************** //被注释的元素,值必须是一个字符串,不能为null,且调用trim()后,长度必须大于0 @NotBlank(message = "") //被注释的元素,值不能为null,但可以为"空",用于基本数据类型的非空校验上,而且被其标注的字段可以使用 @size、@Max、@Min 等对字段数值进行大小的控制 @NotNull(message = "") //被注释的的元素,值不能为null,且长度必须大于0,一般用在集合类上面 @NotEmpty(message = "") //被注释的元素必须符合指定的正则表达式。 @Pattern(regexp = "", message = "") //被注释的元素的大小必须在指定的范围内。 @Size(min =, max =) //被注释的元素,值必须是一个数字,且值必须大于等于指定的最小值 @Min(value = long以内的值, message = "") //被注释的元素,值必须是一个数字,且值必须小于等于指定的最大值 @Max(value = long以内的值, message = "") //被注释的元素,值必须是一个数字,其值必须大于等于指定的最小值 @DecimalMin(value = 可以是小数, message = "") //被注释的元素,值必须是一个数字,其值必须小于等于指定的最大值 @DecimalMax(value = 可以是小数, message = "") //被注释的元素,值必须为null @Null(message = "") //被注释的元素必须是一个数字,其值必须在可接受的范围内 @Digits(integer =, fraction =) //被注释的元素,值必须为true @AssertTrue(message = "") //被注释的元素,值必须为false @AssertFalse(message = "") //被注释的元素必须是一个过去的日期 @Past(message = "") //被注释的元素必须是一个将来的日期 @Future(message = "") //被注释的元素必须是电子邮件地址 @Email(regexp = "", message = "")
注意:除了@NotNull注解以外,其它注解均不检查请求参数为null的情况,例如在某个请求参数上配置了@NotEmpty,当提交的请求参数为null时将通过检查(视为正确),所以,当某个请求参数需要配置为不允许为null时,必须使用@NotNull,且以上不冲突的多个注解可以同时添加在同一个请求参数上!
6. Spring JDBC的事务管理
事务(Transaction):是关系型数据库中一种能够保障多个写操作(增、删、改)要么全部成功,要么全部失败的机制。
在基于Spring JDBC的项目中,只需要在业务方法上添加@Transactional注解,即可使得此方法是事务性的。
@Override
@Transactional // 新添加的注解
public void addNew(CategoryAddNewDTO categoryAddNewDTO) {
// 暂不关心方法内部代码
}
在编程式事务管理过程中,需要先开启事务(BEGIN),然后执行数据操作,当全部完成,需要提交事务(COMMIT),如果失败,则回滚事务(ROLLBACK)。
在基于Spring JDBC的项目中,只需要使用声明式事务即可,也就是只需要在方法上添加@Transactional注解即可。
Spring JDBC实现事务管理大致是:
开启事务:Begin
try {
执行事务方法,即数据访问操作
提交事务:Commit
} catch (RuntimeException e) {
回滚事务:Rollback
}
所以,Spring JDBC在事务管理中,默认将基于RuntimeException进行回滚,可以通过@Transactional的rollbackFor或rollbackForClassName属性来修改,例如:
@Transactional(rollbackFor = {NullPointerException.class, IndexOutOfBoundsException.class})
@Transactional(rollbackForClassName = {"java.lang.NullPointerException", "java.lang.IndexOutOfBoundsException"})
还可以通过noRollbackFor或noRollbackForClassName属性来配置对于哪些异常不回滚。
其实,@Transactional注解可以添加在:
- 接口上
- 将作用于实现了此接口的类中的所有业务方法
- 接口中的业务方法上
- 将作用于实现了此接口的类中的重写的当前业务方法
- 实现类上
- 将作用于当前类中所有业务方法
- 实现类中的业务方法上
- 将仅作用于添加了注解的业务方法
提示:如果在业务类和业务方法上都添加了@Transactional,却配置了相同名称但不同值的注解参数,将以业务方法上的配置为准。
在应用此注解时,由于这是一种声明式事务管理,推荐添加在接口上,或接口中的业务方法上。
理论上,如果将@Transactional添加在接口上,可能有点浪费,毕竟并不是每个业务方法都需要是事务性的。
注意:由于Spring JDBC在处理事务管理时,使用了基于接口的代理模式,所以,业务方法的内部调用时(同一个业务类对象的A方法调用了B方法),被调用方法相当于是“无事务的”,另外,如果某方法不是接口中声明的业务方法,只是实现类自行添加的方法,无论将@Transactional添加在哪里,都是无效的!
7.自带定时任务
计划任务:设定某种规则(通常是与时间相关的规则),当满足规则时,自动执行任务,并且,此规则可能是周期性的满足,则任务也会周期性的执行。
在Spring Boot项目中,需要在配置类上添加@EnableScheduling注解,以开启计划任务,否则,当前项目中所有计划任务都是不允许执行的!
在任何组件类中,自定义方法,在方法上添加@Scheduled注解,则此方法就是计划任务,通过此注解的参数可以配置计划任务的执行规律。
在根包下创建config.ScheduleConfiguration类,在此类上添加@EnableScheduling注解,以开启计划任务:
package cn.tedu.csmall.product.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 计划任务配置类
*
* @author java@tedu.cn
* @version 0.0.1
*/
@Slf4j
@Configuration
@EnableScheduling
public class ScheduleConfiguration {
public ScheduleConfiguration() {
log.debug("创建配置类对象:ScheduleConfiguration");
}
}
然后,在根包下创建schedule.CacheSchedule类,在类上添加@Component注解,并在类中自定义计划任务方法:
package cn.tedu.csmall.product.schedule;
import cn.tedu.csmall.product.service.IBrandService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 处理缓存的计划任务类
*
* @author java@tedu.cn
* @version 0.0.1
*/
@Slf4j
@Component
public class CacheSchedule {
@Autowired
private IBrandService brandService;
public CacheSchedule() {
log.debug("创建计划任务对象:CacheSchedule");
}
// 关于@Schedule注解的参数配置
// fixedRate:执行频率,将按照上一次开始执行的时间来计算下一次的执行时间,以毫秒值为单位
// fixedDelay:执行间隔时间,即上次执行结束后再过多久执行下一次,以毫秒值为单位
// cron:使用1个字符串,其中包括6~7个值,各值之间使用1个空格进行分隔
// >> 在cron的字符串中各值依次表示:秒 分 时 日 月 周(星期) [年]
// >> 以上各值都可以使用通配符
// >> 使用星号(*)表示任意值
// >> 使用问号(?)表示不关心具体值,问号只能用于“日”和“周(星期)”
// >> 例如:"56 34 12 15 11 ? 2022"表示“2022年11月15日12:34:56,无视当天星期几”
// >> 以上各值,可以使用“x/x”格式的值,例如:在分钟对应的位置设置“1/5”,则表示当分钟值为1时执行,且每间隔5分钟执行1次
@Scheduled(fixedRate = 1 * 60 * 1000)
public void rebuildBrandCache() {
log.debug("开始执行【重建品牌缓存】计划任务…………");
brandService.rebuildCache();
log.debug("本次【重建品牌缓存】计划任务执行完成!");
}
}
提示:以上计划任务需要在业务逻辑层补充“重建品牌缓存”的功能,在IBrandService中添加:
/**
* 重建品牌缓存
*/
void rebuildCache();
并在BrandServiceImpl中实现:
@Override
public void rebuildCache() {
log.debug("开始处理【重建品牌缓存】的业务,无参数");
log.debug("准备删除Redis缓存中的品牌数据……");
brandRedisRepository.deleteAll();
log.debug("删除Redis缓存中的品牌数据,完成!");
log.debug("准备从数据库中读取品牌列表……");
List<BrandListItemVO> list = brandMapper.list();
log.debug("从数据库中读取品牌列表,完成!");
log.debug("准备将品牌列表写入到Redis缓存……");
brandRedisRepository.save(list);
log.debug("将品牌列表写入到Redis缓存,完成!");
log.debug("准备将各品牌详情写入到Redis缓存……");
for (BrandListItemVO brandListItemVO : list) {
Long id = brandListItemVO.getId();
BrandStandardVO brandStandardVO = brandMapper.getStandardById(id);
brandRedisRepository.save(brandStandardVO);
}
log.debug("将各品牌详情写入到Redis缓存,完成!");
}
8.在线日志的实现
实现原理:WebScoket
不需要额外操作与理解,直接按步骤复制粘贴即可
实现效果

1.添加依赖
springboot websocket:用于websocket编程
thymeleaf模板:用于模板输出HTML日志
<!-- springboot websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- thymeleaf模板 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
yml配置
一般都是正常配置,检查一下没有的加上就好
server:
port: 8087
spring:
application:
name: "sap-status"
thymeleaf:
prefix: classpath:/static/
mvc:
pathmatch:
# 使用Ant风格的路径匹配器
matching-strategy: ant_path_matcher
web:
resources:
# 静态文件地址,保留官方内容后,进行追加
static-locations: classpath:/static,classpath:/markdown,classpath:/public,classpath:/resources,classpath:/view,classpath:/META-INF/resources
logging:
level:
javao.cn.socketdemocracy: DEBUG
file:
name: ${spring.application.name}.log
在src/main/resources/static/ 添加 logging.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>实时日志</title>
<!-- jquery -->
<!-- <script th:src="@{/js/jquery-1.9.1.min.js}"></script> -->
</head>
<body style="background-color: #2f3248">
<style>
.log-container {
font-family: monospace; /* 使用等宽字体,类似于控制台 */
white-space: pre-wrap; /* 保留空格和换行符 */
word-wrap: break-word; /* 长单词或URL会自动换行 */
line-height: 1.5; /* 行间距 */
background-color: #2d2d2d; /* 深色背景 */
color: #ffffff; /* 白色文字 */
padding: 10px;
overflow-y: auto; /* 垂直滚动条 */
border-radius: 5px; /* 圆角边框 */
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); /* 添加阴影效果 */
}
button{
margin: 15rpx;
border-radius: 5px; /* 圆角边框 */
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
}
bem-cc{
}
.log-level {
padding: 2px 5px;
border-radius: 3px;
margin-right: 5px;
}
.debug { color: #3399ff; background-color: #001a33; }
.info { color: #33cc33; background-color: #003300; }
.warn { color: #ffcc00; background-color: #333300; }
.error { color: #ff3333; background-color: #330000; }
.class-name { color: #298a8a; font-weight: bold; }
.log-title {
margin-top: 10px !important;
text-align: center;
font-size: 1em;
color: #4e5162;
background-color: #2d2d2d;
padding: 10px;
margin-bottom: 10px; /* 与日志显示区之间留出一些空间 */
border-radius: 5px; /* 圆角边框 */
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); /* 添加阴影效果 */
}
</style>
<!-- 标题 -->
<h6 class="log-title">SpringBoot实现的日志在线监听</h6>
<!-- 显示区 -->
<div class="log-container" id="loggingText" contenteditable="true"
style="height:750px" ></div>
<!-- 操作栏 -->
<div style="height: auto;text-align: center; margin-top: 10px; background-color: #2d2d2d;padding: 20px;border-radius: 5px;box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);">
<button onclick="clearLog()" style="color: green; height: 35px;">清屏</button>
<button onclick="scrollToBottom()" style="color: green; height: 35px;">滚动至底部</button>
<button onclick="toggleAutoScroll()" style="color: green; height: 35px;">开启自动滚动</button>
</div>
<script>
// websocket对象
let websocket = null;
let loggingAutoBottom = false;
// 动态获取当前页面所在的服务器IP地址
function getServerIp() {
var currentUrl = new URL(window.location.href);
console.log("地址",currentUrl)
console.log("本地地址",currentUrl.host)
return currentUrl.host;
}
// 判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
websocket = new WebSocket("ws://"+getServerIp()+"/websocket/logging");
} else {
console.error("不支持WebSocket");
}
// 连接发生错误的回调方法
websocket.onerror = function (e) {
console.error("WebSocket连接发生错误");
};
// 连接成功建立的回调方法
websocket.onopen = function () {
console.log("WebSocket连接成功");
};
// 接收到消息的回调方法
websocket.onmessage = function (event) {
if (event.data) {
// 日志内容
const loggingText = document.getElementById("loggingText");
loggingText.innerHTML += event.data;
// 是否开启自动底部
if (loggingAutoBottom) {
// 滚动条自动到最底部
loggingText.scrollTop = loggingText.scrollHeight;
}
}
};
// 连接关闭的回调方法
websocket.onclose = function () {
console.log("WebSocket连接关闭");
};
// 清屏按钮点击事件
function clearLog() {
const loggingText = document.getElementById("loggingText");
loggingText.innerHTML = "";
}
// 滚动至底部按钮点击事件
function scrollToBottom() {
const loggingText = document.getElementById("loggingText");
loggingText.scrollTop = loggingText.scrollHeight;
}
// 开启/关闭自动滚动按钮点击事件
function toggleAutoScroll() {
loggingAutoBottom = !loggingAutoBottom;
const button = document.querySelector('button[onclick="toggleAutoScroll()"]');
button.textContent = loggingAutoBottom ? "关闭自动滚动" : "开启自动滚动";
}
</script>
</body>
</html>
在src/main/resources/添加日志配置文件logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!-- 默认配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 控制台配置 -->
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<!-- 文件配置 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 设置滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件路径:这里%d{yyyyMMdd}表示按天分类日志 -->
<FileNamePattern>${LOG_HOME}/%d{yyyyMMdd}/${APP_NAME}.log</FileNamePattern>
<!-- 日志保留天数 -->
<MaxHistory>15</MaxHistory>
</rollingPolicy>
<!-- 设置日志格式 -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>utf8</charset>
</encoder>
</appender>
<!-- 将文件输出设置成异步输出 -->
<appender name="ASYNC-FILE" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>256</queueSize>
<appender-ref ref="FILE"/>
</appender>
<!-- 将控制台输出设置成异步输出 -->
<appender name="ASYNC-CONSOLE" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>256</queueSize>
<appender-ref ref="CONSOLE"/>
</appender>
<!-- 配置根记录器 -->
<root level="info">
<appender-ref ref="ASYNC-CONSOLE" />
<appender-ref ref="ASYNC-FILE" />
</root>
</configuration>
添加配置1MyEndpointConfigure.java
package javao.cn.socketdemocracy.config;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import javax.websocket.server.ServerEndpointConfig;
/**
* 解决注入其他类的问题,详情参考这篇帖子:webSocket无法注入其他类:https://blog.csdn.net/tornadojava/article/details/78781474
*/
public class MyEndpointConfigure extends ServerEndpointConfig.Configurator implements ApplicationContextAware {
private static volatile BeanFactory context;
@Override
public <T> T getEndpointInstance(Class<T> clazz){
return context.getBean(clazz);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
MyEndpointConfigure.context = applicationContext;
}
}
添加配置2WebSocketConfig
package javao.cn.socketdemocracy.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* WebSocket配置
*/
@Configuration
public class WebSocketConfig {
/**
* 用途:扫描并注册所有携带@ServerEndpoint注解的实例。 @ServerEndpoint("/websocket")
* PS:如果使用外部容器 则无需提供ServerEndpointExporter。
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
/**
* 支持注入其他类
*/
@Bean
public MyEndpointConfigure newMyEndpointConfigure (){
return new MyEndpointConfigure ();
}
}
最后添加工具类LoggingWSServer.java
package javao.cn.socketdemocracy.util;
import javao.cn.socketdemocracy.config.MyEndpointConfigure;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thymeleaf.util.StringUtils;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* WebSocket获取实时日志并输出到Web页面
*/
@Component
@ServerEndpoint(value = "/websocket/logging", configurator = MyEndpointConfigure.class)
public class LoggingWSServer {
@Value("${spring.application.name}")
private String applicationName;
/**
* 连接集合
*/
private static Map<String, Session> sessionMap = new ConcurrentHashMap<String, Session>();
private static Map<String, Integer> lengthMap = new ConcurrentHashMap<String, Integer>();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session) {
//添加到集合中
sessionMap.put(session.getId(), session);
lengthMap.put(session.getId(), 1);//默认从第一行开始
//获取日志信息
new Thread(() -> {
boolean first = true;
while (sessionMap.get(session.getId()) != null) {
BufferedReader reader = null;
try {
System.out.println(applicationName);
//日志文件路径,获取最新的
String filePath = "./" + applicationName + ".log";
//字符流
reader = new BufferedReader(new FileReader(filePath));
Object[] lines = reader.lines().toArray();
//只取从上次之后产生的日志
Object[] copyOfRange = Arrays.copyOfRange(lines, lengthMap.get(session.getId()), lines.length);
for (int i = 0; i < copyOfRange.length; i++) {
String line = (String) copyOfRange[i];
// 转义特殊字符
line = line.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll("\"", """);
// 添加日志级别图标与背景色
line = line.replace("DEBUG", "<span class='log-level debug'>DEBUG</span>");
line = line.replace("INFO", "<span class='log-level info'>INFO</span>");
line = line.replace("WARN", "<span class='log-level warn'>WARN</span>");
line = line.replace("ERROR", "<span class='log-level error'>ERROR</span>");
// 处理类名
String[] split = line.split("]");
if (split.length >= 2) {
String[] split1 = split[1].split("-");
if (split1.length >= 2) {
line = split[0] + "]" + "<span class='class-name'>" + split1[0] + "</span>" + "-" + split1[1];
}
}
// 匹配日期开头加换行,格式如:2019-08-12 14:15:04
Pattern r = Pattern.compile("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}");
Matcher m = r.matcher(line);
if (m.find()) {
int start = m.start();
StringBuilder sb = new StringBuilder(line);
sb.insert(start, "");
line = sb.toString();
}
copyOfRange[i] = line;
}
// 存储最新一行开始
lengthMap.put(session.getId(), lines.length);
// 第一次加载时,若日志行数过多,则只显示最近的200行
if (first && copyOfRange.length > 200) {
copyOfRange = Arrays.copyOfRange(copyOfRange, copyOfRange.length - 200, copyOfRange.length);
first = false;
}
String result = StringUtils.join(copyOfRange, "<br>");
// 发送至前端
send(session, result);
// 休眠一秒
Thread.sleep(1000);
} catch (Exception e) {
//捕获但不处理
e.printStackTrace();
} finally {
try {
reader.close();
} catch (IOException ignored) {
}
}
}
System.out.println("LoggingWebSocketServer 任务结束");
}).start();
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session) {
//从集合中删除
sessionMap.remove(session.getId());
lengthMap.remove(session.getId());
}
/**
* 发生错误时调用
*/
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
/**
* 服务器接收到客户端消息时调用的方法
*/
@OnMessage
public void onMessage(String message, Session session) {
}
/**
* 封装一个send方法,发送消息到前端
*/
private void send(Session session, String message) {
try {
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
