———2018.6.30日更新———:
今天有位童鞋找我要【个人博客系统】第一版的源码,我把源码整理了一下,由于组织混乱代码写的也很水,所以项目还是不上传github了,直接上传百度云了,需要的朋友自行下载
链接:https://pan.baidu.com/s/1b7wSPKmDviAcDfdsx9fm4g 密码:2zrf
声明
第一版的博客系统完全是用Atom手写的(没有用Idea),编译也是直接javac -d xxx.java直接编译成的.class文件;项目目录结构比较混乱,非严格MVC模式,刚学Java的新人,请不要学习!主要看代码流程和前后端数据交互的过程,把道理弄懂就好!!
运行
上传了三个文件:1.配置过的Tomcat(conf文件夹下server.xml) 2.建表sql 3.Tomcat_Lyon文件夹(源代码,静态资源等都在这个文件夹)
项目运行:一般来说Tomcat中运行项目有多种方式:
1.可以将项目直接打包成war后放在webapps文件夹下,然后启动Tomcat;
2.在server.xml里指定Context path指定别处的目标文件夹,然后启动Tomcat;
本项目采用的是方式2.
———2018.5.11日更新———:
时间真是不够用,JSP+Servlet版本的博客项目完全是手写的(都没用Idea),目录和组织混乱,完全是不能看,所以以后应该不会更新了…
但是,新版本的SSM框架版的又来了 ,项目目前用了用的是IDE是Idea,Maven构建,JDK1.8
前端:
Jquery,Ajax,bootstrap4.0,渲染模板Velocity,
编辑器是支持Markdown的开源的Editor.md。
后端:
Spring,SpringMVC,Mybatis。数据库用的Mysql,权限管理Shiro,后面还要支持全文搜索插件,后面会不断重构,只要我有时间和精力!现在想到的有:Restful规范的接口,反向代理,应该都会尝试使用,包括数据库表重新设计,反正会一直重构和完善下去,时机合适会考虑部署上线。
项目框架已经基本搭建好了,登录注册浏览文章功能基本就绪了,基本上一半时间花在了前端页面搞模板扣样式上~哎,这不替换Editor.md死活初始化不成功…坑多的很……以后再把文章一点点写出来吧!
—–更新—–:
1.添加了修改文章时,一键导入原文的功能。
2.添加了前台搜索框、可以进行全文关键字搜索;
3.添加了文章标签模块,支持新增/修改文章的时候编辑标签,也可对标签单独管理;
4.添加了Ajax版的登录,这样登录完成后不需要离开当前页面(之前是点击【登录注册】跳转到登录注册页面进行操作)
项目概述:
这是一个只用了原生Java+Jsp+Servlet开发的一个个人博客系统。实现了个人博客的基本功能,前台页面可以进行文章浏览,关键词搜索,登录注册;登陆后支持对文章进行感谢、评论;然后还可以对评论进行评论和点赞….。后台可以对文章进行增删改查(文章编辑器采用了百度开源的Ueditor);分类分页查找;标签管理。
前端:Html、css、Javascript、Ajax、Bootstrap、Jquery
后端:Java(反射)、Jsp、Servlet
服务器:Tomcat;数据库:Mysql
项目演示:
—–以下是原文——
预警:
此篇完全是流水账,记录了整个项目实际诞生的过程!道路曲折,文字啰嗦,慎入!!!
半年多前,看了《Head First HTML与CSS(第2版)》,跟着做了一个个人博客的静态页面,这个页面也是相当的简单,再后来用bootstap给页面做了一点优化,看起来像一个真正的博客页面了!然后,这个项目就存放在电脑中很久。。。。再后来,学了java,才了解到,可以用java+mysql+tomcat真正实现一个个人博客系统,当时激动坏了!说干就干!于是,这个【个人博客系统】终于诞生了!
1.静态HTML+CSS的博客页面
当然,没有软件开发经验、也没有项目经历,不熟悉软件项目开发的一般流程,对于这个【个人博客系统】我是根据自己的需求一点点写出来的!各种功能也是逐渐完善的!
最开始,写出了静态的html+css页面,用bootsrtap修饰后页面的效果大概是这样的:

