第一次项目总结(瑞吉外卖)

目录

一.项目介绍

二.主要内容

三.遇到的新知识

2.1开发流程

2.2代理工具

2.3部署结构

四.心得


一.项目介绍

瑞吉外卖项目分为后台管理端和移动端(用户端).

主要核心技术是:springboot +mybatis-plus +redis +mysql等

视频链接:黑马程序员Java项目实战《瑞吉外卖》,轻松掌握springboot + mybatis plus开发核心技术的真java实战项目_哔哩哔哩_bilibili

是一个非常适合新手学习的springboot单体项目

二.主要内容

1. 后端Controller层返回结果统一封装的R对象

后端的controller层接收完前端的请求后,要返回什么样的结果是需要按情况变化的,但如果每一个controller返回的结果不一样,前端也要用不同的数据类型进行接收。所以,为了避免麻烦定义了一个统一的返回结果类

public class R implements Serializable {

    private Integer code; //编码:1成功,0和其它数字为失败

    private String msg; //错误信息

    private T data; //数据

    private Map map = new HashMap(); //动态数据

    public static  R success(T object) {
        R r = new R();
        r.data = object;
        r.code = 1;
        return r;
    }

    public static  R error(String msg) {
        R r = new R();
        r.msg = msg;
        r.code = 0;
        return r;
    }

    public R add(String key, Object value) {
        this.map.put(key, value);
        return this;
    }

}

2.定义静态资源映射关系

静态资源映射关系主要用于将前端请求的URI路径与后端服务器资源路径进行映射。

@Configuration
public class MyWebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        //设置静态资源映射关系
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
    }

}

3. 配置消息资源转换器

数据库的主键大都是由mybatis-plus的主键自动生成策略之雪花算法生成的,雪花算法生成的是一个Long类型的数字,而雪花算法生成的主键传输到前端的时候会出现精度丢失现象导致前端拿到的id和数据库中的id不一致

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 */
public class JacksonObjectMapper extends ObjectMapper {

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

                .addSerializer(BigInteger.class, ToStringSerializer.instance)
                .addSerializer(Long.class, ToStringSerializer.instance)
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

4.登录校验过滤器



/**
 * 检查是否登录
 */
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
    //路径匹配器
    public static final AntPathMatcher PATH_MATCHER=new AntPathMatcher();

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        String requestURI = request.getRequestURI();

        log.info("拦截到的请求{}",requestURI);
        /**
         * 需要放行的路径
         */
        String[] urls=new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**",
//                "/common/**",
                "/user/sendMsg",//移动端发送短信
                "/user/login"//移动端登录

        };

        boolean check = check(urls, requestURI);
        if (check){
            log.info("本次请求不需要处理", requestURI);
            filterChain.doFilter(request,response);
            return;
        }
        /**
         * 已登录直接放行
         */
        if (request.getSession().getAttribute("employee")!=null){
            log.info("用户已登录id为{}",request.getSession().getAttribute("employee"));
            Long empId = (Long)request.getSession().getAttribute("employee");
            BaseContext.setCurrentId(empId);
            filterChain.doFilter(request,response);
            return;
        }
        //    用户登录- 如果已登录,就直接放行
        if (request.getSession().getAttribute("user") != null) {
            log.info("用户已登录,登录的id为: {}", request.getSession().getAttribute("user"));
            // 把id保存到当前ThreadLocal线程中。
            Long userId = (Long) request.getSession().getAttribute("user");
            BaseContext.setCurrentId(userId);
            // 放行。
            filterChain.doFilter(request, response);
            return;
        }

        log.info("用户未登录");
        /**
         * 未登录返回未登录结果,通过输出流向客户端响应数据
         */
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;

    }

    /**
     * 路径匹配,检查本次请求是否需要放行
     */
    public boolean check(String[] urls,String requestURI){
        for (String url : urls) {
            boolean match = PATH_MATCHER.match(url, requestURI);
            if (match){
                return true;
            }

        }
        return false;
    }
}

5.全局异常处理器


/**
 * 全局异常处理
 */
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 进行异常处理
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R exceptionHandler(SQLIntegrityConstraintViolationException ex){
        log.error(ex.getMessage());

        if (ex.getMessage().contains("Duplicate entry")){
            String[] split = ex.getMessage().split(" ");
            String msg = split[2]+"已存在";
            return R.error(msg);
        }
        return R.error("未知错误");
    }

    /**
     * 进行异常处理
     * @return
     */
    @ExceptionHandler(CustomException.class)
    public R exceptionHandler(CustomException ex){
        log.error(ex.getMessage());

        return R.error(ex.getMessage());
    }

}


/**
 * 自定义业务异常
 */
public class CustomException extends RuntimeException {
    public CustomException(String message){
        super(message);
    }
}

6.公共字段的填充



/**
 * 自定义元数据对象处理器
 */

@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
    /**
     * 插入主动填充
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共字段主动填充[insert]...");
        log.info(metaObject.toString());
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("createUser", BaseContext.getCurrentId());
        metaObject.setValue("updateUser", BaseContext.getCurrentId());

    }

    /**
     * 更新主动填充
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("公共字段主动填充[insert]...");
        log.info(metaObject.toString());

        long id = Thread.currentThread().getId();
        log.info("线程id为{}",id);
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("updateUser", BaseContext.getCurrentId());
    }
}

7.文件上传和下载


/**
 * 文件上传下载
 */

