微头条项目开发

微头条项目开发

在进行微头条项目开发的时候,学习到了新的知识点,在此记录下来


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学习之路道阻且长,希望一切顺利!