毕业设计(五)总结

in 毕业设计 with 0 comment

喜大普奔,毕设断断续续终于写完了。。总结一手。
后台用的SpringBoot+SpringCloud+MyBatis
前端使用的JQuery+Layui+两个开源前端模版
用到的其他工具有Redis和Zookeeper

收获

先扯扯收获。毕竟是用微服务做的一个项目。也在公司搭了一个微服务架构,用来监测和管理分布式项目的服务关系。主要就是将公司的其他项目注册进Eureka,然后通过SpringBootAdmin、配置中心等微服务的那一套工具来监测和管理项目。

关于SpringCloud和Dubbo+Zookeeper的区别

做到最后。。算是理解了下面这两张图。。
image.png

image.png

第一张图就是SpringCloud在服务调用关系的架构图。下面这张是Dubbo+Zookeeper组合的分布式的架构图。

因为我之前有些过Dubbo+Zookeeper的小项目,所以在用SpringCloud写毕设时被影响了,前面在架构方面设计时,还是使用的Dubbo+Zookeeper根据不同的角色业务做的切割服务。
在后面做学生后台管理、教师后台管理和管理员后台管理业务时,重复代码那是一个多呀,比如修改密码,都是确定密码后,然后去后台代码判断修改。又是因为切割服务是根据角色业务切割的,所以。。修改密码,这同一个业务代码,重复写了三遍。和这个类似的还有很多。。
所以,毕设写完了,对这两个不同技术栈对分布式架构更加理解了。

对JDK8新特性的使用

JDK8的比较出名的就是stream流和lambda表达式。这两个新特性都在毕设用上了。
stream流就是主要用于遍历集合,然后对集合中的对象进行操作。比如在显示课程信息代码中,我的课程表的授课教师是使用的User表的id,在前端页面需要显示教师Id,所以,在查找出课程后,要重新遍历一次,根据教师id找到教师姓名。具体的实现代码如下

@Override
    public List<Course> findCourseBySort(String sort) {
        // 根据分类找到课程
        List<Course> courses = courseMapper.findCourseBySort(sort);
        // stream流遍历
        courses.stream()
                .forEach(course -> {
                    course.setUserName(userMapper.findStudent(String.valueOf(course.getTeacherId())).getUsername());
                    course.setSeat(String.valueOf(selectCourseMapper.findCountNum(String.valueOf(course.getId()))));
                });
        return courses;
    }

中间也掺杂了lombda表达式,反正,项目中,在对于集合操作,都没用for循环都方式来遍历。

对多线程的使用

看完了《Java并发编程的艺术》这本书后,深知了多线程在业务实现方面的使用。既然学了,就能加上这玩意就加上吧。

比如在我的SSO单点登服务中,就用的多线程来完成的。用户账号密码判断成功后,因为是单点登录,在Redis要存Token,还要返回Cookie。这两步操作并没有很大的联系,所以在这里,用多线程来完成在合适不过了。具体代码如下:

if (checkLogin!=null) {
            // 将token存入Redis,其中是以token为K,用户id为V
            new Thread(()-> {
                redisTemplate.opsForValue().set(tokenUserId, String.valueOf(checkLogin.getId()));
                redisTemplate.opsForValue().set(tokenType, idType);
            }).start();
            // 创建Cookie
            new Thread(()->{
                // 通过uuid创建token
                // 将token存入Cookie
                Cookie cookieId = new Cookie("TOKEN", tokenUserId);
                cookieId.setDomain("penglei.com");
                cookieId.setPath("/");
                response.addCookie(cookieId);
                Cookie cookieType = new Cookie("Type", tokenType);
                cookieType.setDomain("penglei.com");
                cookieType.setPath("/");
                response.addCookie(cookieType);
            }).start();
            // 删除验证码Cookie
            Cookie newCookie = new Cookie("CHECKCODE","0");
            newCookie.setPath("/");
            newCookie.setMaxAge(0);
            response.addCookie(newCookie);
            return target;
}

SSO单点登录

SSO已经用一篇文章讲了一下。这里还是在记录一下。这个项目的SSO就是使用的Redis加Cookie完成。学到了什么呢?
学到了关于域的理解、跨域问题和Session不一致的原因。
我修改hosts文件,将本机ip(127.0.0.1)配置到了四个不同的域名下。域名都是xxx.penglei.com的格式。所以,Cookie设置的Domain就是“penglei.com”,这样,这个Cookie就可用作用于我配置的全部域名了,然后存入一个Token,同时也把这个Token作为Redis的key,值是用户的id。就相当于完成了Session的功能,就这样,完成了在login.penglei.com(登录服务)域名下登录后,其他的xxx.penglei.com都登录成功。膨胀点讲,就是一个非常非常的京东的单点登录。
image.png

image.png

分布式锁的使用