@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {

    @Value("${reggie.path}")
    private String basePath;

    /**
     * 文件上传
     * @param file
     * @return
     */
    @PostMapping("/upload")
    public R upload(MultipartFile file){
        //file是临时文件需要转存到指定位置,否则请求完成后临时文件删除
        log.info(file.toString());
        String originalFilename = file.getOriginalFilename();
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
        String fileName = UUID.randomUUID().toString() + suffix;
        File dir = new File(basePath);
        if (!dir.exists()){
            //目录不存在
            dir.mkdirs();
        }
        try {
            file.transferTo(new File(basePath+fileName));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return R.success(fileName);
    }

    /**
     * 文件下载
     * @param name
     * @param response
     */
    @GetMapping("/download")
    public void download(String name, HttpServletResponse response){
        try {
            FileInputStream fileInputStream=new FileInputStream(new File(basePath+name));
            ServletOutputStream outputStream = response.getOutputStream();
            response.setContentType("image/jpeg");
            int len=0;
            byte[] bytes = new byte[1024];
            while ((len=fileInputStream.read(bytes))!=-1){
                outputStream.write(bytes,0,len);
                outputStream.flush();
            }
            outputStream.close();
            fileInputStream.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

三.遇到的新知识

1.mysql主从复制

第一次项目总结(瑞吉外卖)_第1张图片

mysql的主从复制的目的和redis主从复制的目的几乎都是一样的,为了解决单点故障问题,主mysql数据库挂了,从mysql数据库可以继续干活。可以进行读写分离,在并发量大的时候并且是读多写少的环境下,我们可以进行读写分离,让从mysql数据库为只读,主mysql数据库即可读也可以写,相当于分担了主msyql读的并发压力,系统可用性更高。

可以参考:http://t.csdn.cn/T1UQH

2.前后端分离

第一次项目总结(瑞吉外卖)_第2张图片

当前项目中,前端代码和后端代码混合在一起,存在以下问题

1). 开发人员同时负责前端和后端代码开发,分工不明确

2). 开发效率低

3). 前后端代码混合在一个工程中,不便于管理

4). 对开发人员要求高(既会前端,又会后端),人员招聘困难

2.1开发流程

前后端分离开发后,面临一个问题,就是前端开发人员和后端开发人员如何进行配合来共同开发一个项目?可以按照如下流程进行:

1). 定制接口: 这里所说的接口不是我们之前在service, mapper层定义的interface; 这里的接口(API接口)就是一个http的请求地址,主要就是去定义:请求路径、请求方式、请求参数、响应数据等内容。(具体接口文档描述的信息, 如上图)

2). 前后端并行开发: 依据定义好的接口信息,前端人员开发前端的代码,服务端人员开发服务端的接口; 在开发中前后端都需要进行测试,后端需要通过对应的工具来进行接口的测试,前端需要根据接口定义的参数进行Mock数据模拟测试。

3). 联调: 当前后端都开发完毕并且自测通过之后,就可以进行前后端的联调测试了,在这一阶段主要就是校验接口的参数格式。

4). 提测: 前后端联调测试通过之后,就可以将项目部署到测试服务器,进行自动化测试了。

2.2代理工具

1)YAPI

第一次项目总结(瑞吉外卖)_第3张图片

YApi 是高效、易用、功能强大的 api 管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护 API,YApi 还为用户提供了优秀的交互体验,开发人员只需利用平台提供的接口数据写入工具以及简单的点击操作就可以实现接口的管理。

YApi让接口开发更简单高效,让接口的管理更具可读性、可维护性,让团队协作更合理。

源码地址: https://github.com/YMFE/yapi

官方文档: https://hellosean1025.github.io/yapi/

可以导出html文件或md文件,主要描述的就是接口的基本信息, 包括: 请求路径、请求方式、接口描述、请求参数、返回数据等信息。

2)Swagger

Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。功能主要包含以下几点:

A. 使得前后端分离开发更加方便,有利于团队协作

B. 接口文档在线自动生成,降低后端开发人员编写接口文档的负担

C. 接口功能测试

使用Swagger只需要按照它的规范去定义接口及接口相关的信息,再通过Swagger衍生出来的一系列项目和工具,就可以做到生成各种格式的接口文档,以及在线接口调试页面等等。

直接使用Swagger, 需要按照Swagger的规范定义接口, 实际上就是编写Json文件,编写起来比较繁琐、并不方便, 。而在项目中使用,我们一般会选择一些现成的框架来简化文档的编写,而这些框架是基于Swagger的,如knife4j。

可以在浏览器浏览生成的接口文档,Knife4j还支持离线文档,对接口文档进行下载,支持下载的格式有:markdown、html、word、openApi(JSON)。

2.3部署结构

第一次项目总结(瑞吉外卖)_第4张图片

四.心得

第一次写这种单体项目对于我的收获无疑是非常多的,我学到的不只是很多企业级的知识,更学到了各种各样的开发思想。同时在越深入学习中越能发现自己的不足,我们要学的东西还很多,希望以后能一直进步,与君共勉!

你可能感兴趣的:(mybatis,java,spring,boot)