精华内容
下载资源
问答
  • 在app.vue页面写一个判断事件并调用 _isMobile(){ let flag = navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG...

    在app.vue页面写一个判断事件并调用
    在这里插入图片描述

    _isMobile(){
      let flag = navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i)
      // localStorage.setItem('isiphone',flag)
      localStorage.setItem('ismobile',flag?1:0)
      return flag;
    },
    

    // 判断跳转pc端页面还是移动端页面
    // 使用钩子函数对路由进行权限跳转
    获取存储的ismobile 进行判断

    navigator.userAgent 用来浏览器的user-agent信息
    再用match()判断是不是移动端

    router.beforeEach((to, from, next) => {
    
    var ismobile = localStorage.getItem('ismobile');
    
      if(ismobile == null){
        let flag = navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i)
    
        ismobile = flag ? 1 : 0
      }
    
      // 电脑
      if (ismobile == 0) {
        if (to.path == '/') {
            next('/admin_index')
        } else {
            next();
        }
      }
    
      // 手机
      if (ismobile == 1) {
        if (to.path == '/') {
            next('/home')
        } else {
            next();
        }
      }
    
    })
    
    展开全文
  • 正文开始之前我要给大家分享波免费的资料,需要的同学可以留言评论 、看准 投资就是投人, 投资就是投团队, 尤其要看准团队的领头人。 对目标企业团队成员的要求是: 富有激情, 和善诚信、 专业敬业、 ...

     

    正文开始之前我要给大家分享一波免费的资料,需要的同学可以留言评论

    一、看准

    投资就是投人, 投资就是投团队, 尤其要看准团队的领头人。 对目标企业团队成员的要求是: 富有激情, 和善诚信、 专业敬业、 善于学习。

    二、发掘一两大优势(优势行业、 优势企业)

    在优势行业中发掘、 寻找优势企业。

    优势行业是指具有广阔发展前景、 国家政策支持、 市场成长空间巨大的行业;

    优势企业是在优势行业中具有核心竞争力,细分行业排名靠前的优秀企业, 其核心业 务或主营业务要突出, 企业的核心竞争力要突出, 要超越其他竞争者。

    三、弄清

    三个模式(业务模式、 盈利模式、 营销模式)

    就是弄清目标企业是如何挣钱的。 业务模式是企业提供什么产品或服务, 业务流程如何实现, 包括业务逻辑是否可行, 技术是否可行, 是否符合消费者心理和使用习惯等, 企业的人力、 资金、 资源是否足以支持。 盈利模式是指企业如何挣钱, 通过什么手段或环节挣钱。 营销模式是企业如何推广自己的产品或服务, 销售渠道、 销售激励机制如何等。 好的业务模式, 必须能够盈利, 好的盈利模式, 必须能够推行。

    四、查看

    四个指标(营业收入、营业利润、净利率、增长率)

    投资的重要目标是目标企业尽快改制上市,关注、查看目标企业近三年的上述前两个指标尤为重要。看重的盈利能力和成长性,我们由此关注上述的后两个指标。净利率是销售净利润率,表达了一个企业的盈利能力和抗风险能力。增长率可以迅速降低投资成本。让投资人获取更高的投资回报。把握这四个指标,则基本把握了项目的可投资性。

    五、 厘清

    五个结构(股权结构、 高管结构、 业务结构、 客户结构、 供应商结构)

    厘清五个结构也很重要, 让投资人对目标企业的具体结构很清晰, 便于判断企业的好坏优劣。

    六、考察

    六个层面(历史合规、财务规范、依法纳税、产权清晰、劳动 合规、环保合规)

    考察六个层面是对目标企业的深度了解,任何一个层面存在关键性问题,就可能影响企业的改制上市,当然,有些企业存在一些键性问题,就细小瑕疵,可可能影响企业的改制上以通过规范于段予以改进。市。

    历史合规:目标企业的历史沿革合法合规,在注册验资、股权变更等方面不存在重大历史瑕疵:

    财务规范:财务制度健全,会计标准合规,坚持公正审计:

    依法纳税:不存在纳税的问题:

    产权清晰:企业的产权清晰到位(含专利、商标、房产等),不存在纠纷;

    劳动合规:严格执行劳动法规:

    七、落实

    七个关注(制度汇编、 例会制度、 企业文化、战略规划、 人力资源、 公共关系、 激励机制)

    七个关注是对目标企业细小环节的关注。 如果其中存在问题, 可以通过规范、 引导的办法加以改进。 但其现状是判断目标企业经营管理的重要依据。

    八、分析

    八个数据、总资产周转率, 资产负债率、 流动比率、 应收帐款周转率、销售毛利率、 净值报酬率、 经营活动净现金流、 市场占有率。

    在理清四个指标的基础上, 我们很有必要分析以上八个数据, 这是我们队目标企业的深度分析、 判断。

    资产周转率: 表示多资产表明一个公司是资产(资本〉密集型还是轻资产型。 该项指标反映资产总额的周转速度,周转越快, 反映销售能力越强, 企业可以通过薄利多销的办法, 加速资产的周转, 带来利润绝对数的增加。

    资产负债率: 资产负债率是负债总额除以资产总额的百分比, 也就是负债总额与资产总额的比例关系。 资产负债率反映在总资产中有多大比例是通过借债来筹资的, 也可以衡量企业在清算时保护债权人利益的程度;资产负债 率的高低, 体现一个企业的资本结构是否合理。

    流动比率: 流动比率是流动资产除以流动负债的比例, 反映企业的短期偿债能力。 流动资产是最容易变现的资产,流动资产越多, 流动负债越少, 则短期偿债能力越强。

    应收账款周转率(应收账款周转天数〉: 应收账款周转率反映应收账款的周转速度, 也就是年度内应收账款转为现金的平均次数。

    销售毛利率: 销售毛利率, 是企业销售净利率的最初基础, 没有足够大的毛利率便不能盈利。

    净值报酬率: 净值报酬率是净利润与平均股东权益(所有者权益)的百分比, 也叫股东权益报酬率。 该指标反映股东权益的收益水平。

    经营活动净现金流: 经营活动净现金流, 是企业在一个会计期间经营活动产生的现金流入与经营活动产生的现金流出的差额。

    市场占有率: 市场占有率, 也可称为 “市场份额” 是企业在运作的市场上所占有的百分比, 是企业的产品在市场上所占份额, 也就是企业对市场的控制能力。

    展开全文
  • 《程序设计综合训练实践报告》 此项目为图书信息管理系统,是一个采用了mysql+mybatis框架+java编写的maven项目

    一、实验目的

    题目七 图书信息管理系统

    1 功能描述

    设计一个图书信息管理系统,使之具有新建图书信息、显示、插入、删除、查询和排序等功能。

    2 具体设计要求

    图书信息包括:图书编号、书名、作者名、出版单位、出版时间、价格等。

    系统以菜单方式工作:

    ① 图书信息录入(要求图书信息用文件保存)

    ② 图书信息浏览

    ③ 插入图书信息

    ④ 查询(可以有多种查询方式,如按书名查询、按作者名查询、按价格查询等);

    ⑤ 排序(可以按图书编号排序、按书名排序、按出版时间排序、按价格排序等);

    ⑥ 修改图书信息

    ⑦ 删除图书信息

    二、项目概况

    1.总述

    此项目为图书信息管理系统,是一个采用了mysql+mybatis框架+java编写的maven项目

    2. 技术栈选择

    Mysql,mybatis

    3.环境介绍

    数据库:mysql8.0

    框架:mybatis

    项目结构:maven3.0

    语言:Java

    Jdk版本:jdk11.0.5(Jdk8.0以上)

    编写的IDE:IDEA 2020.01

    依赖jar包:
    在这里插入图片描述

    4. 功能概述

    该图书信息管理系统实现了便捷的图书信息管理,利用命令行操作的方式让用户的操作更为简洁。

    本系统提供Sql和noSql两种运行模式。

    Sql模式直接对mysql数据库进行操作,便于数据的持久化和规范化,让数据能够更加便捷高效,同时可以存储大量数据,便于进行大数据的管理,如果你想真正用此系统管理你的信息,建议采用此种模式。

    noSql模式是把数据载入内存中,优点是速度快,但缺点也很明显,在面对大量数据的情况下显得有些力不从心,此模式建议在数据量小的情况下使用。

    两种模式都支持以下功能:

    在这里插入图片描述

    5.功能结构

    在这里插入图片描述

    6.项目文件结构

    在这里插入图片描述

    三、数据结构描述

    1.实体类Book(持久化层)

    在这里插入图片描述

    2.Sql模式下的数据库结构

    在这里插入图片描述

    库名:library

    表名:books

    字段名 代表含义 数据类型 格式
    id 图书编号 INT 主键,PK,not null
    title 书名 VARCHAR(20) not null
    name 作者名 VARCHAR(20) not null
    publisher 出版商 VARCHAR(20)
    time 出版时间 DATE
    price 价格 DECIMAL(7,4)

    3.Sql模式下的Mapper映射(接口结构)

    在这里插入图片描述

    4.noSql模式下的数据结构

    采用LinkedList<Book>来维护图书信息
    在这里插入图片描述

    四、程序模块描述

    该项目大致分为两个一级模块,分别在两个java文件中
    在这里插入图片描述

    各个文件内用分别有若干个二级模块

    1.图书信息录入(通过文件录入)模块

    在这里插入图片描述

    2. 图文信息浏览模块

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cjHHTSx8-1592834754185)(media/image13.png)]{width="3.058333333333333in" height="0.25in"}

    3. 插入模块

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ijayrnyv-1592834754186)(media/image14.png)]{width="3.3in" height="0.3in"}

    4. 查询模块

    在这里插入图片描述

    其下按查询条件有分为若干个三级模块

    5. 排序模块

    在这里插入图片描述

    其下按排序条件有分为若干个三级模块

    6. 更新模块

    7. 删除模块

    8. 写出模块

    至于各模块的功能,见名知意,在此就不一一赘述了。

    而对于各个模块的返回值和参数,容我卖个关子,此内容将在下个模块中讲解!

    五、主要功能模块的算法流程图

    1.Sql模式下的算法流程(以查找为例)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Do8u8qto-1592834754194)(media/image20.png)]{width="5.763194444444444in" height="6.290972222222222in"}

    2. 各个模块间的连接算法流程

    "套娃"式的连接方式

    何为"套娃"?

    可以理解为类似递归的连接方式。

    与递归有什么不同?

    如果采用递归,就要把低层级的模块套在高层级的模块里。这样子虽然连接了各个模块,达到了类似菜单的效果,但有以下两个较为致命的缺点:

    1. 当运行完一个模块后,该模块就运行结束,无法做到循环操作,这与实验要求不符

    2. 由于该项目是一个系统,各模块间要不断退出重进,循环多次的递归会造成堆栈不断压缩,有堆栈溢出风险

      那我们该如何做呢?

      对于第一个缺点,我采用了一个死循环来解决;对于第二个缺点,我的思路就是把改变模块重复调用的时机和位置,让它既能达到效果,又不会导致堆栈溢出

      基本思路:

      每个函数都有一个整数类型的返回值,只要返回1,就说明该级模块需要退回上一级;返回0则说明不需要,即留在当前模块。

      而是留在当前模块还是返回上一级模块,由该模块(记为模块3)的上一级(记为模块2)控制,如果模块3返回一了,就在模块2的上一级(记为模块1)再次调用模块2,即可做到返回上一层;而如果模块3返回0则在模块2再次循环调用,直至模块返回1
      这样做不仅能实现功能,而且能避免多次"套娃"导致堆栈溢出的风险

      在这里插入图片描述

    六、代码清单

    1.项目结构全览

    在这里插入图片描述

    2. 实体类Book

    ackage com.dreamchaser.domain;
    
    import java.math.BigDecimal;
    import java.util.Date;
    
    /**
     * books
     * 
     * @author 金昊霖
     */
    public class Book {
    
        /** 图书编号 */
        private Integer id;
    
        /** 图书名称 */
        private String title;
    
        /** 作者姓名 */
        private String name;
    
        /** 出版社 */
        private String publisher;
    
        /** 出版时间 */
        private Date time;
    
        /** 价格 */
        private BigDecimal price;
    
        public Book() {
        }
    
        /**
         * 用于清空对象里的数据
         */
        public void clear(){
            this.id=null;
            this.title=null;
            this.name=null;
            this.publisher=null;
            this.time=null;
            this.price=null;
        }
    
        public Book(Integer id, String title, String name, String publisher, Date time, BigDecimal price) {
            this.id = id;
            this.title = title;
            this.name = name;
            this.publisher = publisher;
            this.time = time;
            this.price = price;
        }
    
        public Book(BigDecimal price) {
            this.price = price;
        }
    
        /**
         * 获取图书编号
         * 
         * @return 图书编号
         */
        public Integer getId() {
            return this.id;
        }
    
        /**
         * 设置图书编号
         * 
         * @param id
         *          图书编号
         */
        public void setId(Integer id) {
            this.id = id;
        }
    
        /**
         * 获取图书名称
         * 
         * @return 图书名称
         */
        public String getTitle() {
            return this.title;
        }
    
        /**
         * 设置图书名称
         * 
         * @param title
         *          图书名称
         */
        public void setTitle(String title) {
            this.title = title;
        }
    
        /**
         * 获取作者姓名
         * 
         * @return 作者姓名
         */
        public String getName() {
            return this.name;
        }
    
        /**
         * 设置作者姓名
         * 
         * @param name
         *          作者姓名
         */
        public void setName(String name) {
            this.name = name;
        }
    
        /**
         * 获取出版社
         * 
         * @return 出版社
         */
        public String getPublisher() {
            return this.publisher;
        }
    
        /**
         * 设置出版社
         * 
         * @param publisher
         *          出版社
         */
        public void setPublisher(String publisher) {
            this.publisher = publisher;
        }
    
        /**
         * 获取出版时间
         * 
         * @return 出版时间
         */
        public Date getTime() {
            return this.time;
        }
    
        /**
         * 设置出版时间
         * 
         * @param time
         *          出版时间
         */
        public void setTime(Date time) {
            this.time = time;
        }
    
        /**
         * 获取价格
         * 
         * @return 价格
         */
        public BigDecimal getPrice() {
            return this.price;
        }
    
        /**
         * 设置价格
         * 
         * @param price
         *          价格
         */
        public void setPrice(BigDecimal price) {
            this.price = price;
        }
    
      
    }
    

    3.Mapper映射

    ①接口类 BooKMapper

    package com.dreamchaser.mapper;
    
    import com.dreamchaser.domain.Book;
    
    import java.util.List;
    import java.util.Map;
    
    public interface BookMapper {
        public List<Book> selectSmaller(Map<String,Object> map);
        public List<Book> selectAll();
        public List<Book> selectBigger(Map<String,Object> map);
        public List<Book> findBookByCondition(Map<String,Object> map);
        public List<Book> findBooksById(int id);
        public void insertBook(Map<String,Object> map);
        public void insertBooks(List<Book> books);
        public void updateBookById(Map<String,Object> map);
        public void updateBooks(List<Map<String,Object>> list);
        public void deleteBookById(int id);
        public void deleteBooksByIds(List<Integer> ids);
        /**
         * @param map 要注意这里的map中只能有一个对象
         * @return
         */
        public List<Book> findBookByConditionO(Map<String,Object> map);
    }
    

    ②BookMapper.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <!-- books 路径不是/,而是.!!!-->
    <mapper namespace="com.dreamchaser.mapper.BookMapper">
        <!-- 字段映射 -->
        <resultMap id="booksMap" type="Book">
            <!--设置主键,提高mybatis性能-->
            <id property="id" column="id"/>
        </resultMap>
    
    
    
        <!-- 表查询字段 -->
        <sql id="allColumns">
            b.id, b.title, b.name, b.publisher, b.time, b.price
        </sql>
    
        <!-- 查询小于条件的结果 多条会自动打包成list  注:&lt;指的是<,在xml中<是非法的 -->
        <select id="selectSmaller" parameterType="map" resultMap="booksMap">
            SELECT
            <include refid="allColumns"/>
            from books b
            <where>
                <if test="time != null and time!=''">
                    AND b.time &lt;=#{time}
                </if>
                <if test="price != null and price !=''">
                    AND b.price &lt;=#{price}
                </if>
            </where>
        </select>
    
        <!-- 查询小于条件的结果   注:&lt;指的是<,在xml中<是非法的 -->
        <select id="selectBigger" parameterType="map" resultMap="booksMap">
            SELECT
            <include refid="allColumns"/>
            from books b
            <where>
                <if test="time != null and time!=''">
                    AND b.time >=#{time}
                </if>
                <if test="price != null and price !=''">
                    AND b.price >=#{price}
                </if>
            </where>
        </select>
        <!-- 查询所有数据 -->
        <select id="selectAll" resultMap="booksMap">
            SELECT
            <include refid="allColumns" />
            FROM books b
        </select>
        
        <!-- 根据条件参数查询数据列表 -->
        <select id="findBookByCondition" resultMap="booksMap" parameterType="map">
            SELECT
            <include refid="allColumns" />
            FROM books b WHERE 1 = 1
            <if test="title != null and title != ''">
                AND b.title LIKE CONCAT('%', #{title}, '%')
            </if>
            <if test="name != null and name != ''">
                AND b.name LIKE CONCAT('%', #{name}, '%')
            </if>
            <if test="publisher != null and publisher != ''">
                AND b.publisher LIKE CONCAT('%', #{publisher}, '%')
            </if>
            <if test="time != null">
                AND b.time = #{time}
            </if>
            <if test="price != null">
                AND b.price = #{price}
            </if>
        </select>
        
        <!-- 根据主键查询数据 -->
        <select id="findBooksById" resultMap="booksMap" parameterType="int">
            SELECT
            <include refid="allColumns" />
            FROM books b WHERE b.id =#{id}
        </select>
        
        <!-- 插入数据 -->
        <insert id="insertBook" parameterType="map">
            INSERT INTO books (
                id, title, name, publisher, time, price
            ) VALUES (
                #{id},
                #{title},
                #{name},
                #{publisher},
                #{time},
                #{price}
            )
        </insert>
        
        <!-- 批量插入数据 -->
        <insert id="insertBooks" parameterType="list">
            INSERT INTO books (
                id, title, name, publisher, time, price
            ) VALUES
            <foreach collection="list" index="index" item="item" separator=",">
                (
                    #{item.id},
                    #{item.title},
                    #{item.name},
                    #{item.publisher},
                    #{item.time},
                    #{item.price}
                )
            </foreach>
        </insert>
        
        <!-- 修改数据 -->
        <update id="updateBookById" parameterType="map">
            UPDATE books
            <set>
                <if test="title != null">
                    title = #{title},
                </if>
                <if test="name != null">
                    name = #{name},
                </if>
                <if test="publisher != null">
                    publisher = #{publisher},
                </if>
                <if test="time != null">
                    time = #{time},
                </if>
                <if test="price != null">
                    price = #{price}
                </if>
            </set>
            WHERE id = #{id}
        </update>
        
        <!-- 批量修改数据 -->
        <update id="updateBooks" parameterType="list">
            <foreach collection="list" index="index" item="item" separator=";">
                UPDATE books
                <set>
                    <if test="item.title != null">
                        title = #{item.title},
                    </if>
                    <if test="item.name != null">
                        name = #{item.name},
                    </if>
                    <if test="item.publisher != null">
                        publisher = #{item.publisher},
                    </if>
                    <if test="item.time != null">
                        time = #{item.time},
                    </if>
                    <if test="item.price != null">
                        price = #{item.price}
                    </if>
                </set>
                WHERE id = #{item.id}
            </foreach>
        </update>
        
        <!-- 根据主键删除数据 -->
        <delete id="deleteBookById" parameterType="int">
            DELETE FROM books WHERE id = #{id}
        </delete>
        
        <!-- 根据主键批量删除数据 -->
        <delete id="deleteBooksByIds" parameterType="list">
            DELETE FROM books WHERE id IN
            <foreach collection="list" index="index" item="id" open="(" separator="," close=")">
                #{id}
            </foreach>
        </delete>
    
        <!-- 根据条件排序查询-->
        <select id="findBookByConditionO" resultMap="booksMap" parameterType="map">
            SELECT
            <include refid="allColumns" />
            FROM books b WHERE 1 = 1
            <if test="title != null and title != ''">
                ORDER BY title
            </if>
            <if test="name != null and name != ''">
                ORDER BY name
            </if>
            <if test="time != null">
                ORDER BY time
            </if>
            <if test="price != null">
                ORDER BY price
            </if>
        </select>
    </mapper>
    

    4.工具类 {#工具类 .list-paragraph}

    ①日期工具类 DateUtil

    用于Date和字符串之间的转换

    package com.dreamchaser.util;
    
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    /**
     * 日期工具类,用于date和字符串之间的转换
     * @author 金昊霖
     */
    public class DateUtil {
        public static String dateToString(Date date,String format){
            return new SimpleDateFormat(format).format(date);
        }
        public static Date stringToDate(String date,String format){
            try {
                return new SimpleDateFormat(date).parse(date);
            } catch (ParseException e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    

    ②展示工具类 Displayer

    用于展示传入的数据

    package com.dreamchaser.util;
    
    import com.dreamchaser.domain.Book;
    import java.util.List;
    
    /**
     * 显示类,用于展示传入的数据
     *
     * @author 金昊霖
     */
    public class Displayer {
    //    public static void main(String[] args) {
    //        Book book = new Book(123, "狂人日记", "鲁迅", "追梦出版社", new Date("2020/6/18"),
    //                new BigDecimal("30.06"));
    //        List<Book> list = new LinkedList<>();
    //        list.add(book);
    //        show(list);
    //    }
    
    
        public static void show(List<Book> list) {
            System.out.println("--------------------------------------------------------------------------");
            System.out.println("| 图书编号    书名           作者名     出版单位       出版时间            价格  |");
            for (Book book : list) {
                String date="";
                if (book.getTime()!=null){
                    date=DateUtil.dateToString(book.getTime(),"yyyy-MM-dd");
                }
                System.out.println("| " + book.getId() + "       " + book.getTitle() + "         " + book.getName() + "     " +
                        book.getPublisher() + "     "+date+ "      " + book.getPrice() + " |");
            }
            System.out.println("--------------------------------------------------------------------------");
        }
    
        public static void show(Book book) {
            System.out.println("-------------------------------------------------------------------------");
            System.out.println("| 图书编号    书名           作者名     出版单位       出版时间            价格 |");
    
    
            String date="";
            if (book.getTime()!=null){
                date=DateUtil.dateToString(book.getTime(),"yyyy-MM-dd");
            }
            System.out.println("| " + book.getId() + "       " + book.getTitle() + " " + book.getName() + "     " +
                    book.getPublisher() + "     "+date+ "      " + book.getPrice() + " |");
    
            System.out.println("-------------------------------------------------------------------------");
        }
    }
    

    ③文件工具类 FileUtil

    package com.dreamchaser.util;
    
    import com.dreamchaser.domain.Book;
    
    import java.io.*;
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 读取文件工具类
     */
    public class FileUtil {
        /**
         * 读取txt文件内容
         * @param path 文件路径
         * @return List<String> txt文件内容封装成一个list返回,如果文件不存在就返回null
         */
        public static List<String> readTxtFile(String path){
            File file=new File(path);
            List<String> list=new ArrayList<>();
            if (file.exists()&&file.isFile()){
                BufferedReader reader=null;
                try {
                    reader=new BufferedReader(new FileReader(file));
                    String line=null;
                    while ((line=reader.readLine())!=null){
                        list.add(line);
                    }
    
                    return list;
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }finally {
                    try {
                        //最后将输入流关闭
                        reader.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
    
            }
            return null;
        }
    
        public static boolean writeFile(List<Book> list,String path){
            File file=new File(path);
            if (file.exists()&&file.isFile()){
                try {
                    BufferedWriter bufferedWriter=new BufferedWriter(new FileWriter(file));
                    for (Book book:list){
                        bufferedWriter.write(book.getId()+"    ");
                        bufferedWriter.write(book.getTitle()+"    ");
                        bufferedWriter.write(book.getName()+"    ");
                        bufferedWriter.write(book.getPublisher()+"    ");
                        bufferedWriter.write(book.getTime()+"    ");
                        bufferedWriter.write(book.getPrice()+"    ");
                        bufferedWriter.newLine();
                        bufferedWriter.flush();
                        bufferedWriter.close();
                        return true;
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
    
            }
            return false;
        }
    

    ④SqlSession工具类

    用于返回所需的SqlSession对象

    package com.dreamchaser.util;
    
    import org.apache.ibatis.io.Resources;
    import org.apache.ibatis.session.SqlSession;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.apache.ibatis.session.SqlSessionFactoryBuilder;
    
    import java.io.IOException;
    import java.io.InputStream;
    
    /**
     * SqlSession工具类
     * 用于产生sqlSession
     */
    public class SqlSessionUtil {
        public SqlSession getSqlSession(){
            // 指定全局配置文件
            String resource = "mybatis-config.xml";
            // 读取配置文件
            InputStream inputStream = null;
            try {
                inputStream = Resources.getResourceAsStream(resource);
            } catch (IOException e) {
                e.printStackTrace();
            }
            // 构建sqlSessionFactory
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            return sqlSessionFactory.openSession();
        }
    }
    

    5.模块类 {#模块类 .list-paragraph}

    ①总驱动类 Driver

    用于整个项目的驱动

    package com.dreamchaser;
    
    
    import java.util.Scanner;
    
    /**
     * 驱动类
     * 主要负责统一Sql和NoSql两个模式
     * 为了便捷,功能之间采用“套娃”的方式连接(实质就是避免递归调用造成堆栈溢出)
     * 套娃的基本思路:每个函数都有一个整数类型的返回值,只要返回1,就能返回上一级(实现原理,由该函数(记为函数1)的
     * 上一级(记为函数2)控制,如果函数1返回一了,就在函数2的上一级(记为函数3)再次调用函数2,即可做到返回上一层;
     * 而如果函数1返回0则在函数2再次循环调用,直至函数返回1)
     * 这样做不仅能实现功能,而且能避免多次“套娃”导致堆栈溢出的风险
     * @author 金昊霖
     */
    public class Driver {
    
        static Scanner scanner = new Scanner(System.in);
    
        public  static void main(String[] args) {
    
            while (true) {
                if (choosePattern(1) == 1) {
                    break;
                }
            }
        }
        /**
         * 一级模块
         * 选择模式模块
         * i 用于递归时判断其是否是第一次来还是输入错误来的
         *
         * @return 用于判断函数状态,0表示继续呆在这层,1表示退回上一层
         */
        private  static int choosePattern(int i) {
            if (i == 1) {
                System.out.println("\n\n\n||||图书信息管理系统||||       \n");
                System.out.println("技术栈选择:mysql+mybatis+java");
                System.out.println("作者:软工1902 金昊霖\n");
                System.out.println("请选择存储模式:");
                System.out.println("1.mysql存储(持久化规范数据存储)");
                System.out.println("2.简单运存存储(如要数据持久化,则需导出文件)");
                System.out.println("3.退出该系统\n\n");
                System.out.println("请输入你的选择(序号):");
            }
    
            switch (scanner.nextInt()) {
                case 1:
                    //这样做既能使其能返回上一级,而且想留在此级时不会造成“无限套娃”,即堆栈不断压缩的情况
                    while (true) {
                        if (PatternSql.chooseFunction(1) == 1) {
                            return 0;
                        }
                    }
                case 2:
                    while (true) {
                        if (PatternNoSql.chooseFunction(1) == 1) {
                            return 0;
                        }
                    }
                case 3:
                    return 1;
                default:
                    System.out.println("抱歉,输入的是非法字符,请重新输入:");
                    //这里是采用递归,暂时没办法,如不采用会很麻烦
                    return choosePattern(0);
            }
        }
    }
    

    ②SqlPattern模块类

    集成Sql模式

    package com.dreamchaser;
    
    import com.dreamchaser.mapper.BookMapper;
    import com.dreamchaser.domain.Book;
    import com.dreamchaser.util.DateUtil;
    import com.dreamchaser.util.Displayer;
    import com.dreamchaser.util.FileUtil;
    import com.dreamchaser.util.SqlSessionUtil;
    import org.apache.ibatis.session.SqlSession;
    
    import java.math.BigDecimal;
    import java.util.*;
    
    /**
     * Sql模式
     */
    public class PatternSql {
        static Scanner scanner = new Scanner(System.in);
        /**
         * mybatis中的sql
         */
        static SqlSession sqlSession = new SqlSessionUtil().getSqlSession();
        /**
         * 获取mybatis为我们创建好的Mapper对象
         */
        static BookMapper bookMapper = sqlSession.getMapper(BookMapper.class);
        /**
         * 空map,用于记录数据,每次方法加载时都要清空
         */
        static Map<String, Object> map = new HashMap<>();
        /**
         * 测试xml到底有没有载入
         * 这里就不删了,留作纪念
         * @param args
         */
    //    public static void main(String[] args) {
            PatternSql patternSql=new PatternSql();
    //        SqlSession sqlSession =new SqlSessionUtil().getSqlSession();
    //        List list = sqlSession.selectList("com.dreamchaser.mapper.BookMapper.selectAll");
    //        Displayer.show(list);
    //    }
    
        /**
         * 二级模块
         *
         * @param i 用于递归时判断其是否是第一次来还是输入错误来的
         * @return 用于返回上一级, 返回1就表示需要返回上一级,返回0则说明继续执行
         */
        public static int chooseFunction(int i) {
            if (i == 1) {
                System.out.println("\n\n功能:");
                System.out.println("1.图书信息录入");
                System.out.println("2.图书信息浏览");
                System.out.println("3.插入图书信息");
                System.out.println("4.查询");
                System.out.println("5.排序");
                System.out.println("6.修改图书信息");
                System.out.println("7.删除图书信息");
                System.out.println("8.导出为文件");
                System.out.println("9.返回模式选择");
                System.out.println("\n\n请输入你需要选择的功能(序号):");
            }
    
            while (true) {
                switch (scanner.nextInt()) {
                    case 1:
                        saveFile();
                        return 0;
                    case 2:
                        selectAll();
                        return 0;
                    case 3:
                        addOneBook();
                        return 0;
                    case 4:
                        while (true) {
                            if (chooseSelect(1) == 1) {
                                return 0;
                            }
                        }
                    case 5:
                        while (true) {
                            if (chooseOrder(1) == 1) {
                                return 0;
                            }
                        }
                    case 6:
                        updateBook();
                        return 0;
                    case 7:
                        deleteOne();
                        return 0;
                    case 8:
                        writeToFile();
                        return 0;
                    case 9:
                        return 1;
                    default:
                        System.out.println("抱歉,输入的是非法字符,请重新输入:");
                        return chooseFunction(0);
                }
            }
        }
    
        private static void deleteOne() {
            System.out.println("请输入你要删除书籍的图书编号:");
            bookMapper.deleteBookById(scanner.nextInt());
            System.out.println("删除成功!");
        }
    
        private static void updateBook() {
            map.clear();
            System.out.println("请输入你要更新的书籍图书编号:");
            map.put("id", scanner.nextInt());
            scanner.nextLine();
            System.out.println("是否要修改其图书名称(若是,请输入其名称,若否,则输入“否”):");
            String title = scanner.nextLine();
            if (!title.equals("否")) {
                map.put("title", title);
            }
            System.out.println("是否要修改其作者姓名(若是,请输入其姓名,若否,则输入“否”):");
            String name = scanner.nextLine();
            if (!name.equals("否")) {
                map.put("name", name);
            }
            System.out.println("是否要修改其出版社(若是,请输入其出版社名称,若否,则输入“否”):");
            String publisher = scanner.nextLine();
            if (!publisher.equals("否")) {
                map.put("publisher", publisher);
            }
            System.out.println("是否要修改其出版时间(若是,请输入其时间,请以“2020/10/06”的形式输入时间,若否,则输入“否”):");
            String time = scanner.nextLine();
            if (!time.equals("否")) {
                map.put("time", new Date(time));
            }
            System.out.println("是否要修改其价格(若是,请输入其价格,若否,则输入“否”):");
            String price = scanner.nextLine();
            if (!price.equals("否")) {
                map.put("price", new BigDecimal(price));
            }
            bookMapper.updateBookById(map);
            System.out.println("更新成功!");
        }
        private static void writeToFile() {
            System.out.println("请输入你要保存的文件名(如:dreamchaser.txt):");
            scanner.nextLine();
            Boolean flag=FileUtil.writeFile(bookMapper.selectAll(),scanner.nextLine());
            if (flag){
                System.out.println("保存成功!");
            }else {
                System.out.println("保存失败,请确认输入的文件路径是否正确!");
            }
    
        }
    
        private static void saveFile() {
            System.out.println("请输入要录入的文件(txt,且需每一行都是一条记录)路径(绝对路径或者相对路径皆可):");
            //把回车吃掉
            scanner.nextLine();
            List<String> list = FileUtil.readTxtFile(scanner.nextLine());
            String[] strings=null;
            if (list!=null){
                for (String s:list){
                    strings=s.split(" |\n");
                    map.clear();
                    map.put("id",strings[0]);
                    map.put("title",strings[1]);
                    map.put("name",strings[2]);
                    map.put("publisher",strings[3]);
                    map.put("time", DateUtil.stringToDate(strings[4],"yyyy/MM/dd"));
                    map.put("price",new BigDecimal(strings[5]));
                    bookMapper.insertBook(map);
                }
                System.out.println("录入成功");
            }else {
                System.out.println("文件未找到或者文件不符合要求。请重新输入!");
            }
    
        }
    
        private static void selectAll() {
            List<Book> books = bookMapper.selectAll();
            Displayer.show(books);
        }
    
        private static void addOneBook() {
            map.clear();
            System.out.println("请输入你要插入书籍的图书编号:");
            map.put("id", scanner.nextInt());
            scanner.nextLine();
            System.out.println("请输入你要插入书籍的图书名称:");
            String title = scanner.nextLine();
            map.put("title", title);
            System.out.println("请输入你要插入书籍的作者姓名:");
            String name = scanner.nextLine();
            map.put("name", name);
            System.out.println("请输入你要插入书籍的出版社(若是,请输入其出版社名称,若否,则输入“否”):");
            String publisher = scanner.nextLine();
            if (!publisher.equals("否")) {
                map.put("publisher", publisher);
            }
            System.out.println("请输入你要插入书籍的出版时间(若是,请输入其时间,请以“2020/10/06”的形式输入时间,若否,则输入“否”):");
            String time = scanner.nextLine();
            if (!time.equals("否")) {
                map.put("time", new Date(time));
            }
            System.out.println("请输入你要插入书籍的价格(若是,请输入其价格,若否,则输入“否”):");
            String price = scanner.nextLine();
            if (!price.equals("否")) {
                map.put("price", new BigDecimal(price));
            }
            bookMapper.insertBook(map);
            System.out.println("插入成功");
        }
    
        /**
         * 排序模块
         * 三级模块
         *
         * @param i
         * @return
         */
        private static int chooseOrder(int i) {
            if (i == 1) {
                System.out.println("\n\n排序:");
                System.out.println("1.按图书编号排序");
                System.out.println("2.按书名排序");
                System.out.println("3.按出版时间排序");
                System.out.println("4.按价格排序等");
                System.out.println("5.返回功能选择");
                System.out.println("\n\n请输入你需要的排序方式(序号):");
            }
    
            while (true) {
                switch (scanner.nextInt()) {
                    case 1:
                        selectOrderById();
                        return 0;
                    case 2:
                        selectOrderByTitle();
                        return 0;
                    case 3:
                        selectOrderByTime();
                        return 0;
                    case 4:
                        selectOrderByPrice();
                        return 0;
                    case 5:
                        return 1;
                    default:
                        System.out.println("抱歉,输入的是非法字符,请重新输入:");
                        return chooseFunction(0);
                }
            }
        }
    
        private static void selectOrderByPrice() {
            map.clear();
            map.put("price", "1");
            List<Book> books = bookMapper.findBookByConditionO(map);
            Displayer.show(books);
        }
    
        private static void selectOrderByTime() {
            map.clear();
            map.put("time", "1");
            List<Book> books = bookMapper.findBookByConditionO(map);
            Displayer.show(books);
        }
    
        private static void selectOrderByTitle() {
            map.clear();
            map.put("title", "1");
            List<Book> books = bookMapper.findBookByConditionO(map);
            Displayer.show(books);
        }
    
        private static void selectOrderById() {
            map.clear();
            map.put("id", "1");
            List<Book> books = bookMapper.findBookByConditionO(map);
            Displayer.show(books);
        }
    
        /**
         * 查询模块
         * 三级模块
         *
         * @param i
         * @return
         */
        private static int chooseSelect(int i) {
            if (i == 1) {
                System.out.println("\n\n查询:");
                System.out.println("1.按书名查询");
                System.out.println("2.按作者名查询");
                System.out.println("3.按价格查询(小于)");
                System.out.println("4.按价格查询(等于)");
                System.out.println("5.按价格查询(大于)");
                System.out.println("6.返回模式选择");
                System.out.println("\n\n请输入你需要的查询方式(序号):");
            }
    
            while (true) {
                switch (scanner.nextInt()) {
                    case 1:
                        selectByTitle();
                        return 0;
                    case 2:
                        selectByName();
                        return 0;
                    case 3:
                        selectByPriceS();
                        return 0;
                    case 4:
                        selectByPrice();
                        return 0;
                    case 5:
                        selectByPriceB();
                        return 0;
                    case 6:
                        return 1;
                    default:
                        System.out.println("抱歉,输入的是非法字符,请重新输入:");
                        return chooseFunction(0);
                }
            }
        }
    
        private static void selectByPriceB() {
            System.out.println("请输入你要查询的价格:");
            map.clear();
            map.put("price", scanner.nextInt());
            List<Book> books = bookMapper.selectBigger(map);
            Displayer.show(books);
        }
    
        private static void selectByPrice() {
            System.out.println("请输入你要查询的价格:");
            map.clear();
            map.put("price", scanner.nextInt());
            List<Book> books = bookMapper.findBookByCondition(map);
            Displayer.show(books);
        }
    
        private static void selectByPriceS() {
            System.out.println("请输入你要查询的价格:");
            map.clear();
            map.put("price", new BigDecimal(scanner.nextInt()));
            List<Book> books = bookMapper.selectSmaller(map);
            Displayer.show(books);
        }
    
        private static void selectByName() {
            System.out.println("请输入你要查询的作者姓名:");
            map.clear();
            //因为后面是nextLine,而之前是nextInt,会算上回车键,所以得先用nextLine吃掉回车
            scanner.nextLine();
            map.put("name", scanner.nextLine());
            List<Book> books = bookMapper.findBookByCondition(map);
            Displayer.show(books);
        }
    
        private static void selectByTitle() {
            System.out.println("请输入你要查询的书籍名称:");
            map.clear();
            scanner.nextLine();
            map.put("title", scanner.nextLine());
            List<Book> books = bookMapper.findBookByCondition(map);
            Displayer.show(books);
        }
    
    }
    

    ③noSql模块类

    集成noSql模式

    package com.dreamchaser;
    
    import com.dreamchaser.domain.Book;
    import com.dreamchaser.util.DateUtil;
    import com.dreamchaser.util.Displayer;
    import com.dreamchaser.util.FileUtil;
    
    import java.math.BigDecimal;
    import java.util.*;
    
    public class PatternNoSql {
        static Scanner scanner = new Scanner(System.in);
        /**
         * 维护的链表
         */
        static List<Book> list = new LinkedList<>();
        static Book book = new Book();
    
    
    
        /**
         * 二级模块
         *
         * @param i 用于递归时判断其是否是第一次来还是输入错误来的
         * @return 用于返回上一级, 返回1就表示需要返回上一级,返回0则说明继续执行
         */
        public static int chooseFunction(int i) {
            if (i == 1) {
                System.out.println("\n\n功能:");
                System.out.println("1.图书信息录入");
                System.out.println("2.图书信息浏览");
                System.out.println("3.插入图书信息");
                System.out.println("4.查询");
                System.out.println("5.排序");
                System.out.println("6.修改图书信息");
                System.out.println("7.删除图书信息");
                System.out.println("8.导出为文件");
                System.out.println("9.返回模式选择");
                System.out.println("\n\n请输入你需要选择的功能(序号):");
            }
    
            while (true) {
                switch (scanner.nextInt()) {
                    case 1:
                        saveFile();
                        return 0;
                    case 2:
                        selectAll();
                        return 0;
                    case 3:
                        addOneBook();
                        return 0;
                    case 4:
                        while (true) {
                            if (chooseSelect(1) == 1) {
                                return 0;
                            }
                        }
                    case 5:
                        while (true) {
                            if (chooseOrder(1) == 1) {
                                return 0;
                            }
                        }
                    case 6:
                        updateBook();
                        return 0;
                    case 7:
                        deleteOne();
                        return 0;
                    case 8:
                        writeToFile();
                        return 0;
                    case 9:
                        return 1;
                    default:
                        System.out.println("抱歉,输入的是非法字符,请重新输入:");
                        return chooseFunction(0);
                }
            }
        }
    
        private static void writeToFile() {
            System.out.println("请输入你要保存的文件名(如:dreamchaser.txt):");
            scanner.nextLine();
            Boolean flag=FileUtil.writeFile(list,scanner.nextLine());
            if (flag){
                System.out.println("保存成功!");
            }else {
                System.out.println("保存失败,请确认输入的文件路径是否正确!");
            }
        }
    
        private static void deleteOne() {
            System.out.println("请输入你要删除书籍的图书编号:");
            int id = scanner.nextInt();
            boolean flag = list.removeIf(a -> a.getId() == id);
            if (flag) {
                System.out.println("删除成功!");
            } else {
                System.out.println("未找到相应的图书!");
            }
        }
    
        private static void updateBook() {
            book.clear();
            System.out.println("请输入你要更新的书籍图书编号:");
            book.setId(scanner.nextInt());
            scanner.nextLine();
            System.out.println("是否要修改其图书名称(若是,请输入其名称,若否,则输入“否”):");
            String title = scanner.nextLine();
            if (!title.equals("否")) {
                book.setTitle(title);
            }
            System.out.println("是否要修改其作者姓名(若是,请输入其姓名,若否,则输入“否”):");
            String name = scanner.nextLine();
            if (!name.equals("否")) {
                book.setName(name);
            }
            System.out.println("是否要修改其出版社(若是,请输入其出版社名称,若否,则输入“否”):");
            String publisher = scanner.nextLine();
            if (!publisher.equals("否")) {
                book.setPublisher(publisher);
            }
            System.out.println("是否要修改其出版时间(若是,请输入其时间,请以“2020/10/06”的形式输入时间,若否,则输入“否”):");
            String time = scanner.nextLine();
            if (!time.equals("否")) {
                book.setTime(new Date(time));
            }
            System.out.println("是否要修改其价格(若是,请输入其价格,若否,则输入“否”):");
            String price = scanner.nextLine();
            if (!price.equals("否")) {
                book.setPrice(new BigDecimal(price));
            }
            for (Book book1 : list) {
                if (book1.getId().equals(book.getId())) {
                    list.remove(book1);
                    list.add(book);
                }
            }
            System.out.println("更新成功!");
        }
    
        private static void saveFile() {
            System.out.println("请输入要录入的文件(txt,且需每一行都是一条记录)路径(绝对路径或者相对路径皆可):");
            //把回车吃掉
            scanner.nextLine();
            List<String> list1 = FileUtil.readTxtFile(scanner.nextLine());
            String[] strings = null;
            if (list1 != null) {
                for (String s : list1) {
                    strings = s.split(" |\n");
                    book.clear();
                    book.setId(Integer.parseInt(strings[0]));
                    book.setTitle(strings[1]);
                    book.setName(strings[2]);
                    book.setPublisher(strings[3]);
                    book.setTime(DateUtil.stringToDate(strings[4], "yyyy/MM/dd"));
                    book.setPrice(new BigDecimal(strings[5]));
                    list.add(book);
                }
                System.out.println("录入成功");
            } else {
                System.out.println("文件未找到或者文件不符合要求。请重新输入!");
            }
    
        }
    
        private static void selectAll() {
            Displayer.show(list);
        }
    
        private static void addOneBook() {
            book.clear();
            System.out.println("请输入你要插入书籍的图书编号:");
            book.setId(scanner.nextInt());
            scanner.nextLine();
            System.out.println("请输入你要插入书籍的图书名称:");
            String title = scanner.nextLine();
            book.setTitle(title);
            System.out.println("请输入你要插入书籍的作者姓名:");
            String name = scanner.nextLine();
            book.setName(name);
            System.out.println("请输入你要插入书籍的出版社(若是,请输入其出版社名称,若否,则输入“否”):");
            String publisher = scanner.nextLine();
            if (!publisher.equals("否")) {
                book.setPublisher(publisher);
            }
            System.out.println("请输入你要插入书籍的出版时间(若是,请输入其时间,请以“2020/10/06”的形式输入时间,若否,则输入“否”):");
            String time = scanner.nextLine();
            if (!time.equals("否")) {
                book.setTime(new Date(time));
            }
            System.out.println("请输入你要插入书籍的价格(若是,请输入其价格,若否,则输入“否”):");
            String price = scanner.nextLine();
            if (!price.equals("否")) {
                book.setPrice(new BigDecimal(price));
            }
            list.add(book);
            System.out.println("插入成功");
        }
    
        /**
         * 排序模块
         * 三级模块
         *
         * @param i
         * @return
         */
        private static int chooseOrder(int i) {
            if (i == 1) {
                System.out.println("\n\n排序:");
                System.out.println("1.按图书编号排序");
                System.out.println("2.按书名排序");
                System.out.println("3.按出版时间排序");
                System.out.println("4.按价格排序等");
                System.out.println("5.返回功能选择");
                System.out.println("\n\n请输入你需要的排序方式(序号):");
            }
    
            while (true) {
                switch (scanner.nextInt()) {
                    case 1:
                        selectOrderById();
                        return 0;
                    case 2:
                        selectOrderByTitle();
                        return 0;
                    case 3:
                        selectOrderByTime();
                        return 0;
                    case 4:
                        selectOrderByPrice();
                        return 0;
                    case 5:
                        return 1;
                    default:
                        System.out.println("抱歉,输入的是非法字符,请重新输入:");
                        return chooseFunction(0);
                }
            }
        }
    
        private static void selectOrderByPrice() {
            /**
             * 把 x -> System.out.println(x) 简化为 System.out::println 的过程称之为 eta-conversion
             * 把 System.out::println 简化为 x -> System.out.println(x) 的过程称之为 eta-expansion
             * 范式:
             * 类名::方法名
             * 方法调用
             *
             * person -> person.getAge();
             * 可以替换成
             * Person::getAge
             *
             * x -> System.out.println(x)
             * 可以替换成
             * System.out::println
             * out是一个PrintStream类的对象,println是该类的方法,依据x的类型来重载方法
             * 创建对象
             *
             * () -> new ArrayList<>();
             * 可以替换为
             * ArrayList::new
             */
            list.sort(Comparator.comparing(Book::getPrice));
            Displayer.show(list);
        }
    
        private static void selectOrderByTime() {
            list.sort(Comparator.comparing(Book::getTime));
            Displayer.show(list);
        }
    
        private static void selectOrderByTitle() {
            list.sort(Comparator.comparing(Book::getTitle));
            Displayer.show(list);
        }
    
        private static void selectOrderById() {
            list.sort(Comparator.comparing(Book::getId));
            Displayer.show(list);
        }
    
        /**
         * 查询模块
         * 三级模块
         *
         * @param i
         * @return
         */
        private static int chooseSelect(int i) {
            if (i == 1) {
                System.out.println("\n\n查询:");
                System.out.println("1.按书名查询");
                System.out.println("2.按作者名查询");
                System.out.println("3.按价格查询(小于)");
                System.out.println("4.按价格查询(等于)");
                System.out.println("5.按价格查询(大于)");
                System.out.println("6.返回模式选择");
                System.out.println("\n\n请输入你需要的查询方式(序号):");
            }
    
            while (true) {
                switch (scanner.nextInt()) {
                    case 1:
                        selectByTitle();
                        return 0;
                    case 2:
                        selectByName();
                        return 0;
                    case 3:
                        selectByPriceS();
                        return 0;
                    case 4:
                        selectByPrice();
                        return 0;
                    case 5:
                        selectByPriceB();
                        return 0;
                    case 6:
                        return 1;
                    default:
                        System.out.println("抱歉,输入的是非法字符,请重新输入:");
                        return chooseFunction(0);
                }
            }
        }
    
        private static void selectByPriceB() {
            System.out.println("请输入你要查询的价格:");
            List<Book> result = new LinkedList<>();
            for (Book book1 : list) {
                if (book1.getPrice().compareTo(new BigDecimal(scanner.nextInt())) == 1) {
                    result.add(book1);
                }
            }
            Displayer.show(result);
        }
    
        private static void selectByPrice() {
            System.out.println("请输入你要查询的价格:");
            List<Book> result = new LinkedList<>();
            for (Book book1 : list) {
                if (book1.getPrice().compareTo(new BigDecimal(scanner.nextInt())) == 0) {
                    result.add(book1);
                }
            }
            Displayer.show(result);
        }
    
        private static void selectByPriceS() {
            System.out.println("请输入你要查询的价格:");
            List<Book> result = new LinkedList<>();
            for (Book book1 : list) {
                if (book1.getPrice().compareTo(new BigDecimal(scanner.nextInt())) == -1) {
                    result.add(book1);
                }
            }
            Displayer.show(result);
        }
    
        private static void selectByName() {
            System.out.println("请输入你要查询的作者姓名:");
            //因为后面是nextLine,而之前是nextInt,会算上回车键,所以得先用nextLine吃掉回车
            scanner.nextLine();
            List<Book> result = new LinkedList<>();
            for (Book book1 : list) {
                if (book1.getName().equals(scanner.nextLine())) {
                    result.add(book1);
                }
            }
            Displayer.show(result);
        }
    
        private static void selectByTitle() {
            System.out.println("请输入你要查询的书籍名称:");
            scanner.nextLine();
            List<Book> result = new LinkedList<>();
            for (Book book1 : list) {
                if (book1.getTitle().equals(scanner.nextLine())) {
                    result.add(book1);
                }
            }
            Displayer.show(result);
        }
    }
    

    6.配置文件 {#配置文件 .list-paragraph}

    ①db.properties

    配置数据库的路径,用户密码等

    dbc.driver=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/library?serverTimezone=UTC
    jdbc.username=root
    jdbc.password=jinhaolin
    

    ②log4j.properties

    配置log4j

    log4j.rootCategory=DEBUG, CONSOLE,LOGFILE
    
    
    
    
    
    
    log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
    log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
    log4j.appender.CONSOLE.layout.ConversionPattern=-%p-%d{yyyy/MM/dd HH:mm:ss,SSS}-%l-%L-%m%n
    
    
    log4j.appender.LOGFILE=org.apache.log4j.FileAppender
    log4j.appender.LOGFILE.File=E:/axis.log
    log4j.appender.LOGFILE.Append=true
    log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
    log4j.appender.LOGFILE.layout.ConversionPattern=-%p-%d{yyyy/MM/dd HH:mm:ss,SSS}-%l-%L-%m%n
    

    ③mybatis-config.xml

    配置mybatis框架

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
    
        <properties resource="db.properties"/>
    
        <!--用于开启缓存,优化查询速度,但基本没什么用,效果很不明显。
        优化查询速度一般采取的措施:
        1.构建索引
        2.使用redio缓存数据库
        3.使用搜索引擎(一般用于电商项目)-->
    
    
        <typeAliases>
            <!--方式一:为类起别名-->
    <!--        <typeAlias type="com.dreamchaser.domain.User" alias="user"/>-->
            <!--方式二:使用package起别名,该包下的类别名是mybatis自动为我们取好的,就是类名(不区分大小写,但最好按照约定
            俗成的标准去写)-->
            <package name="com.dreamchaser.domain"/>
        </typeAliases>
        <environments default="development">
            <environment id="development">
                <transactionManager type="JDBC"/>
                <dataSource type="POOLED">
                    <property name="driver" value="${jdbc.driver}"/>
                    <property name="url" value="${jdbc.url}"/>
                    <property name="username" value="${jdbc.username}"/>
                    <property name="password" value="${jdbc.password}"/>
                </dataSource>
            </environment>
        </environments>
        <!--注册中心,指定mapper映射文件-->
        <mappers>
            <!--方式一:单独注册-->
    <!--        <mapper resource="com/dreamchaser/dao/UserDao.xml"/>-->
    <!--        <mapper resource="com.dreamchaser.mapper/BookMapper.xml"/>-->
            <!--方式二:使用接口的全路径注册-->
            <!--方式三:批量注册,该包下的所有mapper映射文件自动注册(通常采用此种方法注册)-->
            <mapper resource="com.dreamchaser/mapper/BookMapper.xml"/>
    <!--        <package name="com.dreamchaser.mapper"/>-->
        </mappers>
    </configuration>
    

    ④pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
    
      <groupId>org.example</groupId>
      <artifactId>library</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>war</packaging>
    
      <name>图书信息管理系统 Maven Webapp</name>
      <!-- FIXME change it to the project's website -->
      <url>http://www.example.com</url>
    
      <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.7</maven.compiler.source>
        <maven.compiler.target>1.7</maven.compiler.target>
      </properties>
    
      <dependencies>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.11</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis</artifactId>
          <version>3.5.4</version>
        </dependency>
        <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>8.0.18</version>
        </dependency>
      </dependencies>
    
      <build>
        <finalName>图书信息管理系统</finalName>
        <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
          <plugins>
            <plugin>
              <artifactId>maven-clean-plugin</artifactId>
              <version>3.1.0</version>
            </plugin>
            <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
            <plugin>
              <artifactId>maven-resources-plugin</artifactId>
              <version>3.0.2</version>
            </plugin>
            <plugin>
              <artifactId>maven-compiler-plugin</artifactId>
              <version>3.8.0</version>
            </plugin>
            <plugin>
              <artifactId>maven-surefire-plugin</artifactId>
              <version>2.22.1</version>
            </plugin>
            <plugin>
              <artifactId>maven-war-plugin</artifactId>
              <version>3.2.2</version>
            </plugin>
            <plugin>
              <artifactId>maven-install-plugin</artifactId>
              <version>2.5.2</version>
            </plugin>
            <plugin>
              <artifactId>maven-deploy-plugin</artifactId>
              <version>2.8.2</version>
            </plugin>
          </plugins>
        </pluginManagement>
          <plugins>
              <plugin>
                  <groupId>org.apache.maven.plugins</groupId>
                  <artifactId>maven-compiler-plugin</artifactId>
                  <configuration>
                      <source>8</source>
                      <target>8</target>
                  </configuration>
              </plugin>
          </plugins>
    
          <resources>
          <resource>
            <directory>src/main/java</directory>
            <includes>
              <include>**/*.xml</include>
              <include>**/*.properties</include>
            </includes>
          </resource>
          <resource>
            <directory>src/main/resources</directory>
            <includes>
              <include>**/*.xml</include>
              <include>**/*.properties</include>
            </includes>
          </resource>
        </resources>
      </build>
    </project>
    
    

    七、部分测试结果展示

    1.Sql模式

    在这里插入图片描述

    ①录入信息:

    在这里插入图片描述

    ②图书信息浏览

    在这里插入图片描述

    ③插入图书信息

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fjBQlm3d-1592834754202)(media/image26.png)]{width="5.766666666666667in" height="4.794444444444444in"}

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aH4ifBAq-1592834754203)(media/image27.png)]{width="5.768055555555556in" height="4.034027777777778in"}

    注:因为我数据库里的时钟设置的是UTC

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2gnHtt4j-1592834754204)(media/image28.png)]{width="5.558333333333334in" height="0.25in"}

    所以存储的日期比我输入的日期提早一天

    ④查询,以按价格查询(小于)为例

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MgOCow8g-1592834754206)(media/image30.png)]{width="4.525in" height="6.2in"}

    ⑤排序,以按价格排序为例

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BO1iCREb-1592834754206)(media/image31.png)]{width="5.766666666666667in" height="6.126388888888889in"}

    ⑥修改图书信息

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-isAdUUHz-1592834754208)(media/image32.png)]{width="5.768055555555556in" height="5.129861111111111in"}

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ELFMT4xw-1592834754208)(media/image33.png)]{width="5.768055555555556in" height="4.222916666666666in"}

    ⑦删除图书信息

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kcMsEHdY-1592834754209)(media/image34.png)]{width="4.25in" height="4.183333333333334in"}

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jga4kSbf-1592834754210)(media/image35.png)]{width="5.763194444444444in" height="4.084722222222222in"}

    ⑧导出为文件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-emBgVxoe-1592834754210)(media/image36.png)]{width="4.133333333333334in" height="3.941666666666667in"}

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MlGT7mHX-1592834754212)(media/image37.png)]{width="3.683333333333333in" height="4.016666666666667in"}

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4QIAIPtj-1592834754213)(media/image38.png)]{width="5.756944444444445in" height="3.3340277777777776in"}

    2.noSql模式

    NoSql模式与Sql模式区别只是底层实现原理不同,其测试效果是几乎一致的,在此,就只展现该模式的部分功能

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gyhdh4AS-1592834754213)(media/image39.png)]{width="3.675in" height="3.2583333333333333in"}

    ①插入图书信息

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bbcx53IR-1592834754214)(media/image40.png)]{width="5.7652777777777775in" height="4.627777777777778in"}

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vi6EX8P0-1592834754215)(media/image41.png)]{width="5.763194444444444in" height="3.5729166666666665in"}

    其余功能就不展示测试效果图了,其效果和Sql模式是一致的。

    八、调试过程中的主要问题、难点以及解决过程

    实话说,在调试的过程中我遇到了很多问题,也查阅了很多技术博客,这之中有些确实能解决问题,而有些则是查阅了很多博客,尝试了很多方法还是没能解决问题,或者说出现了另一个问题,对此,我也渐渐摸索出了一套查找解决bug的方法,收获还是蛮大的。

    以下列举了我调试中的几个主要问题和解决过程。

    1.如何组织各个模块?

    我一开始是采用递归循环的方式,但仔细一想,不对!经过我不断调试改进,最终用"套娃"的方式解决了问题,详细思路可以回到 第五部分、主要功能模块的算法流程图 去查看。

    2.如何让整个项目有条不紊,井然有序?

    整个项目累计代码总量超过千行,如果代码之间逻辑不清晰,关系复杂,那么这个项目调试,后期维护将变得举步维艰。

    那么如何做到有条不紊,井然有序呢?

    首先要做到项目结构清晰,可以像下面这样,实体类和实体类放在一起,工具类单独放在一起,资源配置文件放在一起,做到项目结构条理清晰。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eb1kZj8d-1592834754216)(media/image42.png)]{width="4.166666666666667in" height="5.916666666666667in"}

    其次,要做到代码封装抽象,具体做法就是把复用性高的代码抽离出来,封装成一个工具类,比如我要每个模块都有打印图书信息结果的需求,那么我们完全可以把它封装起来,比如这里我就把它封装成Displayer工具类,其内部有两个方法,如下图:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UfhixQRw-1592834754217)(media/image43.png)]{width="5.7659722222222225in" height="4.5055555555555555in"}

    可以看到Displayer类有两个方法,但方法名其实都是show,只是针对不同情况进行重载,让方法用起来更加方便。这里还有个小细节,就是方法都是采用静态的方式,因为这里并没有要初始化的数据,所以采用静态,这样可以让代码调用的时候并不需要实例化即可调用其内的方法,让工具类使用起来就更加方便。

    3.Maven项目,Mybatis框架,IDEA 的坑

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FFHKe28f-1592834754218)(media/image44.png)]{width="5.949305555555555in" height="3.3833333333333333in"}

    上图是我在调试遇到的一个问题,可以看到程序报了Mapped Statements collection does not contain value for xxx的异常,这很明显是mybatis框架报的异常,通过报错信息大概猜测是mybatis XXX容器内不包含我写的Mapper(因为那时候我还不知道Mapped Statement是什么东西),然后我就无脑将这段报错信息贴到百度上搜,确实有很多博客记录了此错误及解决方法,我截了一个下来,如图:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3jIQMgpS-1592834754218)(media/image45.png)]{width="5.761805555555555in" height="3.923611111111111in"}

    但实际上我按照博客上一个个去做,并没有解决问题,这时候我已经花了一个下午时间去查找,问题没解决,倒是把mybatis框架复习了一遍。

    苦思之下,我开始逐步调试,以下是我的思考过程:

    因为问题肯定出在mybatis框架上,所以我逐步调试,但是呢,我又不懂mybatis源码,看得云里雾里。不过我之前自学Java的时候,跟着视频写过一个类似mybatis的框架------SORM框架(不过功能肯定没mybatis框架复杂,是个小型版的框架),做完后学了其他知识后,自己又回头帮它迭代优化了一下,增加了新的功能,优化了结构。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xgD3bEPv-1592834754219)(media/image46.png)]{width="5.7652777777777775in" height="2.5256944444444445in"}

    这段经历让我能大概理解mybatis框架的一些行为,比如在这个地方我就注意到了mappedStatement对象size为0。这时我就猜测这应该是框架本身并没有读取到我写的sql语句,那是由什么造成的呢?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lhLJdZAA-1592834754220)(media/image47.png)]{width="7.3493055555555555in" height="4.134722222222222in"}

    这时候我就开始测试,不用接口类的方式(因为创建实体类也是mybatis框架底层做的),为了缩小问题的范围,我们采用原始的方式(但不是原生jdbc),发现还是这个错误,然后我开始怀疑mapper注册问题

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-935VOvTl-1592834754221)(media/image48.png)]{width="3.7333333333333334in" height="0.2916666666666667in"}

    这里我原本是采用包扫描的方式注册,然后我开始尝试用指定路径文件方式去注册

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xOySgCVz-1592834754221)(media/image49.png)]{width="5.716666666666667in" height="0.375in"}

    然后异常变了,说找不到这个文件

    好家伙,之前包扫描的时候报的是Mapped Statements collection does not contain value for xxx,现在直接报没找到这个文件!

    这时候我就开始思考为什么?

    为什么我用包扫描的方式就不报错呢?而用具体的文件路径就报错呢?

    真的是包扫描时找到了xml文件而具体文件路径没找到吗?

    不对,不是这样的,换个角度讲,包扫描没扫描到,会报错吗?不会,那问题区间缩小,很可能就是因为xml文件路径的问题。而其他配置文件是找到了的,不然它根本不会提示找不到(路径是写在mybatis-config.xml文件里的),既然我们确定了问题所在,这时候我们就需要尝试改变路径

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OzSr4kNB-1592834754222)(media/image50.png)]{width="5.759722222222222in" height="3.2402777777777776in"}

    这时候我再去查博客,搜索的不是异常信息,而是配置文件的路径该怎么写?

    在搜索的过程中我逐渐意识到我的项目结构可能与别人不同,所以我在搜索时加了Maven限定词,好家伙,不搜不知道,一搜我找到了原因所在。

    原来Maven项目编译时会把文件全都输出到Target文件夹下面

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c5SmxUUw-1592834754223)(media/image51.png)]{width="3.2083333333333335in" height="2.625in"}

    而默认情况下配置文件只会把resource文件夹下面的配置文件输出,这就造成Java文件夹下面的Mapper文件根本不会输出到target里,这样当然就找不到了,于是我修改了Maven项目中核心配置文件pom.xml信息,加入了下面的配置

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z36E0Xmk-1592834754224)(media/image52.png)]{width="4.45in" height="3.558333333333333in"}

    然后呢?

    还是找不到…

    本着不抛弃不放弃的精神,我开始关注target文件夹的文件结构

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nPRweG5A-1592834754225)(media/image53.png)]{width="4.216666666666667in" height="4.65in"}

    什么,居然有两个com.dreamchser,这是为什么呢?

    然后我开始测试加百度,然后发现了IDEA的神坑之处------当我们创建一个包时,com.dreamchser和com/dreamchaser是不同的!

    com.dreamchaser就是指第二个圈里的包,com/dreamchaser指的是第一个圈里的包

    .和/的差别真的是坑死我我了!

    我仔细思考了下,之前查询博客的时候,确实有博客提到idea中创建包时/和.是不一样,但当时我以为我的mapper是被读取进去了,所以没在意,只是检查了其他部分,知道后面调试运行底层源码时MappedStatement这个对象的size=0,通过字面意思猜测mybatis实际上是没有读取进去的,进而开始了这方面的排查,最终找到了原因。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p0USVokk-1592834754227)(media/image54.png)]{width="5.759722222222222in" height="3.2402777777777776in"}

    如果用使用动态代理改造CRUD的方式,用接口实现,这意味着接口路径要和xml中那么namespace中的值一致,而在mybatis配置中mapper注册的时候路径要写的是被打包进target/classes下的路径,注意.和/ 的区别

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N0EWl5HT-1592834754228)(media/image55.png)]{width="5.767361111111111in" height="1.9770833333333333in"}

    这次经历让我明白了该如何去解决问题。要解决问题的前提就是要知道问题的原因,需要定位问题,而不是报个错就盲目复制粘贴报错报错信息去搜博客,这确实可能会让你解决问题,但是有很大几率是你搜遍了网上的解决方式也没有解决问题,因为通常一个框架的同一个异常其实是有很多原因,你就会像无头苍蝇那样乱转,运气好可能会解决问题,运气不好就会到处碰壁。

    九.必做题和附加题

    1.必做题

    此部分另外已提交,就不在此赘述了

    2. 附加题

    ①题目要求

    要求写出算法思想和代码

    编写三个函数分别实现高精度加法、减法和乘法运算。在主函数中输入任意两个很大的正整数,可根据菜单提示,反复选择相应的操作进行计算。

    菜单:1、输入任意两个正整数

    2、高精度加法

    3、高精度减法

    4、高精度乘法

    0、退出

    ②算法思想

    我们知道正常的类型是无法存储这种大数值的,这里我们采用两个String来存储两个正整数,然后模拟我们平常计算加减乘除的过程来写代码,对每一位分别处理,最终得到我们想要的结果。

    ③代码

    package com.dreamchaser;
    
    import java.util.Scanner;
    
    public class Main {
        static Scanner scanner = new Scanner(System.in);
        static String s1 = "";
        static String s2 = "";
    
        public static void main(String[] args) {
            Scanner scanner = new Scanner(System.in);
            while (true) {
                if (choosePattern(1) == 1) {
                    break;
                }
            }
        }
    
        private static int choosePattern(int i) {
            if (i == 1) {
                System.out.println("\n\n1.输入任意两个正整数");
                System.out.println("2.高精度加法");
                System.out.println("3.高精度减法");
                System.out.println("4、高精度乘法");
                System.out.println("0、退出");
                System.out.println("\n\n请输入你的选择(序号):");
            }
            switch (scanner.nextInt()) {
                case 1:
                    //吃掉回车
                    scanner.nextLine();
                    print();
                    return 0;
                case 2:
                    add();
                    return 0;
                case 3:
                    delete();
                    return 0;
                case 4:
                    multiplication();
                    return 0;
                case 5:
                    return 1;
                default:
                    System.out.println("抱歉,输入的是非法字符,请重新输入:");
                    return choosePattern(0);
            }
        }
    
        private static void multiplication() {
            //表示进位
            int i, j, k;
            int[] c = new int[202];
            s1 = new StringBuilder(s1).reverse().toString();
            s2 = new StringBuilder(s2).reverse().toString();
            for (i = 0; i < s1.length(); i++) {
                for (j = 0; j < s2.length(); j++) {
                    c[i + j] += (s1.charAt(i) - 48) * (s2.charAt(j) - 48);
                }
            }
            for (k = 1; k <= s1.length() + s2.length(); k++) {
                c[k] += c[k - 1] / 10;
                c[k - 1] %= 10;
            }
    
            while (c[k] == 0 && k >= 1) {
                k--;
            }
            for (; k >= 0; k--) {
                System.out.print(c[k]);
            }
            System.out.println();
    
        }
    
        private static void delete() {
            //表示进位
            int i, j, r = 0, k = 0;
            boolean flag = true;
            int[] c = new int[101];
            for (i = s1.length() - 1, j = s2.length() - 1; j >= 0; i--, j--) {
                //两个位数相减再减去接的位数
                c[k++] = (s1.charAt(i) - s2.charAt(j) - r);
                //清零标记
                r = 0;
                if (c[k - 1] < 0) {
                    c[k - 1] += 10;
                    r = 1;
                }//如果是负数就借十,并标记
            }
            //剩下的继续减
            while (i >= 0) {
                //减去借的
                c[k++] = (s1.charAt(i) - '0' - r);
                //清零标记
                r = 0;
                //如果是负数就借十,并标记
                if (c[k - 1] < 0) {
                    c[k - 1] += 10;
                    r = 1;
                }
                i--;
            }
            //输出
            for (i = k - 1; i >= 0; i--) {
                //防止前导0输出的操作
                if (c[i] != 0 || flag) {
                    System.out.print(c[i]);
                    flag = true;
                }
            }
            //如果都没有输出,说明相减结果为0,应当输出0
            if (flag == false) {
                System.out.print(0);
            }
            System.out.println();
    
        }
    
        private static void add() {
            //表示进位
            int i, j, r = 0, k = 0;
            boolean flag = true;
            int[] c = new int[101];
            //从最低位相加,相加他们的公共部分,所以j>=0
            for (i = s1.length() - 1, j = s2.length() - 1; j >= 0; i--, j--) {
                //两个位数和进位的相加后取个位
                c[k++] = (r + s1.charAt(i) - '0' + s2.charAt(j) - '0') % 10;
                //记录进位
                r = (r + s1.charAt(i) - '0' + s2.charAt(j) - '0') / 10;
            }
            //再把剩下的继续加
            while (i >= 0) {
                //位数和进位的相加后取个位
                c[k++] = (r + s1.charAt(i) - '0') % 10;
                //记录进位
                r = (r + s1.charAt(i) - '0') / 10;
                i--;
            }
            //如果还有进位,进到最高位
            if (r != 0) {
                c[k++] = r;
            }
            //输出
            for (i = k - 1; i >= 0; i--) {
                //防止前导0输出的操作
                if (c[i] != 0 || flag) {
                    System.out.print(c[i]);
                    ;
                    flag = true;
                }
            }
            if (flag == false) {
                System.out.print(0);
            }
            System.out.println();
        }
    
        private static void print() {
            System.out.println("请输入第一个整数:");
            s1 = scanner.nextLine();
            System.out.println("请输入第二个整数:");
            s2 = scanner.nextLine();
            //把大的字符串放前面,方便操作
            if (s1.length() < s2.length() || (s1.length() == s2.length() && s1.compareTo(s2) > 0)) {
                String temp = "";
                temp = s1;
                s1 = s2;
                s2 = temp;
            }
        }
    
    }
    

    ④测试结果

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iKZlSIsN-1592834754228)(media/image56.png)]{width="2.533333333333333in" height="3.1416666666666666in"}

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gxU7w2uK-1592834754230)(media/image57.png)]{width="2.4833333333333334in" height="2.2583333333333333in"}

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-exS3HWy1-1592834754230)(media/image58.png)]{width="2.4916666666666667in" height="2.5416666666666665in"}

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wzznsxKt-1592834754230)(media/image59.png)]{width="3.591666666666667in" height="2.375in"}

    十、短学期实践的心得体会

    花了三天时间写代码,一天时间写实验报告,总计四天的努力,累计超过千行的代码(确切是1632行,没错,我真的算了!),虽然过程艰辛,但是结果令人满意。

    这个项目是我第一个独立完成的项目!说真的,很多东西你看似会了,但是真正到自己去做项目的时候,会发现很多问题,这个不会,那个不会,最后还是要靠百度解决,毕竟面向百度编程这句话不是可不是白讲的。

    在这个过程中其实我也学到了很多,尤其是关于mybatis框架和Maven的认识更加深入了。而且这个过程中我渐渐形成了一套属于自己代码风格和编程习惯,而且我对于如何去定位查找解决bug也有了更加清晰的认识。

    在这个过程中,我也认识到了自己的很多不足,未来我也要更加扎实的学习,更要尝试去多做项目,这样才能将知识技术化为内在,才能做到真正的融汇贯通,游刃有余!

    谨以此记,共勉!

    软件工程1902 金昊霖

    如果对此项目有什么疑惑或者建议,欢迎在评论区评论。

    ------------------------2020.7.28------------------------------------
    应评论要求,该项目已上传至码云
    推荐我的一些其他博客:
    适合初学Java的人自学的:用我的亲身经历来告诉你如何自学Java?
    适合刚学完SpringBoot的练手项目:【项目实战】个人博客(SpringBoot,SSM,thymeleaf,Semantic UI)——从设计思路到成品

    展开全文
  • 编写高质量可维护的代码既是程序员的基本修养,也是决定项目成败的关键因素,本文试图总结出问题项目普遍存在的共性问题并给出相应的解决方案。1. 程序员的宿命?程序员的职业生涯中难免遇到烂项...

    编写高质量可维护的代码既是程序员的基本修养,也是能决定项目成败的关键因素,本文试图总结出问题项目普遍存在的共性问题并给出相应的解决方案。


    1. 程序员的宿命?

    程序员的职业生涯中难免遇到烂项目,有些项目是你加入时已经烂了,有些是自己从头开始亲手做成了烂项目,有些是从里到外的烂,有些是表面光鲜等你深入进去发现是个“焦油坑”,有些是此时还没烂但是已经出现问题征兆走在了腐烂的路上。

    国内基本上是这样,国外情况我了解不多,不过从英文社区和技术媒体上老外同行的抱怨程度看,应该是差不多的,虽然整体素质可能更高,但是也因更久的信息化而积累了更多问题。毕竟“焦油坑、Shit_Mountain 屎山”这些舶来的术语不是无缘无故被发明出来的。

    Any way,这大概就是我们这个行业的宿命——要么改行,要么就是与烂项目烂代码长相伴。就像宇宙的“熵增加定律”一样:

    孤立系统的一切自发过程均向着令其状态更无序的方向发展,如果要使系统恢复到原先的有序状态是不可能的,除非外界对它做功。

    面对这宿命的阴影,有些人认命了麻木了,逐渐对这个行业失去热情。

    那些不认命的选择与之抗争,但是地上并没有路,当年软件危机的阴云也从未真正散去,人月神话仍然是神话,于是人们做出了各自不同的判断和尝试:

    • 掀桌子另起炉灶派:

      • 很多人把项目做烂的原因归咎于项目前期的基础没打好、需求不稳定一路打补丁、前面的架构师和程序员留下的烂摊子难以收拾。

      • 他们要么没有信心去收拾烂摊子,要么觉得这是费力不讨好,于是要放弃掉项目,寄希望于出现一个机会能重头再来。

      • 但是他们对于如何避免重蹈覆辙、做出另一个烂项目是没有把握也没有深入思考的,只是盲目乐观的认为自己比前任更高明。

    • 激进改革派:

      • 这个派别把原因归结于烂项目当初没有采用正确的编程语言、最新最强大的技术栈或工具。

      • 他们中一部分人也想着有机会另起炉灶,用上时下最流行最热门的技术栈(spring boot、springcloud、redis、nosql、docker、vue)。

      • 或者即便不另起炉灶,也认为现有技术栈太过时无法容忍了(其实可能并不算过时),不用微服务不用分布式就不能接受,于是激进的引入新技术栈,鲁莽的对项目做大手术。

      • 这种对刚刚流行还不成熟技术的盲目跟风、技术选型不慎重的情况非常普遍,今天在他们眼中落伍的技术栈,其实也不过是几年前另一批人赶的时髦。

      • 我不反对技术上的追新,但是同样的,这里的问题是:他们对于大手术的风险和副作用,对如何避免重蹈覆辙用新技术架构做出另一个烂项目,没有把握也没有深入思考的,只是盲目乐观的认为新技术能带来成功。

      • 也没人能阻止这种简历驱动的技术选型浮躁风气,毕竟花的是公司的资源,用新东西显得自己很有追求,失败了也不影响简历美化,简历上只会增加一段项目履历和几种精通技能,不会提到又做烂了一个项目,名利双收稳赚不赔。

    • 保守改良派:

      • 还有一类人他们不愿轻易放弃这个有问题但仍在创造效益的项目,因为他们看到了项目仍然有维护的价值,也看到了另起炉灶的难度(万事开头难,其实项目的冷启动存在很多外部制约因素)、大手术对业务造成影响的代价、系统迁移的难度和风险。

      • 同时他们尝试用温和渐进的方式逐步改善项目质量,采用一系列工程实践(主要包括重构热点代码、补自动化测试、补文档)来清理“技术债”,消除制约项目开发效率和交付质量的瓶颈。

    如果把一个问题项目比作病入膏肓的病人,那么这三种做法分别相当于是放弃治疗、截肢手术、保守治疗。


    2. 一个 35+ 程序员的反思

    年轻时候我也是掀桌子派和激进派的,新工程新框架大开大合,一路走来经验值技能树蹭蹭的涨,跳槽加薪好不快活。

    但是近几年随着年龄增长,一方面新东西学不动了,另一方面对经历过的项目反思的多了观念逐渐改变了。

    对我触动最大的一件事是那个我在 2016 年初开始从零搭建起的项目,在我 2018 年底离开的时候(仅从代码质量角度)已经让我很不满意了。只是,这一次没有任何借口了:

    • 从技术选型到架构设计到代码规范,都是我自己做的,团队不大,也是我自己组建和一手带出来的;

    • 最开始的半年进展非常顺利,用着我最趁手的技术和工具一路狂奔,年底前替换掉了之前采购的那个垃圾产品(对的,有个前任在业务上做参照也算是个很大的有利因素);

    • 做的过程我也算是全力以赴,用尽毕生所学——前面 13 年工作的经验值和走过的弯路、教训,使得公司只用其它同类公司同类项目 20% 的资源就把平台做起来了;

    • 如果说多快好省是最高境界,那么当时的我算是做到了多、快、省——交付的功能非常丰富且贴近业务需求、开发节奏快速、对公司开发资源很节省;

    • 但是现在看来,“好”就远远没有达到了,到了项目中期,简单优先级高的需求都已经做完了,公司业务上出现了新的挑战——接入另一个核心系统以及外部平台,真正的考验来了。

    • 那个改造工程影响面比较大,需要对我们的系统做大面积修改,最麻烦的是这意味着从一个简单的单体系统变成了一个分布式的系统,而且业务涉及资金交易,可靠性要求较高,是难上加难。

    • 于是问题开始出现了:我之前架构的优点——简单直接——这个时候不再是优点了,简单直接的架构在业务环境、技术环境都简单的情况下可以做到多快好省,但是当业务、技术环境都陡然复杂起来时,就不行了;

    • 具体的表现就是:架构和代码层面的结构都快速的变得复杂、混乱起来了——熵急剧增加;

    • 后面的事情就一发不可收拾:代码改起来越来越吃力、测试问题变多、生产环境故障和问题变多、于是消耗在排查测试问题生产问题和修复数据方面的精力急剧增加、出现恶性循环。

    • 到了这个境地,项目就算是做烂了!一个我从头开始做起的没有任何借口的失败!

    于是我意识到一个非常浅显的道理:拥有一张空白的画卷、一支最高级的画笔、一间专业的画室,无法保证你可以画出美丽的画卷。如果你不善于画画,那么一切都是空想和意淫。

    然后我变成了一个“保守改良派”,因为我意识到掀桌子和激进的改革都是不负责任的,说不好听的那样其实是掩耳盗铃、逃避困难,人不可能逃避一辈子,你总要面对。

    即便掀了桌子另起炉灶了,你还是需要找到一种办法把这个新的炉灶烧好,因为随着项目发展之前的老问题还是会一个一个冒出来,还是需要面对现实、不逃避、找办法。

    面对问题不仅有助于你把当前项目做好,也同样有助于将来有新的项目时更好的把握住机会。

    无论是职业生涯还是自然年龄,人到了这个阶段都开始喜欢回顾和总结,也变得比过去更在乎项目、产品乃至公司的商业成败。

    软件开发作为一种商业活动,判断其成败的依据应该是:能否以可接受的成本、可预期的时间节奏、稳定的质量水平、持续交付满足业务需要的功能市场需要的产品。

    其实就是项目管理四要素——成本、进度、范围、质量,传统项目管理理论认为这四要素彼此制约难以兼得,项目管理的艺术在于四要素的平衡取舍。

    关于软件工程和项目管理的理论和著作已经很多很成熟,这里我从程序员的视角提出一个新的观点——质量不可妥协

    • 质量要素不是一个可以被牺牲和妥协的要素——牺牲质量会导致其它三要素全都受损,反之同理,追求质量会让你在其它三个方面同时受益。

    • 在保持一个质量水平的前提下,成本、进度、范围三要素确确实实是互相制约关系——典型的比如牺牲成本(加班加点)来加快进度交付急需的功能。

    • 正如著名的“破窗效应”所启示的那样:任何一种不良现象的存在,都在传递着一种信息,这种信息会导致不良现象的无限扩展,同时必须高度警觉那些看起来是偶然的、个别的、轻微的“过错”,如果对这种行为不闻不问、熟视无睹、反应迟钝或纠正不力,就会纵容更多的人“去打烂更多的窗户玻璃”,就极有可能演变成“千里之堤,溃于蚁穴”的恶果——质量不佳的代码之于一个项目,正如一扇破了的窗之于一幢建筑、一个蚂蚁巢之于一座大堤。

    • 好消息是,只要把质量提上去项目就会逐渐走上健康的轨道,其它三个方面也都会改善。管好了质量,你就很大程度上把握住了项目成败的关键因素。

    • 坏消息是,项目的质量很容易失控,现实中质量不佳、越做越臃肿混乱的项目比比皆是,质量改善越做越好的案例闻所未闻,以至于人们将其视为如同物理学中“熵增加定律”一样的必然规律了。

    • 当然任何事情都有一个度的问题,当质量低于某个水平时才会导致其它三要素同时受损。反之当质量高到某个水平以后,继续追求质量不仅得不到明显收益,而且也会损害其它三要素——边际效用递减定律。

    • 这个度需要你为自己去评估和测量,如果目前的质量水平还在两者之间,那么就应该重点改进项目质量。当然,现实世界中很少看到哪个项目质量高到了不需要重视的程度。


    3. 项目走向衰败的最常见诱因——代码质量不佳

    一个项目的衰败一如一个人健康状况的恶化,当然可能有多种多样的原因——比如需求失控、业务调整、人员变动流失。

    但是作为我们技术人,如果能做好自己分内的工作——编写出可维护的代码、减少技术债利息成本、交付一个健壮灵活的应用架构,那也绝对是功德无量的。

    虽然很难估算出这究竟能挽救多少项目,但是在我十多年职业生涯中,经历的和近距离观察的几十个项目,确实看到了大量的项目正是由于代码质量不佳导致的失败和遗憾。

    同时我也发现其实失败项目的很多问题、症结也确确实实都可以归因到项目代码的混乱和质量低下,比如一个常见的项目腐烂恶性循环:代码乱》bug 多》排查问题耗时》复用度低》加班 996》士气低落……

    所谓“千里之堤,毁于蚁穴”,代码问题就是蚁穴。

    接下来,让我们从项目管理聚焦到项目代码质量这个相对小的领域来深入剖析。编写高质量可维护的代码是程序员的基本修养,本文试图在代码层面找到一些失败项目中普遍存在的症结问题,同时基于个人十几年开发经验总结出的一些设计模式作为药方分享出来。

    关于代码质量的话题其实很难通过一篇文章阐述明白,甚至需要一本书的篇幅,里面涉及到的很多概念关注点之间存在复杂微妙关系。

    推荐《设计模式之美》的第二章节《从哪些维度评判代码质量的好坏?如何具备写出高质量代码的能力?》,这是我看到的关于代码质量主题最精彩深刻的论述。


    4. 一个失败项目复盘

    先贴几张代码截图,看一下这个重病缠身的项目的病灶和症状:

    • 这是该项目中一个最核心、最复杂也是最经常要被改动的 class,代码行数 4881;

    • 结果就是冗长的 API 列表(列表需要滚动 4 屏才能到底,公有私有 API 180 个);

    • 还是那个 Class,头部的 import 延绵到了 139 行,去掉第一行 package 声明和少量空行总共 import 引入了 130 个 class!

    • 还是那个坑爹的组件,从 156 行开始到 235 行声明了 Spring 依赖注入的组件 40 个!

    这里先不去分析这个类的问题,只是初步展示一下病情严重程度。

    我相信这应该不算是特别糟糕的情况,比这个严重的项目俯拾皆是,但是这也应该足够拿来暴露问题、剖析成因了。


    4.1 症结 1:组件粒度过大、API 泛滥

    分层的理念早已深入人心,尤其是业务逻辑层的独立,彻底杜绝了之前(不分层的年代)业务逻辑与展现逻辑、持久化逻辑等混杂的问题。

    但是好景不长,随着业务的复杂和变更,在业务逻辑层的复杂性也急剧增加,成为了新的开发效率瓶颈, 问题就出在了业务逻辑组件的划分方式——按领域模型划分业务逻辑组件:

    • 业界关于如何设计业务逻辑层 并没有标准和最佳实践,绝大多数项目(我自己经历过的项目以及我有机会深入了解的项目)中大家都是想当然的按照业务领域对象来设计;

    • 例如:领域实体对象有 Account、Order、Delivery、Campaign。于是业务逻辑层就设计出 AccountService、OrderService、DeliveryService、CampaignService

    • 这种做法在项目简单是没什么问题,事实上项目简单时 你随便怎么设计都问题不大。

    • 但是当项目变大和复杂以后,就会出现问题了:

      • 组件臃肿:Service 组件的个数跟领域实体对象个数基本相当,必然造成个别 Service 组件变得非常臃肿——API 非常多,代码行数达到几千行;

      • 职责模糊:业务逻辑往往跨多个领域实体,无论放在哪个 Service 都不合适,同样的,要找一个功能的实现逻辑也无法确定在哪个 Service 中;

      • 代码重复 or 逻辑纠缠的两难选择:当遇到一个业务逻辑,其中的某个环节在另一个业务逻辑 API 中已经实现,这时如果不想忍受重复实现和代码,就只能去调用那个 API。

        但这样就造成了业务逻辑组件之间的耦合与依赖,这种耦合与依赖很快会扩散——新的 API 又会被其它业务逻辑依赖,最终形成蜘蛛网一样的复杂依赖甚至循环依赖;

      • 复用代码、减少重复虽然是好的,但是复杂耦合依赖的害处也很大——赶走一只狼引来了一只虎。两杯毒酒给你选!

    前面截图的那个问题组件 ContractService 就是一个典型案例,这样的组件往往是热点代码以及整个项目的开发效率的瓶颈。


    4.2 药方 1:倒金字塔结构——业务逻辑组件职责单一、禁止层内依赖

    问题根源的反面其实就藏着解决方案,只是需要我们有意识的去改变习惯、遵循新的设计风格,而不是凭直觉去设计:

    • 业务逻辑层应该被设计成一个个功能非常单一的小组件,所谓小是指 API 数量少、代码行数少;

    • 由于职责单一因此必然组件数量多,每一个组件对应一个很具体的业务功能点(或者几个相近的);

    • 复用(调用、依赖)只应该发生在相邻的两层之间——上层调用下层的 API 来实现对下层功能的复用;

    • 于是系统架构就自然呈现出倒立的金字塔形状:越接近顶层的业务场景组件数量越多,越往下层的复用性高,于是组件数量越少。


    4.3 症结 2:低内聚、高耦合

    经典面向对象理论告诉我们,好的代码结构应该是“高内聚、低耦合”的:

    • 高内聚:组件本身应该尽可能的包含其所实现功能的所有重要信息和细节,以便让维护者无需跳转到其它多个地方去了解必要的知识。

    • 低耦合:组件之间的互相依赖和了解尽可能少,以便在一个组件需要改动时其它组件不受影响。

    其实这两者就是一体两面,做到了高内聚基本也就做到了低耦合,相反如果内聚度很低,势必存在大量高耦合的组件。

    我观察发现,很多项目都存在低内聚、高耦合的问题。根本原因在于很多程序员,甚至是很多经验丰富的程序员也缺少这方面的意识——对“内聚性”概念不甚清楚,对内聚性被破坏的危害没有意识,对如何避免更是无从谈起

    很多人从一开始就凭直觉写程序,有了一定经验以后一般能认识到重复代码的危害,对复用性有很强的认识,于是就会掉进一个陷阱——盲目追求复用,结果破坏了内聚性。

    • 业界关于“复用性”的认识存在一个误区——认为包括业务逻辑组件在内的任何层面的组件都应该追求最大限度的可复用性

    • 复用当然是好的,但那应该有个前提条件:不增加系统复杂度的情况下的复用,才是好的。

    • 什么样的复用会增加系统复杂性、是不好的呢?前面提到的,一个业务逻辑 API 被另一个业务逻辑 API 复用——就是不好的:

      • 损害了稳定性:因为业务逻辑本身是跟现实世界的业务挂钩的,而业务会发生变化;当你复用一个会发生变化的 API,相当于在沙子上建高楼——地基是松动的;

      • 增加了复杂性:这样的依赖还造成代码可读性降低——在一个本就复杂的业务逻辑代码中,包含了对另一个复杂业务逻辑的调用,复杂度会急剧增加,而且会不断泛滥和传递;

      • 内聚性被破坏:由于业务逻辑被打散在了多个组件的方法内,变得支离破碎,无法在一个地方看清整体逻辑脉络和实现步骤——内聚性被破坏,同时也意味着,这个调用链条上涉及的所有组件之间存在高耦合。


    4.4 药方 2:复用的两种正确姿势——打造自己的 lib 和 framework

    软件架构中有两种东西来实现复用——lib 和 framework,

    • lib 库是供你(应用程序)调用的,它帮你实现特定的能力(比如日志、数据库驱动、json 序列化、日期计算、http 请求)。

    • framework 框架是供你扩展的,它本身就是半个应用程序,定义好了组件划分和交互机制,你需要按照其规则扩展出特定的实现并绑定集成到其中,来完成一个应用程序。

    • lib 就是组合方式的复用,framework 则是继承式的复用,继承的 Java 关键字是 extends,所以本质上是扩展。

    • 过去有个说法:“组合优于继承,能用组合解决的问题尽量不要继承”。我不同意这个说法,这容易误导初学者以为组合优于继承,其实继承才是面向对象最强大的地方,当然任何东西都不能乱用。

    • 典型的继承乱用就是为了获得父类的某个 API 而去继承,继承一定是为了扩展,而不是为了直接获得一个能力,获得能力应该调用 lib,父类不应该去实现具体功能,那是 lib 该做的事。

    • 也不应该为了使用 lib 而去继承 lib 中的 Class。lib 就是用来被组合被调用的,framework 就是用来被继承、扩展的。

    • 再展开一下:lib 既可以是第三方的(log4j、httpclient、fastjson),也可是你自己工程的(比如你的持久层 Dao、你的 utils);

    • framework 同理,既可以是第三方的(springmvc、jpa、springsecurity),也可以是你项目内封装的面向具体业务领域的(比如 report、excel 导出、paging 或任何可复用的算法、流程)。

    • 从这个意义上说,一个项目中的代码其实只有 3 种:自定义的 lib class、自定义的 framework 相关 class、扩展第三方或自定义 framework 的组件 class。

    • 再扩展一下:相对于过去,现在我们已经有了足够多的第三方 lib 和 framework 来复用,来帮助项目节省大量代码,开发工作似乎变成了索然无味、没技术含量的 CRUD。但是对于业务非常复杂的项目,则需要有经验、有抽象思维、懂设计模式的人,去设计面向业务的 framework 和面向业务的 lib,只有这样才能交付可维护、可扩展、可复用的软件架构——高质量架构,帮助项目或产品取得成功。


    4.5 症结 3:抽象不够、逻辑纠缠——High Level 业务逻辑和 Low Level 实现逻辑纠缠

    当我们说“代码中包含的业务逻辑”的时候,我们到底在说什么?业界并没有一个标准,大家经常讲的 CRUD 增删改查其实属于更底层的数据访问逻辑。

    我的观点是:所谓代码中的业务逻辑,是指这段代码所表现出的所有输入输出规则、算法和行为,通常可以分为以下 5 类:

    • 输入合法性校验;

    • 业务规则校验:典型的如检查交易记录状态、金额、时限、权限等,通常包含数据库或外部接口的查询作为参考;

    • 数据持久化行为:数据库、缓存、文件、日志等任何形式的数据写入行为;

    • 外部接口调用行为;

    • 输出/返回值准备。

    当然具体到某一个组件实例,可能不会包括上述全部 5 类业务逻辑,但是也可能每一类业务逻辑存在多个。

    单这样看你可能觉得并不是特别复杂,但是现实中上述 5 类业务逻辑中的每一个通常还包含着一到多个底层实现逻辑,如 CRUD 数据访问逻辑或第三方 API 的调用。

    例如输入合法性校验,通常需要查询对应记录是否存在,外部接口调用前通常需要查询相关记录以获得调用接口需要的参数,调用接口后还需要根据结果更新相关记录状态。

    显然这里存在两个 Level 的逻辑——High Level 的与业务需求对应且关联紧密的逻辑、Low Level 的实现逻辑。

    如果对两个 Level 的逻辑不加以区分、混为一谈,代码质量立刻就会遭到严重损害:

    • 可读性变差:两个维度的复杂性——业务复杂性和底层实现的技术复杂性——被掺杂在了一起,复杂度 1+1>2 剧增,给其他人阅读代码增加很大负担;

    • 可维护性差:可维护性通常指排查和解决问题所需花费的代价高低,当两个 level 的逻辑纠缠在一起,会使排查问题变的更困难,修复问题时也更容易出错;

    • 可扩展性无从谈起:扩展性通常指为系统增加一个特性所需花费的代价高低,代价越高扩展性越差;与排查修复问题类似,逻辑纠缠显然也会使添加新特性变得困难、一不小心就破坏了已有功能。

    下面这段代码就是一个典型案例——High Level 的逻辑流程(参数获取、反序列化、参数校验、缓存写入、数据库持久化、更新相关交易记录)完全淹没在了 Low Level 的实现逻辑(字符串比较、Json 反序列化、redis 操作、dao 操作以及前后各种琐碎的参数准备和返回值处理)。下一节我会针对这段问题代码给出重构方案。

    @Overridepublic void updateFromMQ(String compress) {    try {        JSONObject object = JSON.parseObject(compress);        if (StringUtils.isBlank(object.getString("type")) || StringUtils.isBlank(object.getString("mobile")) || StringUtils.isBlank(object.getString("data"))){            throw new AppException("MQ返回参数异常");        }        logger.info(object.getString("mobile")+"<<<<<<<<<获取来自MQ的授权数据>>>>>>>>>"+object.getString("type"));        Map map = new HashMap();        map.put("type",CrawlingTaskType.get(object.getInteger("type")));        map.put("mobile", object.getString("mobile"));        List<CrawlingTask> list = baseDAO.find("from crt c where c.phoneNumber=:mobile and c.taskType=:type", map);        redisClientTemplate.set(object.getString("mobile") + "_" + object.getString("type"),CompressUtil.compress( object.getString("data")));        redisClientTemplate.expire(object.getString("mobile") + "_" + object.getString("type"), 2*24*60*60);        //保存成功 存入redis 保存48小时        CrawlingTask crawlingTask = null;        // providType:(0:新颜,1XX支付宝,2:ZZ淘宝,3:TT淘宝)        if (CollectionUtils.isNotEmpty(list)){            crawlingTask = list.get(0);            crawlingTask.setJsonStr(object.getString("data"));        }else{            //新增            crawlingTask = new CrawlingTask(UUID.randomUUID().toString(), object.getString("data"),                    object.getString("mobile"), CrawlingTaskType.get(object.getInteger("type")));            crawlingTask.setNeedUpdate(true);        }        baseDAO.saveOrUpdate(crawlingTask);        //保存芝麻分到xyz        if ("3".equals(object.getString("type"))){            String data = object.getString("data");            Integer zmf = JSON.parseObject(data).getJSONObject("taobao_user_info").getInteger("zm_score");            Map param = new HashMap();            param.put("phoneNumber", object.getString("mobile"));            List<Dperson> list1 = personBaseDaoI.find("from xyz where phoneNumber=:phoneNumber", param);            if (list1 !=null){                for (Dperson dperson:list1){                    dperson.setZmScore(zmf);                    personBaseDaoI.saveOrUpdate(dperson);                    AppFlowUtil.updateAppUserInfo(dperson.getToken(),null,null,zmf);//查询多租户表  身份认证、淘宝认证 为0 置为1                }            }        }    } catch (Exception e) {        logger.error("更新my MQ授权信息失败", e);        throw new AppException(e.getMessage(),e);    }}
    


    4.6 药方 3:控制逻辑分离——业务模板 Pattern of NestedBusinessTemplate

    解决“逻辑纠缠”最关键是要找到一种隔离机制,把两个 Level 的逻辑分开——控制逻辑分离,分离的好处很多:

    • 根据经验,当我们着手维护一段代码时,一定是想先弄清楚它的整体流程、算法和行为,而不是一上来就去研究它的细枝末节;

    • 控制逻辑分离后,只需要去看 High Level 部分就能了解到上述内容,阅读代码的负担大幅度降低,代码可读性显著增强;

    • 读懂代码是后续一切维护、重构工作的前提,而且一份代码被读的次数远远高于被修改的次数(高一个数量级),因此代码对人的可读性再怎么强调都不为过,可读性增强可以大幅度提高系统可维护性,也是重构的最主要目标。

    • 同时,根据我的经验,High Level 业务逻辑的变更往往比 Low Level 实现逻辑变更要来的频繁,毕竟前者跟业务直接对应。当然不同类型项目情况不一样,另外它们发生变更的时间点往往也不同;

    • 在这样的背景下,控制逻辑分离的好处就更明显了:每次维护、扩充系统功能只需改动一个 Levle 的代码,另一个 Level 不受影响或影响很小,这会大幅降低修改成本和风险。

    我在总结过去多个项目中的教训和经验后,总结出了一项最佳实践或者说是设计模式——业务模板 Pattern of NestedBusinessTemplat,可以非常简单、有效的分离两类逻辑,先看代码:

    public class XyzService {
    abstract class AbsUpdateFromMQ {	public final void doProcess(String jsonStr) {		try {				JSONObject json = doParseAndValidate(jsonStr);				cache2Redis(json);				saveJsonStr2CrawingTask(json);				updateZmScore4Dperson(json);		} catch (Exception e) {				logger.error("更新my MQ授权信息失败", e);				throw new AppException(e.getMessage(), e);		}	}	protected abstract void updateZmScore4Dperson(JSONObject json);	protected abstract void saveJsonStr2CrawingTask(JSONObject json);	protected abstract void cache2Redis(JSONObject json);	protected abstract JSONObject doParseAndValidate(String json) throws AppException;}
    
    @SuppressWarnings({ "unchecked", "rawtypes" })public void processAuthResultDataCallback(String compress) {    new AbsUpdateFromMQ() {@Overrideprotected void updateZmScore4Dperson(JSONObject json) {                //保存芝麻分到xyz	            if ("3".equals(json.getString("type"))){	                String data = json.getString("data");	                Integer zmf = JSON.parseObject(data).getJSONObject("taobao_user_info").getInteger("zm_score");	                Map param = new HashMap();	                param.put("phoneNumber", json.getString("mobile"));	                List<Dperson> list1 = personBaseDaoI.find("from xyz where phoneNumber=:phoneNumber", param);	                if (list1 !=null){	                    for (Dperson dperson:list1){	                        dperson.setZmScore(zmf);	                        personBaseDaoI.saveOrUpdate(dperson);	                        AppFlowUtil.updateAppUserInfo(dperson.getToken(),null,null,zmf);	                    }	                }	            }}
    @Overrideprotected void saveJsonStr2CrawingTask(JSONObject json) {                   Map map = new HashMap();    	            map.put("type",CrawlingTaskType.get(json.getInteger("type")));    	            map.put("mobile", json.getString("mobile"));    	            List<CrawlingTask> list = baseDAO.find("from crt c where c.phoneNumber=:mobile and c.taskType=:type", map);    	            CrawlingTask crawlingTask = null;    	            // providType:(0:xx,1yy支付宝,2:zz淘宝,3:tt淘宝)    	            if (CollectionUtils.isNotEmpty(list)){    	                crawlingTask = list.get(0);    	                crawlingTask.setJsonStr(json.getString("data"));    	            }else{    	                //新增    	                crawlingTask = new CrawlingTask(UUID.randomUUID().toString(), json.getString("data"),    	                		json.getString("mobile"), CrawlingTaskType.get(json.getInteger("type")));    	                crawlingTask.setNeedUpdate(true);    	            }    	            baseDAO.saveOrUpdate(crawlingTask);}
    @Overrideprotected void cache2Redis(JSONObject json) {                   redisClientTemplate.set(json.getString("mobile") + "_" + json.getString("type"),CompressUtil.compress( json.getString("data")));    	            redisClientTemplate.expire(json.getString("mobile") + "_" + json.getString("type"), 2*24*60*60);}
    @Overrideprotected JSONObject doParseAndValidate(String json) throws AppException {                   JSONObject object = JSON.parseObject(json);    	            if (StringUtils.isBlank(object.getString("type")) || StringUtils.isBlank(object.getString("mobile")) || StringUtils.isBlank(object.getString("data"))){    	                throw new AppException("MQ返回参数异常");    	            }    	            logger.info(object.getString("mobile")+"<<<<<<<<<获取来自MQ的授权数据>>>>>>>>>"+object.getString("type"));                    return object;	}	}.doProcess(compress);}
    

    如果你熟悉经典的 GOF23 种设计模式,很容易发现上面的代码示例其实就是 Template Method 设计模式的运用,没什么新鲜的。

    没错,我这个方案没有提出和创造任何新东西,我只是在实践中偶然发现 Template Method 设计模式真的非常适合解决广泛存在的逻辑纠缠问题,而且也发现很少有程序员能主动运用这个设计模式;

    一部分原因可能是意识到“逻辑纠缠”问题的人本就不多,同时熟悉这个设计模式并能自如运用的人也不算多,两者的交集自然就是少得可怜;不管是什么原因,结果就是这个问题广泛存在成了通病。

    我看到一部分对代码质量有追求的程序员 他们的解决办法是通过"结构化编程"和“模块化编程”:

    • 把 Low Level 逻辑提取成 private function,被 High Level 代码所在的 function 直接调用;

      • 问题 1 硬连接不灵活:首先,这样虽然起到了一定的隔离效果,但是两个 level 之间是静态的硬关联,Low Level 无法被简单的替换,替换时还是需要修改和影响到 High Level 部分;

      • 问题 2 组件内可见性造成混乱:提取出来的 private function 在当前组件内是全局可见的——对其它无关的 High Level function 也是可见的,各个模块之间仍然存在逻辑纠缠。

        这在很多项目中的热点代码中很常见,问题也很突出:试想一个包含几十个 API 的组件,每个 API 的 function 存在一两个关联的 private function,那这个组件内部的混乱程度、维护难度是难以承受的。

    • 把 Low Level 逻辑抽取到新的组件中,供 High Level 代码所在的组件依赖和调用;更有经验的程序员可能会增加一层接口并且借助 Spring 依赖注入;

      • 问题 1 API 泛滥:提取出新的组件似乎避免了“结构化编程”的局限性,但是带来了新的问题——API 泛滥:因为组件之间调用只能走 public 方法,而这个 API 其实没有太多复用机会根本没必要做成 public 这种最高可见性。

      • 问题 2 同层组件依赖失控:组件和 API 泛滥后必然导致组件之间互相依赖成为常态,慢慢变得失控以后最终变成所有组件都依赖其它大部分组件,甚至出现循环依赖;比如那个拥有 130 个 import 和 40 个 Spring 依赖组件的 ContractService。

    下面介绍一下 Template Method 设计模式的运用,简单归纳就是:

    • High Level逻辑封装在抽象父类AbsUpdateFromMQ的一个final function中,形成一个业务逻辑的模板;

    • final function保证了其中逻辑不会被子类有意或无意的篡改破坏,因此其中封装的一定是业务逻辑中那些相对固定不变的东西。至于那些可变的部分以及暂时不确定的部分,以abstract protected function形式预留扩展点;

    • 子类(一个匿名内部类)像“做填空题”一样,填充模板实现Low Level逻辑——实现那些protected function扩展点;由于扩展点在父类中是abstract的,因此编译器会提醒子类的程序员该扩展什么。

    那么它是如何避免上面两个方案的 4 个局限性的:

    • Low Level 需要修改或替换时,只需从父类扩展出一个新的子类,父类全然不知无需任何改动;

    • 无论是父类还是子类,其中的 function 对外层的 XyzService 组件都是不可见的,即便是父类中的 public function 也不可见,因为只有持有类的实例对象才能访问到其中的 function;

    • 无论是父类还是子类,它们都是作为 XyzService 的内部类存在的,不会增加新的 java 类文件更不会增加大量无意义的 API(API 只有在被项目内复用或发布出去供外部使用才有意义,只有唯一的调用者的 API 是没有必要的);

    • 组件依赖失控的问题当然也就不存在了。

    SpringFramework 等框架型的开源项目中,其实早已大量使用 Template Method 设计模式,这本该给我们这些应用开发程序员带来启发和示范,但是很可惜业界没有注意到和充分发挥它的价值。

    NestedBusinessTemplat 模式就是对其充分和积极的应用,前面一节提到过的复用的两种正确姿势——打造自己的 lib 和 framework,其实 NestedBusinessTemplat 就是项目自身的 framework。


    4.7 症结 4:无处不在的 if else 牛皮癣

    无论你的编程启蒙语言是什么,最早学会的逻辑控制语句一定是 if else,但是不幸的是它在你开始真正的编程工作以后,会变成一个损害项目质量的坏习惯。

    几乎所有的项目都存在 if else 泛滥的问题,但是却没有引起足够重视警惕,甚至被很多程序员认为是正常现象。

    首先我来解释一下为什么 if else 这个看上去人畜无害的东西是有害的、是需要严格管控的

    • if else if ...else 以及类似的 switch 控制语句,本质上是一种 hard coding 硬编码行为,如果你同意“magic number 魔法数字”是一种错误的编程习惯,那么同理,if else 也是错误的 hard coding 编程风格;

    • hard coding 的问题在于当需求发生改变时,需要到处去修改,很容易遗漏和出错;

    • 以一段代码为例来具体分析:

    if ("3".equals(object.getString("type"))){          String data = object.getString("data");          Integer zmf = JSON.parseObject(data).getJSONObject("taobao_user_info").getInteger("zm_score");          Map param = new HashMap();          param.put("phoneNumber", object.getString("mobile"));          List<Dperson> list1 = personBaseDaoI.find("from xyz where phoneNumber=:phoneNumber", param);          if (list1 !=null){              for (Dperson dperson:list1){                  dperson.setZmScore(zmf);                  personBaseDaoI.saveOrUpdate(dperson);                  AppFlowUtil.updateAppUserInfo(dperson.getToken(),null,null,zmf);              }          }}
    
    • if ("3".equals(object.getString("type")))

      • 显然这里的"3"是一个 magic number,没人知道 3 是什么含义,只能推测;

      • 但是仅仅将“3”重构成常量 ABC_XYZ 并不会改善多少,因为 if (ABC_XYZ.equals(object.getString("type"))) 仍然是面向过程的编程风格,无法扩展;

      • 到处被引用的常量 ABC_XYZ 并没有比到处被 hard coding 的 magic number 好多少,只不过有了含义而已;

      • 把常量升级成 Enum 枚举类型呢,也没有好多少,当需要判断的类型增加了或判断的规则改变了,还是需要到处修改——Shotgun Surgery(霰弹式修改)

    • 并非所有的 if else 都有害,比如上面示例中的 if (list1 !=null) { 就是无害的,没有必要去消除,也没有消除它的可行性。判断是否有害的依据:

      • 如果 if 判断的变量状态只有两种可能性(比如 boolean、比如 null 判断)时,是无伤大雅的;

      • 反之,如果 if 判断的变量存在多种状态,而且将来可能会增加新的状态,那么这就是个问题;

      • switch 判断语句无疑是有害的,因为使用 switch 的地方往往存在很多种状态。

    4.8 药方 4:充血枚举类型——Rich Enum Type

    正如前面分析呈现的那样,对于代码中广泛存在的状态、类型 if 条件判断,仅仅把被比较的值重构成常量或 enum 枚举类型并没有太大改善——使用者仍然直接依赖具体的枚举值或常量,而不是依赖一个抽象。

    于是解决方案就自然浮出水面了:在 enum 枚举类型基础上进一步抽象封装,得到一个所谓的“充血”的枚举类型,代码说话:

    • 实现多种系统通知机制,传统做法:

    enum NOTIFY_TYPE {    email,sms,wechat;  }  //先定义一个enum——一个只定义了值不包含任何行为的“贫血”的枚举类型
    if(type==NOTIFY_TYPE.email){ //if判断类型 调用不同通知机制的实现     。。。}else if (type=NOTIFY_TYPE.sms){    。。。}else{    。。。}
    
    • 实现多种系统通知方式,充血枚举类型——Rich Enum Type 模式:

    enum NOTIFY_TYPE {    //1、定义一个包含通知实现机制的“充血”的枚举类型  email("邮件",NotifyMechanismInterface.byEmail()),  sms("短信",NotifyMechanismInterface.bySms()),  wechat("微信",NotifyMechanismInterface.byWechat());  
      String memo;  NotifyMechanismInterface notifyMechanism;
      private NOTIFY_TYPE(String memo,NotifyMechanismInterface notifyMechanism){//2、私有构造函数,用于初始化枚举值      this.memo=memo;      this.notifyMechanism=notifyMechanism;  }  //getters ...}
    public interface  NotifyMechanismInterface{ //3、定义通知机制的接口或抽象父类    public boolean doNotify(String msg);
        public static NotifyMechanismInterface byEmail(){//3.1 返回一个定义了邮件通知机制的策的实现——一个匿名内部类实例        return new NotifyMechanismInterface(){            public boolean doNotify(String msg){                .......            }        };    }    public static NotifyMechanismInterface bySms(){//3.2 定义短信通知机制的实现策略        return new NotifyMechanismInterface(){            public boolean doNotify(String msg){                .......            }        };    }     public static NotifyMechanismInterface byWechat(){//3.3 定义微信通知机制的实现策略        return new NotifyMechanismInterface(){            public boolean doNotify(String msg){                .......            }        };    }}
    //4、使用场景NOTIFY_TYPE.valueof(type).getNotifyMechanism().doNotify(msg);
    
    • 充血枚举类型——Rich Enum Type 模式的优势:

      • 不难发现,这其实就是 enum 枚举类型和 Strategy Pattern 策略模式的巧妙结合运用;

      • 当需要增加新的通知方式时,只需在枚举类 NOTIFY_TYPE 增加一个值,同时在策略接口 NotifyMechanismInterface 中增加一个 by 方法返回对应的策略实现;

      • 当需要修改某个通知机制的实现细节,只需修改 NotifyMechanismInterface 中对应的策略实现;

      • 无论新增还是修改通知机制,调用方完全不受影响,仍然是 NOTIFY_TYPE.valueof(type).getNotifyMechanism().doNotify(msg);

    • 与传统 Strategy Pattern 策略模式的比较优势:常见的策略模式也能消灭 if else 判断,但是实现起来比较麻烦,需要开发更多的 class 和代码量:

      • 每个策略实现需单独定义成一个 class;

      • 还需要一个 Context 类来做初始化——用 Map 把类型与对应的策略实现做映射;

      • 使用时从 Context 获取具体的策略;

    • Rich Enum Type 的进一步的充血:

      • 上面的例子中的枚举类型包含了行为,因此已经算作充血模型了,但是还可以为其进一步充血;

      • 例如有些场景下,只是要对枚举值做个简单的计算获得某种 flag 标记,那就没必要把计算逻辑抽象成 NotifyMechanismInterface 那样的接口,杀鸡用了牛刀;

      • 这时就可以在枚举类型中增加 static function 封装简单的计算逻辑;

    • 策略实现的进一步抽象:

      • 当各个策略实现(byEmail bySms byWechat)存在共性部分、重复逻辑时,可以将其抽取成一个抽象父类;

      • 然后就像前一章节——业务模板 Pattern of NestedBusinessTemplate 那样,在各个子类之间实现优雅的逻辑分离和复用。


    5. 重构前的火力侦察:

    为你的项目编制一套代码库目录/索引——CODEX

    以上就是我总结出的最常见也最影响代码质量的 4 个问题及其解决方案:

    • 职责单一、小颗粒度、高内聚、低耦合的业务逻辑层组件——倒金字塔结构;

    • 打造项目自身的 lib 层和 framework——正确的复用姿势;

    • 业务模板 Pattern of NestedBusinessTemplate——控制逻辑分离;

    • 充血的枚举类型 Rich Enum Type——消灭硬编码风格的 if else 条件判断;

    接下来就是如何动手去针对这 4 个方面进行重构了,但是事情还没有那么简单。

    上面所有的内容虽然来自实践经验,但是要应用到你的具体项目,还需要一个步骤——火力侦察——弄清楚你要重构的那个模块的逻辑脉络、算法以致实现细节,否则贸然动手,很容易遗漏关键细节造成风险,重构的效率更难以保证,陷入进退两难的尴尬境地。

    我 2019 年一整年经历了 3 个代码十分混乱的项目,最大的收获就是摸索出了一个梳理烂代码的最佳实践——CODEX:

    • 在阅读代码过程中,在关键位置添加结构化的注释,形如://CODEX ProjectA 1 体检预约流程 1 预约服务 API 入口

    • 所谓结构化注释,就是在注释内容中通过规范命名的编号前缀、分隔符等来体现出其所对应的项目、模块、流程步骤等信息,类似文本编辑中的标题 1、2、3;

    • 然后设置 IDE 工具识别这种特殊的注释,以便结构化的显示。Eclipse 的 Tasks 显示效果类似下图;

    • 这个结构化视图,本质上相对于是代码库的索引、目录,不同于 javadoc 文档,CODEX 具有更清晰的逻辑层次和更强的代码查找便利性,在 Eclipse Tasks 中点击就能跳转到对应的代码行;

    • 这些结构化注释随着代码一起提交后就实现了团队共享;

    • 这样的一份精确无误、共享的、活的源代码索引,无疑会对整个团队的开发维护工作产生巨大助力;

    • 进一步的,如果在 CODEX 中添加 Markdown 关键字,甚至可以将导出的 CODEX 简单加工后,变成一张业务逻辑的 Sequence 序列图,如下所示。

    6. 总结陈词——不要辜负这个程序员最好的时代

    毫无疑问这是程序员最好的时代,互联网浪潮已经席卷了世界每个角落,各行各业正在越来越多的依赖 IT。

    过去只有软件公司、互联网公司和银行业会雇佣程序员,随着云计算的普及、产业互联网和互联网+兴起,已经有越来越多的传统企业开始雇佣程序员搭建 IT 系统来支撑业务运营。

    资本的推动 IT 需求的旺盛,使得程序员成了稀缺人才,各大招聘平台上,程序员的岗位数量和薪资水平长期名列前茅。

    但是我们这个群体的整体表现怎么样呢,扪心自问,我觉得很难令人满意,我所经历过的以及近距离观察到的项目,鲜有能够称得上成功的。

    这里的成功不是商业上的成功,仅限于作为一个软件项目和工程是否能够以可接受的成本和质量长期稳定的交付。

    商业的短期成功与否,很多时候与项目工程的成功与否没有必然联系,一个商业上很成功的项目可能在工程上做的并不好,只是通过巨量的资金资源投入换来的暂时成功而已。

    归根结底,我们程序员群体需要为自己的声誉负责,长期来看也终究会为自己的声誉获益或受损。

    我认为程序员最大的声誉、最重要的职业素养,就是通过写出高质量的代码做好一个个项目、产品,来帮助团队、帮助公司、帮助组织创造价值、增加成功的机会。

    希望本文分享的经验和方法能够对此有所帮助!

    本文是我的一位技术总监好友:权哥花了半个月时间写出来的良心文章,强烈推荐给大家,文章很长很硬很有价值,大家可以收藏多看几遍。希望大家看完之后转发、点在看,好文章要让更多的人看到。

    有道无术,术可成;有术无道,止于术

    欢迎大家关注Java之道公众号

    好文章,我在看❤️

    展开全文
  • 一个项目开发和发布由公司中不同部门的人负责,当开发项目的人用最新的Xcode写代码、而负责打包的同事因为特殊原因不能升级Xcode的时候,开发的同事就必须迁就发布的同事了,此时将会用到以Swfit版本号为依据的条件...
  • (当然也可以设置两个站点,一个m.xxxx.com, 一个www.xxxx.com 前端依据浏览器是mobile还是pc来跳转) 在APP.vue中 methods:{ // 添加判断方法 isMobile() { let flag = navigator.userAgen
  • 程序员的职业生涯中难免遇到烂项目,有些项目是你加入时已经烂了,有些是自己从头开始亲手成了烂项目,有些是从里到外的烂,有些是表面光鲜等你深入进去发现是“焦油坑”,有些是此时还没烂但是已经出现问题征兆...
  • 如何判断一个文件是否被关闭?

    千次阅读 2012-06-16 21:59:23
    做项目的时候遇到了下面这个问题:如何判断一个打开的txt文件是否被关闭? 在打开一个txt文件的时候,notepad程序是自动通过文件路径的参数首先复制文件,然后马上就关闭了文件通道,这个时候打开的其实只是notepad...
  • 内存优化说起来,技术的都知道,减少拷贝次数,去掉不必要的内存空间申请,但是我们在项目的时候,难免要数据储存,当然数据库是最大的内存使用表现;这里讲数据库相关内容,主要讲讲流媒体线程中的内存优化;...
  • 、影响软件开发项目进度的因素  要有效地进行进度控制,必须对影响进度的因素进行分析,事先或及时采取必要的措施,尽量缩小计划进度与实际进度的偏差,实现对项目的主动控制。软件开发项目中影响进度的因素很多...
  • 这些经历很多不是一个人的经历,这些总结很多也不是出自一个人之手,如同我们觉得一段代码写的很好,必定会收藏整理成为自己的一部分加以完善共享,接着不断的有人完善共享下去...所以每一个有相同观感的人都可以...
  • 一个项目的整个测试流程

    千次阅读 2020-03-17 10:26:54
    最近一直在进行接口自动化的测试工作,同时对于一个项目的整个测试流程进行了梳理,希望对你有用~~~ 需求分析: 整体流程图: 需求提取 -> 需求分析 -> 需求评审 -> 更新后的测试需求跟踪xmind ...
  • HR教你如何判断一个公司的好坏?

    万次阅读 2017-12-21 16:41:00
    昨天晚上,接到了位朋友打来的电话,问我到底应该怎样去判断一家公司的好坏,因为这位朋友已经在工作两年了,也可以说是位职场老鸟了,所以我很好奇他为什么会这么问。他说前几天他从原来的公司辞职了去了一家房...
  • 第三版信息系统项目管理师47过程的输入输出及工具
  • JS判断一个空间点在陆地还是海洋

    千次阅读 2019-05-22 14:32:52
    文章结构问题概述思路方法实现过程插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必可少的KaTeX数学...
  • 判断一个点是否在某个区域内(多边形)背景:比如滴滴会根据乘客所在的不同区域,给出不同的价格。市区堵一点,那么价格也高点。获取服务范围只规定在某个范围内原理:求解从该点向右发出的水平线射线与多边形各边的...
  • Maven项目依据条件进行打包war包

    千次阅读 2015-03-28 15:11:39
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-inst
  • 一个SSM完整项目开发心得

    千次阅读 2019-07-04 20:56:00
    这学期就一个完整的SSM项目。(如果考研失败就去外包一波CRUD boy) 所以边考研边利用业余时间了一学期,接着期末一周的Web课设时间终于把这个项目1.0搞定了。 下面就来谈一下开发完我开发完这个项目的...
  • 最近在论坛上看到一位朋友问到这个...当面试官问到你一个开放性的问题,一般是希望你能够积极主动地和他讨论问题(这样的面试官以我的经验判断,一定是个好面试官),而不是给出一箩筐标准答案,在我们知道上下文.
  • IT项目经理应该什么

    万次阅读 2012-09-28 15:02:48
    经常看到这样的项目经理,一副整天忙得团团转的样子,电话不停地作响,一个小时之内要发出几十个指令,好像他所领导的团队离开了他就一天也活下去。然后他还会说:"我很忙"或"我很累","我需要增加人手"。这样的...
  • 优化if判断

    2019-08-07 10:58:29
    在某个场景下我们的函数中有判断语句,这个判断依据在整个项目运行期间一般不会变化,所以判断分支在整个项目运行期间只会运行某个特定分支,那么就可以考虑惰性载入函数。那么第次运行之后就会覆写这方法,下...
  • 众所周知,尽管企业性质、规模不同,业务和产品种类也是千差万别,但项目决策的过程在性质上大致相同,企业与科研机构的不同之处在于前者非常强调效益,所以项目的设立与市场需求挂钩已是种共识,即根据技术趋势...
  • 项目管理复习题

    万次阅读 多人点赞 2020-09-18 11:54:44
    蓝字位注释,红字为错误原因,紫字为重点 ...提取码:j4jz 第一章 一、填空题 1.敏捷模型包括(4)个核心价值...2、项目是为了创造一个唯一的产品或提供一个唯一的服务而进行的永久性的努力。(×) 3、过程管理就是.
  • react开发:从零开始搭建一个react项目

    万次阅读 多人点赞 2017-06-29 17:48:40
    从头开始建立一个React App - 项目基本配置 npm init 生成 package.json 文件.安装各种需要的依赖: npm install --save react - 安装React.npm install --save react-dom 安装React Dom,这个包是用来...
  •  要做好一个项目经理,是很有点难的,他首先必须要是技术和管理的化身,其次要具备较好的形象和极佳的口才,同时拥有一定的人格魅力,另外他还要具备一定的设计头脑和审美观,还有很多,不再赘述....在大多数boss的...
  • 听声辨位,一个让我感到毛骨悚然的 GitHub 项目

    千次阅读 多人点赞 2020-12-31 15:59:22
    前不久,一个叫做 Keytap 的 “黑科技” 在国外火了。Keytap 通过监听你敲击键盘的声音,就还原出你输入的内容。 而且,只需要通过你电脑里的麦克风,就完成声波采集的任务。 在一段发布于网上的..
  • 项目报警机制

    千次阅读 2012-07-06 16:51:19
    如何判断一个项目正在向这个方向发展(但尚未陷入灾难之中)呢?如何在拯救措施的成本变高之前判断出需要对软件项目采取一定的措施?这些问题的答案在于EWS系统的报警机制。 出于讨论的目的,我们将问题划分为项目...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 71,799
精华内容 28,719
关键字:

判断一个项目能不能做的依据