分布式锁也用一篇文章讲了下。继续复读一遍。
其实今天又将我的那把分布式锁又优化了一次。其实刚刚实现这把锁的时候,问题还是很多。梳理一下我遇到的问题把。

  1. 堆溢出
    堆溢出是我在完成这把分布式锁遇到的第一个问题。主要原因就是在创建这把锁的时候,每次一个线程使用,初始化都要创建大量对象,因为我需要使用的Zookeeper客户端对象都是在Controller创建的,随着压测线程数量增加导致同样的对象创建过多,并且没有被及时清理。内存溢出的问题就出现了。
    解决办法就是在创建客户端对象移到ZookeeperLock对象中,减少了Zookeeper客户端对象的创建。
  2. 连接超时
    这算是我对之前那篇分布式锁文章的补充把。我把初始化的方法放在setLockName方法里面,每次新的线程,给这个锁setLockName都会打开一个新的连接。同样的,随着压测线程数量不断增加,每一个线程都要打开新的连接。连接超时的问题就出现了。
    解决办法就是我把连接Zookeeper方法放在了set方法里面。换句话讲,就是我把连接放在了线程中,是每个线程连接的Zookeeper。我的解决办法就是把连接Zookeeper方法放在了锁了,当创建这个锁的时候,就连接了Zookeeper,这样,每个线程在使用锁时,直接使用的已经连接Zookeeper的对象,避免了重复打开连接而导致的连接超时的问题。

大概我在写分布式锁除了理论知识的问题,大概就是遇到了这两个值得讲讲的问题。

Redis的使用

我项目中,其实Redis并没用很多,主要就是用它来完成SSO单点登录和完成Session的功能,比如登录成功判断、简单权限判断和图片验证码数据的存储。但是我要记录一个问题。。
首先,我的Redis是配置在阿里云学生服务器的Docker里面,所以服务器配置有点差。在我的项目第一次访问Redis,运行时间需要5-6s,感觉有点恐怖。比如验证码的刷新啥的。要第一个用户要等5s,其实当第一个5s连接后,其他用户都会很快的。但是这第一个5s我忍不了。
解决办法就是当用户访问首页时会发送一个异步请求,这个异步请求的作用就是通知其他服务器,访问一次redis,然后异步完成这个第一个5s的访问连接时间。
代码很简单,

    // 前端异步请求,该访问redis了
    $.getJSON(
        "http://teacher.penglei.com:9051/teacher/linkRedis",
        function () {
        }
    )

    // 后台开个线程,随便连接一下redis
	public String linkRedis(){
        	new Thread(()->redisTemplate.opsForValue().set("penglei","1231321",3, TimeUnit.SECONDS)).start();
        return null;
    }

就这个样子,减少了第一次连接redis所需要的时间,比如获得验证码从第一个用户的5-6s,减少到了0.6,算是体验增加把。同时验证码这一块,再加上一个简单标示
image.png
多个障眼法,提示体验,美滋滋。

项目介绍

毕设题目,基于微服务架构学生选课系统。主要分为三个角色。即管理员、教师和学生。大概介绍一下就是管理用对教师和学生的管理。教师对于课程管理和学生查看。学生就是选课和已选课程管理。
然后就是分为五个系统。

  1. SSO单点登录系统
  2. 主页系统
  3. 学生后台管理系统
  4. 教师后台管理系统
  5. 管理员后台管理系统

在做项目的过程中,还将前端页面重构了一下的。。

注册中心

微服务嘛,少不了的就是注册中心。
image.png

我这里就是Eureka做了三台集群,然后服务提供者都做了三台集群。充分使用微服务高可用的特性。但是我单点登录并没有加入到微服务中,单点登录就使用的普通的boot项目,然后其他项目访问就行了。

单点登录系统

image.png
这个就是登录页面,用户输入账号、密码和验证码再选择角色,后台先判断验证码,再根据账号、账号和角色,判断是否登录成功。登录失败提示账号或密码错误。登录成功返回url地址栏的target地址,并且生成两个Cookie,Cookie存放Token,Token都是通过UUID生成的,然后作为Key放在Redis中,一个Token是用户id,另一个Token是用户的类型。
验证码随便网上找的一个代码。然后自己修改。验证码就是随机4位数字,这个也是放在Redis中。然后再后台判断验证码的值是否一直,验证码不对,提示验证码输入错误。
登录页面也做了登录判断。如果用户已经登录,再次同过Url访问登录页面,会自动跳转到了首页页面,主要就是判断Cookie有没有值,有值就说明已经登录成功。

学生角色

学生选课系统嘛,只有学生才能选课,所以项目首页就是给学生提供的。先看下首页

首页

首页是使用的开源模版fly
gitee地址:https://gitee.com/sentsin/fly?_from=gitee_search
image.png
登录后的首页右上角显示个人中心和注销按钮,未登录显示的是登录按钮。做了一下登录判断。这个登录判断就是访问页面时,异步请求后台判断登录接口。已经登录返回一个标示,未登录返回一个标示,然后前端根据这个标示来显示不同的页面。

