一、目标
利用已有的前端页面实现一个个人博客系统的后端设计,可以实现用户登录,博客列表展示,博客详情页展示,写博客,删除博客的功能。
二、开发思路
采用前后端分离的方式,网页通过ajax构造HTTP请求和服务端进行数据交互,服务器端接收的请求通过servle解析请求,并返回响应数据,网页拿到响应数据解析后再根据数据的内容渲染到页面。
三、环境准备
JDK、maven、MySQL、相关依赖包5个:lombok(自动生成getter、setter等)、javax.servlet-api(servlet包)、mysql-connector-java(mysql包)、jackson-databind(json数据解析包,用于json序列化和反序列化)、junit(单元测试框架)
四、数据库相关设计
1.数据库设计
数据库设计两张表:
· 用户表用于存放用户信息;
· 文章表存放博客信息。
create table user(id int primary key auto_increment,username varchar(20) not null unique,password varchar(256) not null,nickname varchar(20),head varchar(20),github varchar(256)
)comment '用户表';create table article(id int primary key auto_increment,title varchar(256) not null,content mediumtext not null,create_time datetime,user_id int,foreign key(user_id) references user(id))comment '文章表';
2.实体类设计
在数据库设计好后, 为每张表设计实体类,以字段为成员变量,之所以要设计实体类,是因为在http提交数据的时候,服务端会把数据转换为java对象,同时在服务端返回数据的时候,返回的也是java对象之后转换为json字符串。
@Getter
@Setter
@ToString
public class Article {private Integer id;private String title;private String content;private java.util.Date createTime;private Integer user_id;}@Getter
@Setter
@ToStringpublic class User {private Integer id;private String username;private String password;private String nickname;private String head;private String github;
}
3.JDBC
JDBC步骤:
(1)创建数据库连接对象(这里通过单例模式数据库连接池来创建);
(2)创建操作命令对象(通过连接对象+带占位符的SQL创建)
(3)执行SQL,在执行前要替换掉占位符
(4)如果是:插入、修改、删除需要返回Int类型(说明成功操作了几条)。查询则需要返回查询结果集(一条数据就转为一个Java对象,多条数据需要利用ArrayList< T>)
(5)释放资源
- 在JDBC的第一部分,可以将获取数据库的连接封装起来,采用单例模式。这样做的目的是只创建一个连接池来获取多个连接对象。具体操作如下:
1.创建util工具包,用来存放连接,包括:封装数据库连接和封装session会话管理。
2.双重校验锁实现线程安全的单例模式
public class DButil {/** 1.封装获取数据库连接;* 单例模式* */private static volatile javax.sql.DataSource DS;private static DataSource getDataSource(){if(DS==null){synchronized (DButil.class){if(DS==null){MysqlDataSource ds=new MysqlDataSource();ds.setURL("jdbc:mysql://localhost:3306/blog_java44");ds.setUser("root");ds.setPassword("******");ds.setUseSSL(false);ds.setCharacterEncoding("utf8");DS=ds;}}}return DS;}//提供一个开放出来给jdbc操作的代码使用工具:获取连接对象public static java.sql.Connection getConnection(){try {return getDataSource().getConnection();} catch (SQLException e) {throw new RuntimeException("获取数据库连接失败");}}//释放资源public static void close(Connection c, Statement s, ResultSet re){try {if(re!=null) re.close();if(c!=null) c.close();if(s!=null) s.close();} catch (SQLException e) {throw new RuntimeException("jdbc释放资源出错",e);}}public static void close(Connection c, Statement s){close(c,s,null);}//@test是单元测试注解,可以直接运行(必须 public void)@Testpublic void test(){Assert.assertNotNull(getConnection());
// System.out.println(getConnection());}
}
在最后对连接是否成功进行一个单元测试:使用了一个Assert断言来判断获取对象是否为空。
注:如果报错,说明单元测试不通过。没有报错返回true,则说明单元测试通过。
五、servlet相关设计
1.对session的封装
登录功能里需要有会话管理,
(1)会话管理包括:登录成功后创建session并保存用户信息以及访问敏感资源后校验session,查看用户是否登录。所以考虑把session的校验功能封装起来,以供多处代码的使用。
涉及的问题:
- 如何判断用户是否登录–》返回登录时的用户信息即可。
- 如何设计返回值–》返回用户信息User(如果已经登录就返回User,没登陆就返回Null)
(2)Servlet处理请求和响应时要用到序列化及反序列化,因此考虑将两个功能也封装起来。
- 序列化:用于返回响应数据,把Java对象转为json字符串。在序列化的同时把响应写入响应体发出去。
- 反序列化:用于把请求正文中的json字符串转换为Java对象,方便后端操作。
- 因为原始的日期不是我们想要的形式,所以日期需要格式化一下:可以写个静态代码块,在初始化时就设置ObjectMapper的日期格式
public class WebUtil {
//简单起见,使用了饿汉式单例模式private static final ObjectMapper MAPPER=new ObjectMapper();static {DateFormat df=new SimpleDateFormat("yyyy-MM-dd");MAPPER.setDateFormat(df);}public static User validateLogin(HttpServletRequest req){HttpSession session=req.getSession(false);//从request对象获得session对象User user=null;if(session!=null){user=(User) session.getAttribute("user");}return user;}public static void serialize(HttpServletResponse resp, Object o){try {resp.setContentType("utf-8");resp.setContentType("application/json");String body=MAPPER.writeValueAsString(o);resp.getWriter().write(body);} catch (IOException e) {throw new RuntimeException("序列化出错",e);}}public static <T> T deserialize(HttpServletRequest req,Class<T> clazz){try {return MAPPER.readValue(req.getInputStream(),clazz);} catch (IOException e) {throw new RuntimeException("反序列化异常",e);}}
- 写个Test测试一下能否序列化成功,使用Junit测试框架:
写个public void函数,可以直接运行。
@Testpublic void test() throws JsonProcessingException {Article a =new Article();a.setId(001);a.setTitle("hello world");a.setContent("content");a.setCreateTime(new Date());a.setUser_id(2);String json=MAPPER.writeValueAsString(a);System.out.println(json);}
将Java对象转为json字符串,序列化成功
2.开发前后端接口
这一部分就是基于ajax的前后端分离式开发。开发流程:
- 前端发送ajax请求;
- 后端解析请求;
- 服务端返回响应(json格式)
- 客户端解析响应
在开发前需要思考的问题是:前端的入口是什么?后端需要的数据是什么?请求的格式及字段、响应的格式及字段。
(1)登录功能接口设计
业务分析:后端需要验证账号和密码,那么前端就需要提供账号密码。前端请求方式:POST;后端需要验证结果,来判断登录成功与否,登录成功跳转详情页,登录失败则给提示。最终形成两个文件,一个为前端设计,另一个是后端设计:
- 前端ajax发送请求和解析服务器响应:
//发送ajax请求ajax({method:"POST",url:"login",//注意不要加/contantType:"application/json",body:JSON.stringify({//body里必须是json对象username:username.value,//前边的是json数据、请求数据的键;后边的是dom元素,为账号的值password:password.value,}),callback:function(status,responseText){//var json=handleResponse(status,responseText);if(!json)return;//解析响应正文//登录成功:调转到博客列表页面if(json.ok){location.href="blog_list.html";}else{alert(json.error);//error.innerHTML=json.error; }}})
- 后端接收到请求解析验证和发出响应
注:构造了一个Map类型的响应
@WebServlet("/login")
public class LoginServlet extends HttpServlet {@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//处理请求resp.setCharacterEncoding("UTF-8");//将前端传过来的request json字符串转Java对象User user= WebUtil.deserialize(req, User.class);//构造一个返回数据Map<String,Object> body=new HashMap<>();//验证账号密码User query= UserDao.queryByUsername(user.getUsername());if(query==null){body.put("ok",false);body.put("error","账号不存在");}else if(!query.getPassword().equals(user.getPassword())){body.put("ok",false);body.put("error","账号或密码错误");}else {body.put("ok", true);//登录成功,创建sessionHttpSession session = req.getSession();session.setAttribute("user", query);}//返回响应WebUtil.serialize(resp,body);}
}
- 另外,对帐号密码的验证会另外封装起来实现:
即连接数据库,并从数据库中查询(按照用户名查询),验证用户是否存在,如果存在返回user,不存在时返回null
public static User queryByUsername(String username){Connection c=null;PreparedStatement ps=null;ResultSet rs=null;try{//1创建数据库连接c= DButil.getConnection();String sql="select * from user where username=?";//2创建命令对象ps=c.prepareStatement(sql);//替换占位符:第一个参数是占位符索引ps.setString(1,username);//3执行sqlrs=ps.executeQuery();//4处理结果集while (rs.next()){User user=new User();user.setId(rs.getInt("id"));user.setUsername(username);user.setPassword(rs.getString("password"));user.setNickname(rs.getString("nickname"));user.setHead(rs.getString("head"));user.setGithub(rs.getString("github"));return user;}return null;}catch (SQLException e) {throw new RuntimeException("根据账号查询用户出错",e);}finally {DButil.close((com.mysql.jdbc.Connection) c, (com.mysql.jdbc.Statement) ps,rs);}}
(2)博客列表显示
业务分析:这一部分是登录成功后进入的第一个界面,需要显示博客列表和用户信息。需要实现的功能是展示用户个人信息及博客列表信息。对其中的用户相关数据,不需要再次请求,因为登录后就会在session里保存用户信息。对于博客信息,设计为登陆后只能看到自己发布的文章。综上所述,需要设计两个接口,一个用户信息接口,一个博客列表接口。
- 用户信息接口
前端:在页面初始化时就发送ajax请求来获取数据。
后端:从session中获取用户信息。 - 博客列表接口
前端:页面初始化时发送ajax请求获得文章列表;
后端:初始化时就获取session中用户关联的文章列表
ajax请求及对服务器响应的解析和页面信息填充:
ajax({method:"get",url:"getUserInfo",callback:function(status,responseText){//var json=handleResponse(status,responseText);if(!json)return;//解析响应正文//昵称是设置标签内容if(json.nickname){nickname.innerHTML=json.nickname;}if(json.head){head.src=json.head;}if(json.github){github.href=json.github;}// var nickname=json.nickname;// var github=json.github;// var head=json.head;}});//获取文章列表信息ajax({method:"get",url:"blog/list",callback:function(status,responseText){var json=handleResponse(status,responseText);if(!json)return;//处理响应--解析响应正文Json,遍历文章列表//获取文章列表的父容器var blogListDiv=document.querySelector(".container-right");var str='';for(var i=0;i<json.length;i++){var article =json[i];var desc=article.content.length>50?article.content.substring(0,50):article.content;str+=`<div class="row"><div class="title">${article.title}</div><div class="date">${article.createTime}</div><div class="desc">${desc}</div><div class="to-detail"><a href="blog_content.html?id=${article.id}">查看全文 >></a></div></div>`}blogListDiv.innerHTML=str; }});
后端对请求的响应:
@WebServlet("/blog/list")
public class BlogListServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setCharacterEncoding("UTF-8");//获取session用户User user= WebUtil.validateLogin(req);//没有登录返回401if(user==null){resp.setStatus(401);return;}//登录。。查询文章列表//没有请求数据,但要根据用户查询获取文章List<Article> articles= ArticleDao.queryBlogList(user.getId());WebUtil.serialize(resp,articles);}
}
其中,对文章列表的获取会另外封装起来,即从数据库中根据用户查询文章:
public static List<Article> queryBlogList(Integer userId) {Connection c = null;PreparedStatement ps = null;ResultSet rs = null;try {//1.获取连接c = DButil.getConnection();String sql = "select *from article where user_id=?";//2.创建操作命令对象ps = c.prepareStatement(sql);//替换占位符ps.setInt(1, userId);//3执行sqlrs = ps.executeQuery();//4处理结果集List<Article> articles = new ArrayList<>();while (rs.next()) {Article a = new Article();a.setId(rs.getInt("id"));a.setTitle(rs.getString("title"));a.setContent(rs.getString("content"));java.sql.Timestamp createtime = rs.getTimestamp("create_time");a.setCreateTime(new java.util.Date(createtime.getTime()));a.setUser_id(userId);articles.add(a);}return articles;} catch (Exception e) {throw new RuntimeException("查询模块列表出错", e);} finally {DButil.close((com.mysql.jdbc.Connection) c, (Statement) ps, rs);}}
(3)博客详情页接口设计
业务分析:这一部分需要做的是:获取用户信息及获取文章详情信息。
- 前端ajax请求
包括发起用户信息的请求和文章详情的请求。发出请求时,url会带上文章Id以便后端查询。
同时对返回服务端的响应作出解析填充到页面中。
<script>
//获取用户信息//需要设置的用户信息:先获取dom元素var nickname=document.querySelector("#nickname");var head=document.querySelector("#head");var github=document.querySelector("#github");ajax({method:"get",url:"getUserInfo",callback:function(status,responseText){//var json=handleResponse(status,responseText);if(!json)return;//解析响应正文//昵称是设置标签内容if(json.nickname){nickname.innerHTML=json.nickname;}if(json.head){head.src=json.head;}if(json.github){github.href=json.github;}// var nickname=json.nickname;// var github=json.github;// var head=json.head;}});//获取文章详情信息ajax({method:"get",url:"blog/detail"+location.search,callback:function(status,responseText){var json=handleResponse(status,responseText);if(!json)return;//处理响应--解析响应正文Json,遍历文章列表//获取文章列表的父容器var blogListDiv=document.querySelector(".container-right");var str='';str=`<div class="row"><div class="title">${json.title}</div><div class="date">${json.createTime}</div><div id="content" class="desc" style="background-color">${json.content}</div> </div>` blogListDiv.innerHTML=str; //将ID为content的dom元素,标签内容,渲染md源码//editormd是引入的MarkdownJS库的一个对象editormad.markdownToHTML("content",{markdown:json.content})} });
</script>
- 后端响应
@WebServlet("/blog/detail")
public class BlogDetailServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setCharacterEncoding("UTF-8");String id=req.getParameter("id");Article a= ArticleDao.queryBlogDetail(Integer.parseInt(id));WebUtil.serialize(resp,a);}
}
(4)发布博客接口设计
业务分析:实现博客撰写和上传功能,具体操作为点击发布文章按钮,验证标题和内容,验证通过就提交文章数据。
- 前端请求:
//绑定文章发布事件。。。//获取标题和内容的dom元素var title=document.querySelector("input");var content=document.querySelector("textarea");function articleAdd(){if(!title.value){alert("标题不能为空");return;}if(!content.value){alert("文章内容不能为空");return;}// 验证通过发请求:提交标题和内容到后端做插入文章顺序ajax({method:"post",url:"blog/add",contentType:"application/json",body:JSON.stringify({title:title.value,content:content.value,}),callback:function(status,responseText){var json=handleResponse(status,responseText);if(!json)return;if(json.ok){alert("提交成功!");location.href="blog_list.html";}}})}
- 后端响应:
@WebServlet("/blog/add")
public class BlogAddServlet extends HttpServlet {@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setCharacterEncoding("utf-8");Article a= WebUtil.deserialize(req,Article.class);//获取session中的用户信息User user= WebUtil.validateLogin(req);//没有登录返回401if(user==null){resp.setStatus(401);return;}//登录,插入文章a.setUser_id(user.getId());int n= ArticleDao.insert(a);Map<String,Object> map=new HashMap<>();map.put("ok",true);WebUtil.serialize(resp,map);}
}
(5)注销功能接口
注销功能的实现:在实现这个功能时,只需要约定前后端交互接口。因为客户端代码不需要调整。注销按钮本来就是一个 < a href=“logout”> , 点击的时候就会发送 GET /logou 这样的请求。因此只需要在服务端实现:让session失效并重定向到登录页面即可。
@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//注销功能resp.setCharacterEncoding("UTF-8");HttpSession session=req.getSession(false);if(session!=null){session.invalidate();}//重定向到登录页面resp.sendRedirect("login.html");}
}
技术难点
1.session和cookie会话管理
2.双重校验锁
3.Jackson序列化
4.junit测试
5.servlet
6.JDBC