WikiWiki
首页
Java开发
Java面试
Linux手册
  • AI相关
  • Python Flask
  • Pytorch
  • youlo8
SEO
uniapp小程序
Vue前端
work
数据库
软件设计师
入门指南
首页
Java开发
Java面试
Linux手册
  • AI相关
  • Python Flask
  • Pytorch
  • youlo8
SEO
uniapp小程序
Vue前端
work
数据库
软件设计师
入门指南
  • API工具笔记
  • Java基础
  • SpringAI开发问答系统
  • 前后端分离架构
  • 工作流Activity7
  • 微服务架构

单体项目开发

客户端发出请求的几种方式

  1. 地址栏中输入请求地址后回车发出请求 同步

  2. 通过超链接发出请求 同步

  3. 通过form表单发出请求 同步

  4. 通过前端框架发出异步请求

服务器端接收请求参数的几种方式

  1. 通过HttpServletRequest对象获取参数 (以后基本不用)

  2. 通过在处理请求的方法参数列表处声明的方式接收参数

  3. 通过在处理请求的方法参数列表处声明自定义对象的方式接收参数

    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 / intInteger
bigintLong
char / varchar / text系列String
datetimeLocalDateTime

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

不需要额外操作与理解,直接按步骤复制粘贴即可

实现效果

image-20241022153725726

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("&", "&amp;")
                                .replaceAll("<", "&lt;")
                                .replaceAll(">", "&gt;")
                                .replaceAll("\"", "&quot;");

                        // 添加日志级别图标与背景色
                        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();
        }
    }
}
最近更新:: 2025/8/21 13:52
Contributors: yanpeng_
Next
Java基础