当时,页面比截图中呈现的要简陋不少,首先,还没有文字编辑器,所以文章,是赤裸裸的文字效果,其次,也没有【留言区】,因为留言功能模块,是我最后才添加上去的!!!右边About和Archives…这个就是套用Head First HTML与CSS书中的博客小例子的,至今还未修改掉,打算先放着,以后可以放文章分类标签、或者放些图片之类的!
但是,不论如何,页面成型了!反正乍一眼看上去就是一个博客的样子,然后我又写了静态的文章分类页面,效果大概是这样:

最后,我又颇费心思地设计了一下首页,因为博客网站不能一进来全是文章吧,要有个好看的门脸!!!于是首页也出来了:

后来加上了标签系统,导航条上加了搜索框,效果是这样的:
至此,博客项目的前台静态页面基本都已经有了,下面开始设计后台页面和数据存储!(服务器用Tomcat、数据存储用Mysql)
2.Mysql表结构设计
首先,一级分类有6个模块:
Java、Python、数据库服务器、CS其他、生活杂谈、登录注册
对应数据库myblog下的categary表,表中就2个字段,id和name。

二级分类20个模块,对应数据库myblog中的表categary_2,表中有三个字段:id、mainid、name其中mainid即此二级分类名称所属的一级分类id(譬如第10条记录:id=10,mainid=3,name=Tomcat即表示Tomcat所属的一级分类是id=3的一级分类,即数据库服务器)通过mainid这个外键,即可将表categary_2和表categary关联起来。

分类的sql表建好后,最关键的来了,是文章的sql表,文章表起名为articles,包含哪些字段呢?一篇文章,在数据库中除了基本的id区分外,还应该有文章作者author、标题title、摘要subtitle、内容content、发布时间createdate、主分类mid、文章分类cid(二级分类)

