微头条项目开发
微头条项目开发
在进行微头条项目开发的时候,学习到了新的知识点,在此记录下来
Postman测试工具
Postman API Platform | Sign Up for Free
接口
不同与java中的interface接口文件,我们在这里要提到的是前端 -> 后端的业务接口
在我们目前编写的微头条项目中,前端访问业务接口的形式是通过不同的URI来实现的
在后端中,Controller层定义了不同的业务接口,通过BaseController工具类反射到WebServlet注解上
通过注解的模糊匹配的方式,实现不同的URI访问不同的业务方法
我们在后端代码编写时,可通过postman测试工具来测试接口功能
Token
toker中文释义:令牌
使用传统的Session和Cookie的模式,在并发问题中会有大量的服 务器开销,我们选择Token来解决问题
在验证用户名和密码正确无误后,后端将业务码(200)响应给客户端的同时,将用户信息加密成Token,一起响应给客户端
当客户端再发送请求时,就拿着加密的token解析用户信息,这样客户端就完全不知道用户的信息了
JWT工具类
我们使用JWT工具类对Token进行加密和解析
package com.xiaobai.headline.util;
import com.alibaba.druid.util.StringUtils;
import io.jsonwebtoken.*;
import java.util.Date;
public class JwtUtil {
//Token超时时间
private static long tokenExpiration = 24*60*60*1000;
private static String tokenSignKey = "827724";
/**
* 生成Token
* @param userId Long类型的userId
* @return 一个被加密的串(String)
*/
public static String createToken(Long userId) {
String token = Jwts.builder()
.setSubject("YYGH-USER")
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
.claim("userId", userId)
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP)
.compact();
return token;
}
/**
* 解析Token
* @param token 加密的字符串Token
* @return 原本的userId
*/
public static Long getUserId(String token) {
if(StringUtils.isEmpty(token)) return null;
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
Integer userId = (Integer)claims.get("userId");
return userId.longValue();
}
/**
* 判断token是否过期
* @param token 加密的字符串Token
* @return true:过期(无效) false:没过期(有效)
*/
public static boolean isExpiration(String token){
try {
boolean isExpire = Jwts.parser()
.setSigningKey(tokenSignKey)
.parseClaimsJws(token)
.getBody()
.getExpiration().before(new Date());
//没有过期,有效,返回false
return isExpire;
}catch(Exception e) {
//过期出现异常,返回true
return true;
}
}
}
GET和Post
GET方式发送请求,也可以使用请求体里装JSON的格式
POST方式发送请求,也可以使用URI后面的键值对形式
只不过在html的form表单中,get使用键值对,post使用JSON串
值对象
值对象(Value Object)
当我们发现,前端请求中的数据中,有不完全符合pojo层的实体类的数据,也就是不完全与数据库表中列完全对应的数据
这个时候我们接受数据,就要新建一个类,这个类就是值类(值对象)
他用于接受前端业务接口中传过来的请求参数,在操作数据库时,也可以将值对象中的不同属性分别放到不同的表中
甚至可以不放在任何一个表里
分页
这是我们第一次接触到分页的问题
前端利用响应式数据绑定了一些分页所需要的参数,这些参数大致有:
-
pageData:本页数据
-
pageNum:当前页码数
-
pageSize:每页显示数量
-
totalPage:总页数
-
totalSize:数据的总数
这五个参数也是大部分分页写法的五大参数,将这些参数以键值对(JSON)的方式返回给前端
但很显然,数据库中并没有这些数据,这些是由后端调用数据库获取数据进行处理后的参数
我们需要用到VO(值对象)去包装这些参数,最后转成JSON返回前端
其中,pageNum(当前页码)和pageSize(每页显示数量)由前端提供,直接返回即可
Service
这里就体现出来Controller什么都不管的性质了
Controller只负责调用Service,并且把请求中的类封装好扔进来,再接受封装好的响应类,打个包扔回前端
在Service层中,我们要给这分页五大项赋值
通过数据库查询到pageData 和 totalSize,再通过totalSize和pageSize就能算出totalPage
注:如果总条目数/每页多少条能整除,那就正好分多少页,如果不能整除,要取整除的商后加1
将五大项打包,发给Controller处理
public class NewsHeadlineServiceImpl implements NewsHeadlineService {
private NewsHeadLineDao newsHeadLineDao = new NewsHeadlineDaoImpl();
@Override
public Map findNewsPage(HeadlineQueryVo headlineQueryVo) {
// 每页显示多少条数据和第几页都是前端给的数据,这里不需要再去处理
int pageNum = headlineQueryVo.getPageNum();
int pageSize = headlineQueryVo.getPageSize();
// 调用Dao去数据库查询每条头条的所有信息
// 注:因为要返回头条发布距现在已经过了多少小时,所以不能用NewsHeadLine 用HeadlinePageVo
List<HeadlinePageVo> pageData = newsHeadLineDao.findPageList(headlineQueryVo);
// 查询数据总条数,然后将数据计算后算出能分多少页
int totalSize = newsHeadLineDao.findPageCount(headlineQueryVo);
int totalPage = totalSize % pageSize == 0 ? totalSize / pageSize : totalSize / pageSize + 1;
Map map = new HashMap();
map.put("pageNum", pageNum);
map.put("pageSize", pageSize);
map.put("totalSize", totalSize);
map.put("totalPage", totalPage);
map.put("pageData", pageData);
return map;
}
}
Dao
这个sql是我们写过最复杂的sql
因为要考虑到where参数:type=0 :所有类型都查,
和关键词没有:不设关键词条件查询
当这两个条件存在时,我们用concat为sql语句拼串,建立params集合用来存放占位符参数
要考虑pageData中内容的排序问题
最重要的是:要考虑limit参数,每次请求只请求这一页的内容,考虑从第几页的数据(条数)开始返回,还有一页返回多少条数据
注:将params列表转换为数组才能给可变长参数传参
注:在sql的语句的追加拼串上,要记得前空后空
package com.xiaobai.headline.dao.impl;
import com.xiaobai.headline.dao.BaseDao;
import com.xiaobai.headline.dao.NewsHeadLineDao;
import com.xiaobai.headline.pojo.vo.HeadlinePageVo;
import com.xiaobai.headline.pojo.vo.HeadlineQueryVo;
import java.util.ArrayList;
import java.util.List;
public class NewsHeadlineDaoImpl extends BaseDao implements NewsHeadLineDao {
@Override
public int findPageCount(HeadlineQueryVo headlineQueryVo) {
List params = new ArrayList();
String sql = """
select
count(1)
from
news_headline
where
is_deleted = 0
""";
// 类型和关键词查询问题
if (headlineQueryVo.getType() != 0) {
sql = sql.concat(" and type = ? ");
params.add(headlineQueryVo.getType()); // 精确查询类型
}
if (headlineQueryVo.getKeyWords() != null && !headlineQueryVo.getKeyWords().equals("")) {
sql = sql.concat(" and title like ? ");
params.add("%" + headlineQueryVo.getKeyWords() + "%"); // 模糊查询关键词
}
//param参数是List集合,而这里是可变长参数,需要数组
return baseQueryObject(Long.class, sql, params.toArray()).intValue();
}
@Override
public List<HeadlinePageVo> findPageList(HeadlineQueryVo headlineQueryVo) {
List params = new ArrayList();
String sql = """
select
hid,
title,
type,
page_views pageViews,
TIMESTAMPDIFF(HOUR,create_time,now()) pastHours,
publisher
from
news_headline
where
is_deleted = 0
""";
// 类型和关键词查询问题
if (headlineQueryVo.getType() != 0) {
sql = sql.concat(" and type = ? ");
params.add(headlineQueryVo.getType()); // 精确查询类型
}
if (headlineQueryVo.getKeyWords() != null && !headlineQueryVo.getKeyWords().equals("")) {
sql = sql.concat(" and title like ? ");
params.add("%" + headlineQueryVo.getKeyWords() + "%"); // 模糊查询关键词
}
// 排序问题:根据发布现在时间升序,根据浏览量降序
sql = sql.concat(" order by pastHours asc , page_views desc ");
// 请求的每页数据,从哪条数据开始,返回多少条数据
sql = sql.concat(" limit ?,? ");
params.add((headlineQueryVo.getPageNum() - 1) * headlineQueryVo.getPageSize());// limit参数:从第几条参数(用页码*每页数据量)
params.add(headlineQueryVo.getPageSize());// limit参数:返回多少条数据
//param参数是List集合,而这里是可变长参数,需要数组
return baseQuery(HeadlinePageVo.class, sql, params.toArray());
}
}
LoginFilter
在前端接手了一些操作数据的过滤后,我们后端的filter只是用来处理同源禁止策略的预检请求
但完全交给前端来做登录的验证是否合适呢?
不合适,因为这对后端,对数据都是不太安全的操作
我们后端在接收到数据后,也要用token来验证一下是否为登录状态,这样进入数据库的数据才会更加安全
所以我们编写一个LoginFilter,在进行增删改查的controller之前,做一个登录判断
@WebFilter("/headline/*")
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpServletRequest request = (HttpServletRequest) servletRequest;
String token = request.getHeader("token");
boolean flag = null != token && !JwtUtil.isExpiration(token); // 空串也会返回true,所以无需再判断是否为空串
if (flag) {
filterChain.doFilter(request, response);
} else {
WebUtil.writeJson(response, Result.build(null, ResultCodeEnum.NOTLOGIN));
}
}
}
is_deleted
数据无价,我们在数据库中发现这样一个属性:is_deleted
在进行查询业务的时候,会在判断语句里增加 is_deleted = 0,代表着这条数据没有被删除
删除数据,我们使用一个属性的0和1来代表这此数据是否被删除,而不是真正的从数据库上移除
这样就能有一个后悔药——从数据库原始数据中再次寻找被删掉的数据,相当于一个永久回收站
相比于随着科技进步的低廉存储价格,数据才是真正的无价
总结
微头条项目结束之后,我们在前端框架的帮助下,实现了后端不使用框架完成独立项目
这个项目主要是用来熟悉业务逻辑
Controller层不负责处理任何业务,只是负责req和resp
Service层用来处理具体业务
Dao层与数据库交互
以这种业务逻辑去写代码才会顺畅
接下来就是后端框架的学习,Java学习之路道阻且长,希望一切顺利!