推荐课程

推荐课程主要是用SQL语句的order by rand() limit 4实现的。所以推荐课程是随机的,但是在SQL语句使用随机数,在后面肯定会效率很低,有点代码洁癖的我,有点不能忍,后面有时间再优化,这是一个问题。当务之急是把论文先搞定。
image.png
image.png

课程分类

课程分类是写死了的,但是首页嘛。课程分类这里只能显示8门课程。SQL语句就是根据id倒序,实现新提交的课程就放在首页的课程分类。
绑定点击事件,然后点击其他分类,再次请求,传入这个分类名,重新渲染页面
image.png
image.png

查看课程

查看课程也是一个点击事件,使用layui的组件layer的open方法,打开显示该课程的课程信息。传入的就是该课程的id号,同样也是异步请求,渲染这个弹框。

image.png
查看课程也可以直接点击课程
image.png

课程选择页面

课程选择页面就是对全部课程的分页显示,其中包括搜索课程名或者课程分类或者组合搜索。
image.png
image.png

课程选择页面分页

分页使用的layui的laypage组件render方法,设置的每页显示12门课程,然后点击下一页跳转,后台根据page和limit再次重新渲染页面
带入截图如下
image.png
分页的页数也是laypage.rander方法动态生成的.

课程搜索页面

课程搜索就是使用的表单提交,然后js获得url的get请求值
根据课程名搜索
image.png
根据课程分类搜索
image.png
这里也可以进行组合搜索。主要的后台代码就是Mybatis的动态sql了。就不演示了

选课功能

选课功能只有学生能选择。这里加上了分布锁。然后,做了下选课判断,比如只有学生能选、重复课程无法选择和满人了无法选择。
选课成功与否都会弹框提示用户。使用的是layui的layer.msg方法完成。
image.png

个人中心

个人中心都使用layui的开源模版layui-mini完成的
gitee地址:https://gitee.com/zhongshaofa/layuimini?_from=gitee_search
个人中心根据Cookie中的Token判断了。如果不同身份的角色点击个人中心,都会前往不同的页面。
代码如下

public String myInfo(@CookieValue(value = "Type",required = false) Cookie cookie){
        String idType = redisTemplate.opsForValue().get(cookie.getValue());
        if ("1".equals(idType)){
            return "http://teacher.penglei.com:9051";
        }else if ("0".equals(idType)){
            return "http://student.penglei.com:9001";
        }else {
            return "http://admin.penglei.com:6001";
        }
    }

个人中心也做了登录判断,未登录的用户,强制返回到登录页面。角色不符的用户url访问,强制返回主页。
注销就是删除Token,然后返回到主页

学生端个人中心

image.png

学生端个人中心就是退选,退出登录和修改密码这三个操作。比较简单。

教师角色

教师学生无法选课,这里就主要介绍后台管理页面。
image.png
后台首页如上,就是一个快捷入口、一个校内公告和一个开课记录。校内公告的查看和查看课程一样的。

课程管理

课程管理就是管理该教师自己的课程的

我的课程

image.png
课程管理就是显示自己开的课程,因为实际上,一个教师开课不可能太多,这里就没用分页,就用了一个简单的表格。教师可以删除该课程和新增该课程。

新增课程

image.png
新增课程如上,填写表单,异步请求,就可以新增一门课程。在新增课程后台做了一下判断的,就是如果课程搜课地点和搜课时间已经有课程了,那说明课程冲突了,这里是不允许开课的,会给出一个提示,后台代码如下

public String insertNewCourse(Course course) {
	// 检查
        String check = courseMapper.checkCourse(course);
        System.out.println(check);
        if (check!=null){
            return "0";
        }else {
            courseMapper.insertNewCourse(course);
        }
        return "1";
    }

学生管理

image.png
主要就是使用的layui的table来分页和查询,这个玩意太好用了哈哈哈。

历史公告

image.png
一样,用的layui的table分页,查看历史的公告信息

管理员端

管理员主要就是完成对学生、教师的管理、课程的查看和公告信息的发布
image.png

教师、学生管理

教师和学生管理都差不多
image.png
image.png
image.png
主要就是重置密码和删除该角色,还有就是新增角色。

课程管理

有些数据是我手动添加的,所以授课时间和地点有重复的,这些都是小问题。
image.png
课程管理主要就是一些查看的操作。

公告管理

image.png
image.png
公告管理包括添加公告和删除公告。

总结

以上就是关于我的毕设了。记录一下。
其实项目也有一些可以处理的问题,比如在接口访问,这里可以加个过滤器。还有数据库方面的优化等。当前的首要任务还是写毕设。难顶。接下来的时间就是好好分配写论文,复习之前的知识,要找工作了呀。