建表SQL语句如下:
CREATE TABLE `articles` ( `id` int(50) NOT NULL AUTO_INCREMENT, `author` varchar(30) DEFAULT NULL, `title` varchar(500) DEFAULT NULL, `subtitle` varchar(500) DEFAULT NULL, `content` text, `createdate` datetime DEFAULT NULL, `cid` int(20) DEFAULT NULL, `mid` int(20) DEFAULT NULL, PRIMARY KEY (`id`), KEY `fk_article_categary` (`cid`), KEY `fk_article_maincat` (`mid`), CONSTRAINT `fk_article_categary` FOREIGN KEY (`cid`) REFERENCES `categary_2` (`id`), CONSTRAINT `fk_article_maincat` FOREIGN KEY (`mid`) REFERENCES `categary` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=145 DEFAULT CHARSET=utf8;
可见,一篇文章有2个外键,分别对应主分类categay和二级分类categary_2.然后,我们还要开发登录注册的功能、评论点赞的功能,所以后期加入了User表记录注册用户信息comments表记录文章评论和点赞信息。
3.博客后台管理系统设计
数据库表设计完了,下一步该从哪下手呢?毕竟页面还是死(静态)的啊?!所有的文章数据我可以通过Mysql来操作了,可以实现文章的查询、编辑、删除。但!是!我不能每次发布文章或者转载别人的文章都要通过mysql后台手动操作吧?那多麻烦啊!我需要一个前台页面操作的【博客文章后台管理系统】,这样才能满足我对博客文章的基本需求:增删改查,当然这些操作一定是要在浏览器前台页面完成的!这样才够人性化!
于是,还是同样的套路,先写出后台管理系统的静态页面:

博客后台管理系统需要实现的功能很简单:新增博客文章、对博客文章进行编辑修改、删除文章、查询特定文章。也就是增删改查!静态页面还是相对比较容易写的,毕竟就是html+css的堆叠,加上简单的javascript和bootstrap!难点在于页面点击事件的跳转(跳转到对应的servlet)程序和mysql后台数据的交互,再取数据到前台呈现!当然,这对于初学java的我很有挑战,然而,也充满了乐趣!这,不就是所谓的java全栈工程师么?哈哈,小小得意下^_^
4.博客文章编辑器——Ueditor的集成
个人博客系统,最重要的当然是发表博客文章了,发表个人文章,我们只能求助于开源的文本编辑器,要求简单好用即可,最好还能给文章添加图片、音乐、代码块。综合考虑后,我选择了百度开源的文本编辑器—Ueditor
UEditor – 首页ueditor.baidu.com/website/
5.Filter过滤器应用
filter过滤器可以拦截每一次的servlet请求,常用于用户登录状态的记录和字符编码的转换!这里我还用filter构造了一下地址过滤,让特定的访问地址转换到相对应的servlet程序中!实现跳转功能!
6.Servlet代码的重构——java反射
项目初期,是没有用java反射的。通常一类主功能下有多个页面,一个页面也可能有很多子模块,每个子模块需要单独写一个servlet来处理,这样整个项目会积累越来越多的servlet,而且后期随着功能和页面越来越多,那有功能变动或者页面改动,整个项目就非常不容易去修改和维护!但是,用java反射处理后,一类主功能,基本对应了一个servlet,一个servlet下包含了任意多个子模块,这样程序就精简了不少,而且便于后期的迭代和维护!
7.开发登录注册模块
登录注册模块的主要功能:实现登录注册。实现登录注册还是很重要的,而且,需要记录用户的注册信息、登录或退出状态、因为,后面还有文章的评论,点赞功能,都需要用户注册并且登录状态下操作!后台数据库中新建User表,表字段有:id、telephone、email、username、password。
登录注册界面如下:

注册需要输入用户邮箱、用户名、手机号、密码,其中手机号和邮箱都是唯一的。登录时支持邮箱+密码或者手机号+密码登录。在前台用了简单的javascript对邮箱、手机号进行了校验,
拒绝无效的手机号、邮箱登录注册,校验通过后,则提交给服务器处理。
JS校验代码如下:
<script type="application/x-javascript"> function validate(f){ var re1 = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/; var input1 = f.userid.value; if(/^\d+$/.test(input1)==true){ if(!(/^\d{11,12}$/.test(input1))){ alert("请输入11-12位的手机号!"); f.userid.focus(); return false; } } else{ if(!(re1.test(input1))){ alert("请输入正确格式的邮箱地址!"); f.userid.focus(); return false; } } if(!(/^[0-9a-zA-Z.]{6,16}$/.test(f.userpass.value))){ alert("密码必须是6~16位!"); f.userpass.focus(); return false; } return true; } function validate2(f){ var re2 = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/; var input1 = f.username.value; var input2 = f.useremail.value; var input3 = f.userpass.value; var input4 = f.userphone.value; if(/^\d+$/.test(input4)==true){ if(!(/^\d{11,12}$/.test(input4))){ alert("请输入11-12位的手机号!"); f.userphone.focus(); return false; } }else{ alert("请确保输入的手机号码为数字!"); } if(!(re2.test(input2))){ alert("请输入正确格式的邮箱地址!"); f.useremail.focus(); return false; } if(!(/^[0-9a-zA-Z.]{6,16}$/.test(input3))){ alert("密码必须是6~16位!"); f.userpass.focus(); return false; } return true; } </script>
下面,我们来以登录为例,看一下用户登录时的Java代码实现。
首先,页面登录的部分html代码如下:
<form action="/Lyon/foreuserLogin" method="post" onSubmit="return validate(this)"> <input type="text" name="userid" placeholder="邮箱/手机号" required=""> <input type="password" name="userpass" placeholder="密码" required=""> <ul class="tick w3layouts agileits"> <li> <input type="checkbox" id="brand1" value=""> <span style="color:#FFF">记住我</span> </li> </ul> <div class="send-button w3layouts agileits"> <input type="submit" value="登 录"> </form>
当用户填写好账号密码,点击【登录】后,触发前一段JavaScript验证代码中的validate函数,当手机号/邮箱号,密码位数验证通过后,页面会执行跳转,跳转到http:localhost:8080/Lyon/foreuserLogin路径。
当然,此跳转会被各种过滤器Filter拦截,其中有个名为ForeServletFilter.java的拦截器如下:
package blog.flowingsun.filter; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; public class ForeServletFilter implements Filter { public void destroy() { } @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)resp; String contextPath = request.getServletContext().getContextPath(); request.getServletContext().setAttribute("contextPath", contextPath); String uri = request.getRequestURI(); uri = StringUtils.remove(uri, contextPath); if (uri.startsWith("/fore")) {//后台其他路径 String method = StringUtils.substringAfterLast(uri,"/fore"); request.setAttribute("method", method); req.getRequestDispatcher("/HomeServlet").forward(request, response); return; }else if(uri.equals("/Home")){ String method = "foreHome"; request.setAttribute("method", method); req.getRequestDispatcher("/HomeServlet").forward(request, response); return; } chain.doFilter(request, response); } public void init(FilterConfig arg0) throws ServletException { } }
当被ForeServletFilter.java拦截到以后,拦截器会将foreuserLogin取出,移除fore,剩下userLogin,并以此为method方法名,设置为request请求的属性。之后,跳转到HomeServlet.java中去,HomeServlet中的userLogin方法通过反射被调用执行。
这样,程序就来到了HomeServlet.java中的userLogin方法,代码如下:
public String userLogin(HttpServletRequest request,HttpServletResponse response){ List<String> info = new ArrayList<String>(); //保存所有返回信息 String userid = request.getParameter("userid"); //接收用户登录email或手机号内容 String userpass = request.getParameter("userpass"); //接收userpass内容 String rephone = "^[0-9]+"; User user = new User(); if(Pattern.matches(rephone,userid)){ //判断传入的userid值是邮箱还是手机号 user.setTelephone(userid); //根据不同情况设置具体的传入参数为邮箱号或手机号 }else{user.setEmail(userid); } user.setPassword(userpass); //设置password try{ //先用if语句查询用户邮箱和手机号,如果数据库中已存在,则用户已经注册过!否则,证明是新用户,else语句执行注册过程! User usertoken = UserDAO.findLogin(user); if(usertoken.getId()!=0&&usertoken.getName()!=null){ info.add("<p><font color='white'>亲爱的:"+usertoken.getName()+"</font>登录成功!</p>"); request.setAttribute("info",info); //保存info信息 request.getSession().setAttribute("user", usertoken); return "jsp/index.jsp"; }else{ info.add("<p><font color='white'>账号或密码错误<font color='red'>登录失败,</font>请再次尝试!</font></p>"); request.setAttribute("info",info); //保存info信息 } }catch(Exception e){ }return "jsp/userLogin.jsp"; }
User user = new User();实例化一个用户,将用户邮箱/手机号+密码信息传入这个user,然后通过UserDAO.java中的findLogin()方法可以判断出这个user是否存在于数据库中,findLogin()方法的代码如下:
public User findLogin(User user)throws Exception{ Connection conn=null; //定义数据库连接对象 PreparedStatement pstmt = null; //定义数据库操作对象 try{ String sql = "SELECT id,usrname FROM User WHERE email=? AND password=? OR telephone=? AND password=?"; Class.forName(DBDRIVER); //加载驱动程序 conn = DriverManager.getConnection(DBURL,DBUSER,DBPASSWORD); pstmt = conn.prepareStatement(sql);//实例化操作 pstmt.setString(1,user.getEmail()); //设置邮箱名称 pstmt.setString(2,user.getPassword()); //设置密码 pstmt.setString(3,user.getTelephone()); //设置电话号码 pstmt.setString(4,user.getPassword()); //设置密码 ResultSet rs = pstmt.executeQuery(); //取得查询结果 User usertoken = new User(); if(rs.next()){ user.setId(rs.getInt(1)); user.setName(rs.getString(2)); //取得用户名 } }catch(Exception e){ throw e; }finally{ if(pstmt!=null){ try{ pstmt.close(); //关闭操作 }catch(Exception e){ throw e; } } } return user; }
如果查询成功,表名该用户存在于数据库中,实例化一个usertoken来保存用户登录信息(用户id和用户名信息)
查询成功后if(usertoken.getId()!=0&&usertoken.getName()!=null)成立,则用户登录成功,跳转到首页,并且通过request.getSession().setAttribute(“user”, usertoken);在session范围内添加用户登录成功的信息。这样,一个完整的登录过程就结束了!
登录功能演示视频:
竟然有人点赞,开心 ,我就详细说一下评论模块的实现吧!
8.开发文章评论、点赞模块
先看一下评论模块长什么样子:

看一下,留言区的HTML代码:
<div> <div class="commentHeader" > <h3><span class="f-ff2">留言区</span></h3> </div> <textarea style="height:100px;" class="writeComment" placeholder="告诉我,你的梦想!" id="comment"></textarea> <button type="button" id="commentsubmit" class="btn btn-default btn-xs " style="background-color:#7D9EC0;color:#FFFFFF;width=5px;height=5px;float:right;"> <img src="images/comment2.png" class="commentIcon"/> 评论 </button> </div> <div id="commentList"> <div class="commentCounts" > 共<span class="j-flag">2</span>条评论 </div> <div class="commentItem"> <div><a href="#">亲爱的猪猪</a>:不吃主食,真会更健康吗?</div> <span>(2018-01-17)<a href="#">回复</a></span> <div id="praise"> <span class="praise"><img src="images/zan.png" id="praise-img" /></span> <span id="praise-txt">1</span> <span id="add-num"><em></em></span> <span id="commentId" style="display:none;">65</span> </div> </div> <div class="commentItem"> <div><a href="#">阳光流淌</a>:啊</div> <span>(2018-01-17)<a href="#">回复</a></span> <div id="praise"> <span class="praise"><img src="images/zan.png" id="praise-img" /></span> <span id="praise-txt">0</span> <span id="add-num"><em></em></span> <span id="commentId" style="display:none;">66</span> </div> </div> </div>
重点看一下这段:
<textarea style="height:100px;" class="writeComment" placeholder="告诉我,你的梦想!" id="comment"></textarea> <button type="button" id="commentsubmit" class="btn btn-default btn-xs " style="background-color:#7D9EC0;color:#FFFFFF;width=5px;height=5px;float:right;"> <img src="images/comment2.png" class="commentIcon"/> 评论 </button>
其中,textarea是留言区的大文字输入框,后面跟着评论按button(id=”commentsubmit”)
当用户输入文字,点击评论时,会触发页面上的一段Javascript验证程序,用来判断用户是否登录。
如未登录,则alert弹出一个警告框,”亲,您还未登录,请先登录后再来评论哦!”
如果登录,则将用户的评论传入数据库,同时在页面动态生成一段用户评论的html代码。JavaScript验证代码如下:
<script> $(function(){ var xmlhttp; $("#commentsubmit").click(function(){ $.get( "forecheckLogin", function(result){ if("success"==result){ var usrComment = $("#comment").val(); var articleId = 134; var url = "foresetblogComment"; xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange=checkResult; //响应函数 xmlhttp.open("POST",url,true); xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;"); xmlhttp.send("articleId="+articleId+"&usrComment="+usrComment); }else{ alert("亲,您还未登录,请先登录后再来评论哦!"); } } );return false; }); function checkResult(){ if (xmlhttp.readyState==4 && xmlhttp.status==200){ var usrComment = document.getElementById('comment').value; var newdiv = document.createElement("div"); var commentlist = document.getElementById("commentList"); newdiv.className = 'commentItem'; newdiv.innerHTML = '<div><a href="">阳光流淌</a>:'+usrComment+'</div><span>10月28日13:28</span><div id="praise"><span class="praise" onclick="return function(this)"><img src="images/zan.png" id="praise-img" /></span><span id="praise-txt">0</span><span id="add-num"><em></em></span></div>'; commentlist.appendChild(newdiv); return true; } } }); </script>
功能演示:
评论的大致流程就是这样,下面让我们来看一下【如果登录,则将用户的评论传入数据库】这句话的java代码是怎样实现的?
if("success"==result){ var usrComment = $("#comment").val(); var articleId = 134; var url = "foresetblogComment"; xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange=checkResult; //响应函数 xmlhttp.open("POST",url,true); xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;"); xmlhttp.send("articleId="+articleId+"&usrComment="+usrComment); }else{ alert("亲,您还未登录,请先登录后再来评论哦!"); }
如果用户已登录(”success”==result)则,新建XHR请求(XHR对象可以在不向服务器提交整个页面的情况下,实现局部更新网页。详见XMLHTTPRequest_百度百科)向服务器指定的servlet程序发送文章ID、用户评论信息。
这个处理请求的servlet程序,就是foresetblogComment。(var url = “foresetblogComment”;)XHR请求的地址换成明文的Get请求地址是这样的:
http://localhost:8080/Lyon/foresetblogComment?articleId=...&usrComment=...
其中http://localhost:8080/Lyon这个是整个项目的本地路径,即每个请求都会以它开头。
foresetblogComment即本次即将跳转的程序别名,然后本篇博客文章ID(articleId)和用户评论内容(usrComment)将会通过XHR的POST请求发送给服务器。
重点来了!这个XHR请求,会被过滤器拦截下来,准确的说,每一次请求都会被过滤器拦截。
过滤器ForeServletFilter.java拦截了本次请求,后取得了本次访问的uri(foresetblogComment),然后移除了’fore’,通过java反射,唤醒了对应的名为setblogComment的方法。并将本次请求跳转到HomeServlet.java继续处理。
ForeServletFilter.java的代码如下:
package blog.flowingsun.filter; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; public class ForeServletFilter implements Filter { public void destroy() { } @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)resp; String contextPath = request.getServletContext().getContextPath(); request.getServletContext().setAttribute("contextPath", contextPath); String uri = request.getRequestURI(); String method = StringUtils.substringAfterLast(uri,"/fore"); uri = StringUtils.remove(uri, contextPath); if (uri.startsWith("/fore")) {//后台其他路径 String method = StringUtils.substringAfterLast(uri,"/fore"); request.setAttribute("method", method); req.getRequestDispatcher("/HomeServlet").forward(request, response); return; }else if(uri.equals("/Home")){ String method = "foreHome"; request.setAttribute("method", method); req.getRequestDispatcher("/HomeServlet").forward(request, response); return; } chain.doFilter(request, response); } public void init(FilterConfig arg0) throws ServletException { } }
现在,程序跳转到了HomeServlet.java。来看一下HomeServlet.java中的setblogComment方法:
public String setblogComment(HttpServletRequest request,HttpServletResponse response){ User user = (User) request.getSession().getAttribute("user"); String userName = user.getName(); int userId = user.getId(); int starNum = 0; SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm"); Date date= new Date(); String commentTime=sdf.format(date); String usrComment = request.getParameter("usrComment"); int articleId = Integer.parseInt(request.getParameter("articleId")); if (CommentDAO.setComment(userName,commentTime,usrComment,articleId,userId,starNum)==true){ List<String> info = new ArrayList<String>(); //保存所有返回信息 info.add("<p>亲爱的,评论<font color='red'>成功</font>!</p>"); request.setAttribute("info",info); return "jsp/login.jsp"; }return "jsp/login.jsp"; }
此方法先从request.session中取出了登录用户的信息,初始化时间信息,从XHR的request请求中取出了usrcomment、articleId信息,然后将信息通过CommentDAO的setComment方法存入Mysql数据库,这样就完成了整个评论过程!