INSERT INTO `tag_category` (`t_id`, `t_name`, `is_category`, `is_delete`)
VALUES (1, '随笔', 1, 0),
       (2, '后端', 1, 0),
       (3, 'linux', 1, 0),
       (4, '二三', 1, 0),
       (5, '前端', 1, 0),
       (6, '电影推荐', 1, 0),
       (7, '博客', 0, 0),
       (8, 'sublime', 0, 0),
       (9, 'linux', 0, 0),
       (10, 'shiro', 0, 0),
       (11, 'vim', 0, 0),
       (12, 'SpringMvc', 0, 0),
       (13, 'spring', 0, 0),
       (14, 'javaMail', 0, 0),
       (15, '端口', 0, 0),
       (16, '。', 0, 0),
       (17, 'angular', 0, 0),
       (18, 'css样式', 0, 0),
       (19, 'attribute', 0, 0),
       (20, 'property', 0, 0),
       (21, '随笔', 0, 0),
       (22, '电影', 0, 0),
       (23, 'nio', 0, 0),
       (24, 'netty', 0, 0),
       (25, '系统变量', 0, 0),
       (26, '脚本', 0, 0),
       (27, '网课', 0, 0);


-- 密码123456789
INSERT INTO `user` (`u_id`, `u_email`, `u_pwd`, `u_email_status`, `u_avatar`, `u_desc`, `u_recently_landed_time`,
                    `u_display_name`, `u_role`, `status`)
VALUES (1, 'a@celess.cn', '25f9e794323b453885f5181f1b624d0b', 1, 'a@celess.cn_2.png', '禾几海不需要自我介绍',
        '2020-07-02 11:30:38', '禾几海', 'admin', 0),
       (2, 'b@celess.cn', '25f9e794323b453885f5181f1b624d0b', 1, NULL, NULL, '2020-05-16 13:21:35', NULL, 'user',
        0),
       (3, 'zh56462271@qq.com', '25f9e794323b453885f5181f1b624d0b', 1, NULL, NULL, '2020-05-16 13:21:35', NULL, 'user',
        0);


INSERT INTO `article` (`a_id`, `a_title`, `a_summary`, `a_md_content`, `a_url`, `a_author_id`, `a_is_original`,
                       `a_reading_number`, `a_like`, `a_dislike`, `a_category_id`, `a_publish_date`, `a_update_date`,
                       `a_is_open`, `is_delete`)
VALUES (3, '关于这个博客我有话要说',
        '很高兴 这个博客能和大家见面。\n很早之前就有接触网站的开发，也一直有这方面的兴趣，后来我用wordpress也搭建了自己的第一个博客，也一直使用了有段时间，期间也用过别的小众博客系统，但是总感觉少了份乐趣，于是就有了你现在看到的这个博客\n初衷\n\n想拥有一个网站\n对已有的博客系统不太满意\n想养成写博客的习惯\n\n\n\n后端\n高三毕业的暑假我系统的学习了java，了解到java主要擅长于web开发，我兴奋了，于是我恶补前端的知识，但也只能写写简单的网页，后来我又了解到Spring和S......',
        '# 很高兴 这个博客能和大家见面。\n\n很早之前就有接触网站的开发，也一直有这方面的兴趣，后来我用wordpress也搭建了自己的第一个博客，也一直使用了有段时间，期间也用过别的小众博客系统，但是总感觉少了份乐趣，于是就有了你现在看到的这个博客\n\n## 初衷\n- 想拥有一个网站\n- 对已有的博客系统不太满意\n- 想养成写博客的习惯\n\n## 后端\n高三毕业的暑假我系统的学习了java，了解到java主要擅长于web开发，我兴奋了，于是我恶补前端的知识，但也只能写写简单的网页，后来我又了解到Spring和Springboot，听闻Springboot的极简化配置，于是我果断入了Springboot，期间一遍一遍学习Springboot和各个框架一边编写我的java博客。最终用了将近半个月，完成了java后端接口的编写。后来因为代码的不规范选择重构了一次，最终成型。\n\n## 前端\n前端的开发是整个开发过程中最为繁琐复杂的一部分，从最开始的东拼西凑到后来使用amaze UI的开发，期间踩过不少坑，也曾气馁骂娘过，但最终还是决定继续写下去。\n前端部分目前采用的angular开发的\n\n## 前后端的交互\nangular用起来还是比较爽的，但是怎么和后端交互倒是个问题，由于之前的页面由Springboot来管理，不需要考虑这些问题，但是由于Angular开发的web程序是一个单页面应用，直接嵌入Springboot会无法正常使用，于是我将前后端进行了分离，后端Springboot程序打包成jar包，用java -jar部署，然后由nginx进行代理，前端部分部署到Nginx站点。\n\n## 移动端\n光写一个页面不去适配移动端是肯定不太好的，于是我也进行了简单的适配，很多页面也不是太美观，以后也会慢慢修改。慢慢来，嘿嘿嘿\n\n\n## 写在最后\n这个博客还是花费了我不少的时间，但是从写这个博客我也学习到了不少的知识。总的来说还算是可以，以后也还会慢慢更新，不断的完善。代码以后也会发布到github，感兴趣也可以关注一下。\n\n##### 项目地址\n后续的代码提交将提交这两个仓库[blog-backEnd](https://www.github.com/xiaohai2271/blog-backEnd "blog-backEnd")和[blog-frontEnd](https://www.github.com/xiaohai2271/blog-frontEnd "blog-frontEnd")中\n暂停更新的仓库 [https://github.com/xiaohai2271/blog](https://github.com/xiaohai2271/blog)\n',
        '', 1, 1, 618, 0, 0, 1, '2019-04-16 15:18:47', '2020-06-16 00:01:09', 1, 0),
       (5, 'Linux下sublime无法输入中文',
        '    测试系统：Deepin 15.9.1 （理论上所有 Linux 发行版都通用）\n    输入法：Fcitx 4.2.9.6\n    Sublime Text 版本：Sublime Text 3.1.1（Build 3176）\n\n\n1.保存下述代码为 sublime-imfix.c 文件\n\n\n\n/*\nsublime-imfix.c\nUse LD_PRELOAD to interpose some function to fix sublime input method ......',
        '	测试系统：Deepin 15.9.1 （理论上所有 Linux 发行版都通用）\n	输入法：Fcitx 4.2.9.6\n	Sublime Text 版本：Sublime Text 3.1.1（Build 3176）\n\n> 1.保存下述代码为 sublime-imfix.c 文件\n\n```\n/*\nsublime-imfix.c\nUse LD_PRELOAD to interpose some function to fix sublime input method support for linux.\nBy Cjacker Huang\n\ngcc -shared -o libsublime-imfix.so sublime-imfix.c `pkg-config --libs --cflags gtk+-2.0` -fPIC\nLD_PRELOAD=./libsublime-imfix.so subl\n*/\n#include \n#include \ntypedef GdkSegment GdkRegionBox;\n\nstruct _GdkRegion\n{\n  long size;\n  long numRects;\n  GdkRegionBox *rects;\n  GdkRegionBox extents;\n};\n\nGtkIMContext *local_context;\n\nvoid\ngdk_region_get_clipbox (const GdkRegion *region,\n            GdkRectangle    *rectangle)\n{\n  g_return_if_fail (region != NULL);\n  g_return_if_fail (rectangle != NULL);\n\n  rectangle->x = region->extents.x1;\n  rectangle->y = region->extents.y1;\n  rectangle->width = region->extents.x2 - region->extents.x1;\n  rectangle->height = region->extents.y2 - region->extents.y1;\n  GdkRectangle rect;\n  rect.x = rectangle->x;\n  rect.y = rectangle->y;\n  rect.width = 0;\n  rect.height = rectangle->height;\n  //The caret width is 2;\n  //Maybe sometimes we will make a mistake, but for most of the time, it should be the caret.\n  if(rectangle->width == 2 && GTK_IS_IM_CONTEXT(local_context)) {\n        gtk_im_context_set_cursor_location(local_context, rectangle);\n  }\n}\n\n//this is needed, for example, if you input something in file dialog and return back the edit area\n//context will lost, so here we set it again.\n\nstatic GdkFilterReturn event_filter (GdkXEvent *xevent, GdkEvent *event, gpointer im_context)\n{\n    XEvent *xev = (XEvent *)xevent;\n    if(xev->type == KeyRelease && GTK_IS_IM_CONTEXT(im_context)) {\n       GdkWindow * win = g_object_get_data(G_OBJECT(im_context),"window");\n       if(GDK_IS_WINDOW(win))\n         gtk_im_context_set_client_window(im_context, win);\n    }\n    return GDK_FILTER_CONTINUE;\n}\n\nvoid gtk_im_context_set_client_window (GtkIMContext *context,\n          GdkWindow    *window)\n{\n  GtkIMContextClass *klass;\n  g_return_if_fail (GTK_IS_IM_CONTEXT (context));\n  klass = GTK_IM_CONTEXT_GET_CLASS (context);\n  if (klass->set_client_window)\n    klass->set_client_window (context, window);\n\n  if(!GDK_IS_WINDOW (window))\n    return;\n  g_object_set_data(G_OBJECT(context),"window",window);\n  int width = gdk_window_get_width(window);\n  int height = gdk_window_get_height(window);\n  if(width != 0 && height !=0) {\n    gtk_im_context_focus_in(context);\n    local_context = context;\n  }\n  gdk_window_add_filter (window, event_filter, context);\n}\n\n```\n\n\n> 2.安装 C/C++ 的编译环境和 gtk libgtk2.0-dev\n\n` sudo apt-get install build-essential `\n` sudo apt-get install libgtk2.0-dev `\n\n\n> 3.编译共享内库\n\n`` gcc -shared -o libsublime-imfix.so sublime-imfix.c `pkg-config --libs --cflags gtk+-2.0` -fPIC ``\n\n\n> 4.设置 LD_PRELOAD 并启动 Sublime Text\n\n` D_PRELOAD=./libsublime-imfix.so subl`\n\n> 5.设置快捷方式启动时自动加载库\n\n修改sublime.desktop中的Exeu为\n` Exec=env LD_PRELOAD=/opt/sublime_text_3/libsublime-imfix.so /opt/sublime_text_3/sublime_text\n`\n文章转载自 [https://www.sinosky.org/linux-sublime-text-fcitx.html](''https://www.sinosky.org/linux-sublime-text-fcitx.html'')\n\n',
        'https://www.sinosky.org/linux-sublime-text-fcitx.html', 1, 0, 341, 0, 0, 3, '2019-04-16 15:21:34', NULL, 1, 0),
       (8, 'shiro前后端分离中的跳转问题',
        '\n1.问题描述\n\n\n\n因为想实现前后端分离，并且使用shiro进行权限管理但是碰到一个问题就是shiro的重定向问题\n    1.未登录，shiro会自动重定向到 /login\n    2.访问路径无权限，shiro会抛出401 http错误\n\n\n2.解决\n\n\n\n因为我纯粹只想用springBoot写后端api所以就必须kill掉这些问题通过查资料发现在  org.apache.shiro.web.filter.authz.AuthorizationFilter下有onAcc......',
        '> 1.问题描述\n\n因为想实现前后端分离，并且使用shiro进行权限管理 \n但是碰到一个问题就是shiro的重定向问题\n\n	1.未登录，shiro会自动重定向到 /login\n	2.访问路径无权限，shiro会抛出401 http错误\n\n> 2.解决\n\n因为我纯粹只想用springBoot写后端api所以就必须kill掉这些问题\n通过查资料发现在 ` org.apache.shiro.web.filter.authz.AuthorizationFilter`下有\nonAccessDenied方法，源码：\n```\nprotected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {\n        Subject subject = this.getSubject(request, response);\n        if (subject.getPrincipal() == null) {\n            this.saveRequestAndRedirectToLogin(request, response);\n        } else {\n            String unauthorizedUrl = this.getUnauthorizedUrl();\n            if (StringUtils.hasText(unauthorizedUrl)) {\n                WebUtils.issueRedirect(request, response, unauthorizedUrl);\n            } else {\n                WebUtils.toHttp(response).sendError(401);\n            }\n        }\n\n        return false;\n    }\n```\n重写此方法即可解决重定向的问题\n随即\n``` \npublic class RestAuthorizationFilter extends PermissionsAuthorizationFilter {\n\n    @Override\n    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {\n        response.setContentType("application/Json");\n        response.setCharacterEncoding("UTF-8");\n        Subject subject = this.getSubject(request, response);\n        if (subject.getPrincipal() == null) {\n            response.getWriter().print(ResponseUtil.response(CodeAndMsgEnum.DIDNOTLOGIN, null));\n        } else {\n            String unauthorizedUrl = this.getUnauthorizedUrl();\n            if (StringUtils.hasText(unauthorizedUrl)) {\n                WebUtils.issueRedirect(request, response, unauthorizedUrl);\n            } else {\n                response.getWriter().print(ResponseUtil.response(CodeAndMsgEnum.PERMISSION_FAILD, null));\n            }\n        }\n\n        return false;\n    }\n}\n\n```\n当无权限/需要登录的的时候返回一个json字符串即可\n> 3.最终效果：(note:需要将对象转json，图中是object.toString效果)\n\n### 未登录：\n!["没有登录"](http://56462271.oss-cn-beijing.aliyuncs.com/web/blogimg/didnotlogin.png)\n\n### 无权限：\n!["无权限"](http://56462271.oss-cn-beijing.aliyuncs.com/web/blogimg/permission.png)\n\n',
        '', 1, 1, 245, 0, 0, 2, '2019-04-16 15:23:30', NULL, 1, 0),
       (10, 'Linux下Vim的“假死”',
        '随手一个Ctrl+S 是个好习惯\n然而....\n然而这个习惯却在我使用Vim的时候坑了我\n由于不是经常发生，也就没在意。每次很麻烦的关掉terminal的窗口，重新再打开terminal。今天发生了好几次，很是郁闷。就想看看究竟是怎么回事，结果发现每次按下Ctrl+S就会出现这个问题。\n后来百度，Google才发现 这货是一个快捷键 对应的是 锁定屏幕\n要解除锁定 只需 Ctrl + Q\n我估计这可能是Vim中我记得最牢的一个快捷键之一吧\n',
        '随手一个Ctrl+S 是个好习惯\n\n然而....\n\n然而这个习惯却在我使用Vim的时候坑了我\n\n由于不是经常发生，也就没在意。每次很麻烦的关掉terminal的窗口，重新再打开terminal。今天发生了好几次，很是郁闷。就想看看究竟是怎么回事，结果发现每次按下Ctrl+S就会出现这个问题。\n\n后来百度，Google才发现 这货是一个快捷键 对应的是 <strong>锁定屏幕</strong>\n\n要解除锁定 只需 <strong>Ctrl + Q</strong>\n\n我估计这可能是Vim中我记得最牢的一个快捷键之一吧',
        '', 1, 1, 221, 0, 0, 3, '2019-04-16 15:24:13', NULL, 1, 1),
       (12, '自己动手撸一个Spring MVC控制器',
        '自己动手撸一个 MVC 控制器\n\n     用来加深对java反射的理解的练手项目 也是加深对spring MVC 理解的项目\n    \n\n\n\nMappping 对象\n\n\n\nimport java.lang.reflect.Method;\n\n/**\n * 用于存储每个url映射\n *\n * @author : xiaohai\n * @date : 2019/03/17 20:58\n */\npublic class Mapping {\n    private String[] ......',
        '## 自己动手撸一个 MVC 控制器\n\n	 用来加深对java反射的理解的练手项目 也是加深对spring MVC 理解的项目\n	\n\n\n> Mappping 对象\n\n``` \nimport java.lang.reflect.Method;\n\n/**\n * 用于存储每个url映射\n *\n * @author : xiaohai\n * @date : 2019/03/17 20:58\n */\npublic class Mapping {\n    private String[] path;\n\n    private String requestMethod;// 请求方式\n\n    private Class<?> aclass;\n\n    private Method method;//注解对应的methods\n\n    //setter and getter\n}\n```\n\n<br/>\n\n> RequestMapping注解\n\n```\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.METHOD,ElementType.TYPE})\npublic @interface RequestMapping {\n    String[] value() default "";\n\n    RequestMethod method() default RequestMethod.GET;\n}\n```\n\n<br/>\n> Controller注解\n\n```\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.TYPE)\npublic @interface Controller {\n    String value() default "";\n}\n```\n\n\n\n<br/>\n> 核心servlet\n\n``` \n\nimport cn.celess.boot.Application;\nimport cn.celess.boot.annotation.Controller;\nimport cn.celess.boot.annotation.RequestMapping;\nimport cn.celess.boot.entity.Mapping;\nimport cn.celess.boot.util.*;\nimport cn.celess.logtool.Log;\n\nimport javax.servlet.*;\nimport javax.servlet.http.HttpServlet;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * @author : xiaohai\n * @date : 2019/03/17 10:16\n * 核心控制类\n */\npublic class DispatcherServlet extends HttpServlet {\n    private static final Log log = new Log(DispatcherServlet.class);\n    private static final List<Mapping> mappingList = new ArrayList<Mapping>();\n\n    public DispatcherServlet() {\n        initRequestMapingMap(Application.classpath);\n    }\n\n    /**\n     * 解析 获取请求的参数 即RequestMapping的value值和method\n     * 对path进行整理 保存为/xx/xx/的格式\n     *\n     * @param request\n     * @return\n     */\n    private String[] pareUrl(HttpServletRequest request) {\n        String[] pAndM = new String[2];\n        String path = request.getContextPath() + "/";\n        String requestUri = request.getRequestURI().replaceFirst(path, "");\n        if (requestUri.length() > 0) {\n            if (requestUri.substring(requestUri.length() - 1) != "/") {\n                requestUri += "/";\n            }\n        } else {\n            requestUri += "/";\n        }\n        pAndM[0] = "/" + requestUri;\n        pAndM[1] = request.getMethod();\n\n        return pAndM;\n    }\n\n    @Override\n    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n        log.info("执行了service方法");\n        this.execute(req, resp);\n    }\n\n    /**\n     * 匹配path和RequestMethod  并执行path匹配的方法\n     *\n     * @param request\n     * @param response\n     * @throws ServletException\n     * @throws IOException\n     */\n    public void execute(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {\n\n        String[] s = pareUrl(request);\n        String urlPath = s[0];\n        String requestMethod = s[1];\n\n        Class<?> clazz = null;\n        Method method = null;\n        for (Mapping ml : mappingList) {\n            String[] path = ml.getPath();\n            for (String str : path) {\n                if (urlPath.equals(str)) {\n                    if (requestMethod.toUpperCase().equals(ml.getRequestMethod())) {\n                        method = ml.getMethod();\n                        clazz = ml.getAclass();\n                        break;\n                    }\n                }\n            }\n            if (method != null) break;\n        }\n\n        if (method == null) {\n            response.sendError(404);//没找到映射\n            return;\n        }\n\n        Object obj = null;//方法返回值\n\n        if (method != null) {\n            Object retObject = null;\n            try {\n                //创建类的实例\n                obj = clazz.newInstance();\n                //利用反射执行这个方法\n                retObject = method.invoke(obj, request, response);\n            } catch (IllegalAccessException e) {\n                e.printStackTrace();\n            } catch (InvocationTargetException e) {\n                e.printStackTrace();\n            } catch (InstantiationException e) {\n                e.printStackTrace();\n            }\n        }\n    }\n\n\n    /***\n     * 将controller  解析然后存储起来\n     * @param packageName\n     */\n    private void initRequestMapingMap(String packageName) {\n        List<Class> classList = ReflectUtil.getClasssFromPackage(packageName);\n        for (Class c : classList) {\n            if (c.isAnnotationPresent(Controller.class)) {\n                Method[] methods = c.getDeclaredMethods();\n                for (Method m : methods) {\n                    if (m.isAnnotationPresent(RequestMapping.class)) {\n                        String[] path = m.getAnnotation(RequestMapping.class).value();\n                        // 对path 进行 规范化\n                        for (int i = 0; i < path.length; i++) {\n                            if (!"/".equals(path[i].substring(0, 1))) {\n                                path[i] = "/" + path[i];\n                            }\n                            if (!"/".equals(path[i].substring(path[i].length() - 1))) {\n                                path[i] += "/";\n                            }\n\n                        }\n                        String requestMethod = m.getAnnotation(RequestMapping.class).method().getMethod();\n                        log.info("path : " + Arrays.toString(path) + " requestMethod:" + requestMethod);\n                        Mapping mapping = new Mapping();\n                        mapping.setPath(path);\n                        mapping.setRequestMethod(requestMethod);\n                        mapping.setAclass(c);\n                        mapping.setMethod(m);\n\n                        mappingList.add(mapping);\n                    }\n                }\n            }\n        }\n        log.info("方法映射成功");\n    }\n}\n```\n\n\n',
        '', 1, 1, 314, 0, 0, 2, '2019-04-16 15:25:05', NULL, 1, 0),
       (47, 'javaMail本地测试正常 部署后连接超时',
        '\n错误信息：\n\n\n\n         send mail err:Mail server connection failed; nested exception is com.sun.mail.util.MailConnectException: Couldn’t connect to host, port: smtp.qq.com, 25; timeout -1\n\n部署环境：\n\n\n\n阿里云ecs Ubuntu 16.04.6 LTS\n\n原因\n\n\n\n阿里云限制了25端口\n\n解......',
        '> 错误信息：\n\n``         send mail err:Mail server connection failed; nested exception is com.sun.mail.util.MailConnectException: Couldn’t connect to host, port: smtp.qq.com, 25; timeout -1\n``\n\n> 部署环境：\n\n阿里云ecs Ubuntu 16.04.6 LTS\n> 原因\n\n阿里云限制了25端口\n\n> 解决方案\n\n## 修改端口号:\n\n``` \n###############  email  ##############\nspring.mail.host=smtp.163.com\nspring.mail.username=xxx\nspring.mail.password=[REDACTED] 新增如下\nspring.mail.port=465\nspring.mail.properties.mail.smtp.socketFactory.port=465\nspring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory\nspring.mail.properties.mail.smtp.socketFactory.fallback=false\n```\n\n\n\n> 参考自：\n[https://blog.csdn.net/yc_Low_profile/article/details/80761031](https://blog.csdn.net/yc_Low_profile/article/details/80761031)\n[https://blog.csdn.net/gzy_0922/article/details/79853155](https://blog.csdn.net/gzy_0922/article/details/79853155)',
        '', 1, 1, 346, 0, 0, 2, '2019-04-22 09:13:19', NULL, 1, 0),
       (993, '记一起奇怪的 HttpRequestMethodNotSupportedException ',
        '最近在修改后台接口的时候遇到了一个问题\r\n\r\n\r\n\r\n在我访问大部分接口的时候后台会抛出错误\r\n org.springframework.web.HttpRequestMethodNotSupportedException: Request method ''XXX'' not supported \r\n于是在 org.springframework.web.servlet 下的 doDispatch方法打断点发现请求连接是 /login 这个很好的暴露了为何报 HttpRequestMet......',
        '> 最近在修改后台接口的时候遇到了一个问题\n\n在我访问大部分接口的时候后台会抛出错误\n\n`` org.springframework.web.HttpRequestMethodNotSupportedException: Request method ''XXX'' not supported ``\n\n于是在` org.springframework.web.servlet` 下的` doDispatch`方法打断点发现请求连接是` /login` 这个很好的暴露了为何报` HttpRequestMethodNotSupportedException `这个错误，\n但是问题又来了，我已在` RestAuthorizationFilter`中处理了权限请求，当需要登录时默认返回json字符串，由布尔类型的jsonRes 标识\n\n以下为源代码\n``` \n    @Override\n    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {\n        //是否需要返回值为json格式的数据\n        Boolean jsonRes = Boolean.parseBoolean(request.getParameter("json"));\n        if (jsonRes == null) {\n            jsonRes = true;\n        }\n        response.setContentType("application/Json");\n        response.setCharacterEncoding("utf-8");\n        Subject subject = this.getSubject(request, response);\n        if (subject.getPrincipal() == null) {\n            if (!jsonRes) {\n                this.saveRequestAndRedirectToLogin(request, response);\n            } else {\n                response.getWriter().println(ResponseUtil.response(ResponseEnum.HAVE_NOT_LOG_IN, null).toString());\n            }\n        } else {\n            if (jsonRes) {\n                String unauthorizedUrl = this.getUnauthorizedUrl();\n                if (StringUtils.hasText(unauthorizedUrl)) {\n                } else {\n                    response.getWriter().print(ResponseUtil.response(ResponseEnum.PERMISSION_ERROR, null).toString());\n                }\n            } else {\n                String unauthorizedUrl = this.getUnauthorizedUrl();\n                if (StringUtils.hasText(unauthorizedUrl)) {\n                    WebUtils.issueRedirect(request, response, unauthorizedUrl);\n                } else {\n                    WebUtils.toHttp(response).sendError(401);\n                }\n            }\n        }\n        return false;\n    }\n```\n问题就出在了这个布尔变量上面\n\n断点调试发现该值无法通过if语句改变，即``\n        if (jsonRes == null) {\n            jsonRes = true;\n        }\n``\n不生效后查看` Boolean.parseBoolean()`源代码发现如下\n``` \n    public static boolean parseBoolean(String s) {\n        return ((s != null) && s.equalsIgnoreCase("true"));\n    }\n```\n即已经处理了` null`的情况\n\n一个由 `  HttpRequestMethodNotSupportedException `引发的诡异事件就此结束',
        '', 1, 1, 361, 0, 0, 2, '2019-05-08 05:32:02', NULL, 1, 0),
       (1018, '《小王子》',
        '爱是一场驯养，狐狸说：“也就是说，对于我，你和其他成千上万的小男孩一样，没有不同。我不需要你；而你，也不需要我。于你而言，我和其他成千上万的狐狸也并无二致。但如果你驯养了我，我们便彼此需要。在这个世上，你就是我的唯一，我亦是你的唯一。” 我会住在其中的一颗星星上面.在某一颗星星上微笑着.每当夜晚你仰望星空的时候.就会像是看到所有的星星都在微笑一般。2019.5.8\n',
        '爱是一场驯养，狐狸说：“也就是说，对于我，你和其他成千上万的小男孩一样，没有不同。我不需要你；而你，也不需要我。于你而言，我和其他成千上万的狐狸也并无二致。但如果你驯养了我，我们便彼此需要。在这个世上，你就是我的唯一，我亦是你的唯一。” 我会住在其中的一颗星星上面.在某一颗星星上微笑着.每当夜晚你仰望星空的时候.就会像是看到所有的星星都在微笑一般。\n2019.5.8\n',
        '', 2, 1, 343, 0, 0, 4, '2019-05-08 11:26:39', NULL, 1, 1),
       (1286, 'angular中innerHTML的样式的问题',
        'angular中innerHTML的样式问题\n\nangular会对css文件的样式进行处理\n例如下图就是被处理过的样式\n使用innerHTML插入dom时标签都是原生标签，是无法匹配到css样式的，而且dom上的style样式也会被清除，所以innerHTML内容是没有样式的。但撒并不是意味着我们就无法给他设置样式，具体操作如下\n\n在要插入的组件的ts的注解上面添加 encapsulation: ViewEncapsulation.None,如下：\n\n\n\n@Component......',
        '# angular中innerHTML的样式问题\n\n### angular会对css文件的样式进行处理\n例如下图就是被处理过的样式\n![css样式](http://cdn.celess.cn/img_1558454083327.jpg "css样式")\n<hr>\n\n使用innerHTML插入dom时标签都是原生标签，是无法匹配到css样式的，而且dom上的style样式也会被清除，所以innerHTML内容是没有样式的。\n但撒并不是意味着我们就无法给他设置样式，具体操作如下\n\n1. 在要插入的组件的ts的注解上面添加` encapsulation: ViewEncapsulation.None,`如下：\n\n```\n@Component({\n  selector: ''app-article'',\n  templateUrl: ''./article.component.html'',\n  encapsulation: ViewEncapsulation.None,\n  styleUrls: [''./article.component.css'']\n})\n```\n2. 插入html 设置样式即可\n查看样式\n![css样式](http://cdn.celess.cn/img_1558454764739.jpg "css样式")\n\n### 新的问题\n虽然插入dom的样式解决了，但是新的问题也随之而来了，由于修改后的css内容不由angular进行特殊处理了，改css样式瞬间变成了全局样式，其他页面的样式也会因为你访问这个页面后而改变样式。所以这是一个不太合理的解决方案。特此记录。',
        '', 1, 1, 306, 0, 0, 5, '2019-05-21 16:11:51', NULL, 1, 0),
       (1287, 'HTML attribute和DOM property',
        'Attribute和Property在英文中均为属性的意思,他们有密切的联系，也有不同的差别。\n\nAttribute 是由 HTML 定义的。property 是由 DOM (Document Object Model) 定义的。\n\n\n\n少量 HTML attribute 和 property 之间有着 1:1 的映射，如 id。\n有些 HTML attribute 没有对应的 property，如 colspan。\n有些 DOM property 没有对应的 attribu......',
        'Attribute和Property在英文中均为属性的意思,他们有密切的联系，也有不同的差别。\n\n> Attribute 是由 HTML 定义的。property 是由 DOM (Document Object Model) 定义的。\n- 少量 HTML attribute 和 property 之间有着 1:1 的映射，如 id。\n- 有些 HTML attribute 没有对应的 property，如 colspan。\n- 有些 DOM property 没有对应的 attribute，如 textContent。\n- 大量 HTML attribute 看起来映射到了 property…… 但却不像你想的那样！\n\n> 最后一类尤其让人困惑…… 除非你能理解这个普遍原则：\nattribute 初始化 DOM property，然后它们的任务就完成了。property 的值可以改变；attribute 的值不能改变。\n例如，当浏览器渲染 `<input type="text" value="Bob">` 时，它将创建相应 DOM 节点， 它的 value 这个 property 被初始化为 “Bob”。\n当用户在输入框中输入 “Sally” 时，DOM 元素的 value 这个 property 变成了 “Sally”。 但是该 HTML 的 value 这个 attribute 保持不变。如果你读取 input 元素的 attribute，就会发现确实没变： `input.getAttribute(''value'') // 返回 "Bob"`。\nHTML 的 value 这个 attribute 指定了初始值；DOM 的 value 这个 property 是当前值。\ndisabled 这个 attribute 是另一种特例。按钮的 disabled 这个 property 是 false，因为默认情况下按钮是可用的。 当你添加 disabled 这个 attribute 时，只要它出现了按钮的 disabled 这个 property 就初始化为 true，于是按钮就被禁用了。\n添加或删除 disabled 这个 attribute 会禁用或启用这个按钮。但 attribute 的值无关紧要，这就是你为什么没法通过 `<button disabled="false">仍被禁用</button>` 这种写法来启用按钮。\n设置按钮的 disabled 这个 property（如，通过 Angular 绑定）可以禁用或启用这个按钮。 这就是 property 的价值。\n就算名字相同，HTML attribute 和 DOM property 也不是同一样东西。\n\n摘取自angular中文官网 [原文链接](https://www.angular.cn/guide/template-syntax#html-attribute-vs-dom-property )\n\n看完有点懵 有木有。\n** so，上重点**\n\n**Attribute特性由Html定义，所有出现在HTML标签内的描述节点都是attribute特性。**\n\n**Property属性属于Dom对象，DOM实质就是javascript中的对象。我们可以跟在js中操作普通对象一样获取、设置DOM对象的属性，并且property属性可以是任意类型。**\n\n\n',
        NULL, 1, 1, 416, 0, 0, 5, '2019-07-14 07:11:23', NULL, 1, 1),
       (1288, '💔', '我们生活在最好的年纪，也生活在最糟的年纪。\n', '*我们生活在最好的年纪，也生活在最糟的年纪。*', NULL, 1, 1, 359, 0, 0, 1,
        '2019-09-17 10:27:12', NULL, 1, 0),
       (1289, '电影推荐 《幸福终点站》',
        '幸福终点站\n\n剧情简介\n\n　　为了完成父亲的心愿，维克多（汤姆•汉克斯 Tom Hanks 饰）从故国乘坐飞机前往美国肯尼迪机场，但戏剧性的事情发生了：他被告知祖国发生政变，而他的身份证护照一一失效，同时他的签证也无法再使用。进退两难的维克多只有在机场滞留，等待新证件的办理。　　但是，他在机场等待了整整9个月。在这9个月里面，他用机场的洗手间洗漱，在候机室睡觉，他已经学懂因地制宜，在机场照料自己的生活，甚至还找了一份建筑工地工作。然而维克多的邋遢晦气却招来了机场负责人弗兰克（......',
        '# 幸福终点站\n<div style="text-align: center;">![幸福终点站](http://cdn.celess.cn/img_1568994073154.jpg)</div>\n## 剧情简介\n> 　　为了完成父亲的心愿，维克多（汤姆•汉克斯 Tom Hanks 饰）从故国乘坐飞机前往美国肯尼迪机场，但戏剧性的事情发生了：他被告知祖国发生政变，而他的身份证护照一一失效，同时他的签证也无法再使用。进退两难的维克多只有在机场滞留，等待新证件的办理。\n　　但是，他在机场等待了整整9个月。在这9个月里面，他用机场的洗手间洗漱，在候机室睡觉，他已经学懂因地制宜，在机场照料自己的生活，甚至还找了一份建筑工地工作。然而维克多的邋遢晦气却招来了机场负责人弗兰克（史坦利•图齐 Stanley Tucci 饰）的不满，而恐怕更令他气愤妒忌的是，美丽的空姐艾米利亚（凯瑟琳•泽塔-琼斯 Catherine Zeta-Jones 饰）竟然爱上了维克多。处在甜蜜中的维克多，也在慢慢观察机场的人生百态，自得其乐\n ----豆瓣\n \n## 故事原型\n\n上世纪70年代，身为英伊混血儿的纳瑟里从英国名校布拉德福大学毕业，并在海外参与了反对自己国家伊朗的示威游行。结果于1977年被伊朗开除国籍，此后不得不持临时难民签证流亡到欧洲。1988年，纳瑟里在前往戴高乐机场的地铁中，皮包被盗，丢失了包括难民签证在内的所有能证明其身份的证件，法国当局虽然同意其留在机场，但却不允许他离开航站大厦。尽管7年后，纳塞瑞拿到难民文件，可以自由离去，但他却仍然不愿离开，他认为自己若离开戴高乐机场，就会被逮捕，目前他仍然在等待一架永远不会到来的飞机。对于通过候机厅去往世界各地的飞行员、机场职员、快餐商和千百万乘客来说，58岁的纳塞瑞已经成了一个后现代的标志，如今他又成为好莱坞式符号中的一员。纳塞瑞的故事激起了史蒂文·斯皮尔伯格的创作激情，他买下了纳塞瑞传奇经历的改编权，并将与汤姆·汉克斯合作拍摄了这部《幸福终点站》。纳塞瑞的故事只是激发了斯皮尔伯格的灵感，电影所要讲述的并不是他的故事。好笑的是，纳塞瑞也许是世界上唯一一个即将被斯皮尔伯格当成电影素材却不知道斯皮尔伯格是谁的人，问起他这个问题的时候，纳塞瑞一脸的茫然：“他是日本人吗？”“我知道我现在已经是个名人了，这是我未到巴黎之前从未体会过的一种感觉。”纳塞瑞不无自豪的用他那种软软的波斯语告诉我们。因为常年无法见到阳光，纳塞瑞显得十分苍白，他最近一次看到阳光是在1999年。他说：“我很不快乐，因为我没有私生活可言。”想要将纳塞瑞的故事拼凑到一起是一件很困难的事情，因为他对一件事情总有不同的描述。两年前，他曾经告诉媒体联合国难民署的高级专员一直在找寻他的父母以证明他的难民身份，但是某位发言人说此话纯属无稽之谈。纳塞瑞一直称自己于1977年因参与反政府活动而遭到流放，其驱逐罪名为反对伊朗王默哈迈德·礼萨·巴列维的统治。他曾经持临时的难民签证辗转于欧洲各国，1981年，纳塞瑞在比利时获得了正式的难民签证。这之后，他往返于英国和法国之间，一直没有碰到任何麻烦。直到1988年，纳塞瑞在去往戴高乐机场的地铁中丢失了证明他身份的难民签证和一切证件。他无法证明自己的身份，所以不得不滞留于机场的候机大厅，而这一呆就是10多年。不论如何，只要是在戴高乐机场过境旅客，都会要一张纳塞瑞的照片留念。机场当局也尽可能让纳塞瑞住得舒适一些。机场所有红色塑胶椅最近都拿掉了，只有纳塞瑞睡的那张留了下来。机场附近的商家甚至发起了“留下纳塞瑞”运动。因为有人创下全世界停留在机场最长时间的纪录，对生意只有好处，没有坏处。06年8月，纳塞瑞因健康问题离开机场，到医院接受治疗。\n\n搬上荧屏后，做了很多艺术加工，他找工作当粉刷匠，救了一名自己国家的同胞，甚至被美丽的空姐艾米利亚爱上，居然能工巧匠把厕所小便池拆了，做了一个当年拿破仑送给情人的皇冠宝座，最后完成父亲的夙愿，安然回国\n\n## 生命便是等待--Life Is Waiting\n>  豆瓣用户  [Helen Li](https://www.douban.com/people/helen_bitw/ "Helen Li")的影评\n\n一个来自东欧最小国家的Viktor，去美国帮父亲实现一个未了的夙愿。从飞机起飞到在肯尼迪机场落地的这一段时间，他的国家发生政变，Viktor因此突然成为了没有国籍的人，无法入境，也无从出境；纵然纽约和美国就在数米之遥的门外，他仍然只能等在候机大厅里，直到身份明确。\n\n这一等，便是9个月。\n\n在这9个月的等待中，Viktor一点没有虚度；他在待改建的67号登机口给自己造了一个家；靠着自己的智慧，帮了一个买药救父的俄罗斯人，撮合了一段姻缘，邂逅了一段浪漫，征服了一群原本拿他当笑话的人；当他离去的时候，这个初时语言不通的东欧人，已经让整个机场为之感动。\n\n138分钟的片子， 除了最后的几分钟，几乎都浓缩在极其有限的一个小空间里---肯尼迪机场候机大厅。在这个有限的空间里，形形色色的人们，把时间延展；他们做着不同的工作，却在做着同一件事情---等待：\n折腾着Viktor的海关局长Frank，等待着老局长卸任，等了17年；\nGupta，因为在印度犯了案被通缉，逃匿在机场做一个清洁工，一做23年，而他或许并没有意识到自己在等待。\n美女空姐Amelia，从18岁到39岁，等着男人的一个承诺；她总是住酒店，为的就是男人的BP响起时，能够随时打包起身。\n开餐车的Enrique，等着一个机会向暗恋的女孩靠近；\nViktor的父亲，用了40年，收集了57位爵士高手中的56个签名，他在九泉之下，等着儿子帮他圆上最后一个签名。\n甚至连机场大厅里的背景中的各色人等，也都在各自等待....\n\n不是每一个等待，都会很快就有结果，譬如那些在海关等着通关的人们，这个等待，只需要1分钟而已；\n也不是每一个等待，都会有美丽的结局，如同抱得美人归的An；\n即使每一个美好的结局，也都不一定都有美丽的过程，好像Viktor。\n\nSpielberg说，我们每个人都如Viktor一样，在这充满变数的人生中，或许在某一个瞬间都会迷茫，然后去寻找去等待；而Tom Hanks说，“Viktor终于明白，这个世界不是按他自己的方式前进的；身处其中，只有为自己挣个好生活。”\n\n不知道作家Andrew Niccol在写这个小人物时，是不是想着一位叫做Viktor E Frankl的人。这位饱尝纳粹集中营种种艰辛的幸存者这样说，“在任何特定的环境中，人们还有一种最后的自由，就是选择自己的态度。”影片中的Viktor，不是心理学者，也不是成功学家，他只是在那样一个无从选择的环境里，在他那张憨厚的笑脸里，在七零八落令人捧腹的英语中，一连串令我们忍俊不禁的小事中，展现给我们如何乐天知命，如何心存良善，如何努力的让自己离结果更近一步。而其间的深意，与Viktor Frankl的表达，异曲同工。\n\n这部Spielberg导演，Tom Hanks和Catherine Zeta Jones主演的片子，据称票房并不理想。老实说，这部片子的确漏洞不少，结尾也多少有点画蛇添足；可是，这3个人的组合，却让这一步看似俗套的片子无限鲜活，甚至那一个个不起眼的小人物，也都刻画得个性鲜明，令人难忘。纵然这不是你所熟悉的斯氏大片，你仍然可以从中看到清晰的斯氏烙印---天马行空甚至满怀童心的想象以及对人性的高调宣扬。\n\n你或许可以觉得这是好莱坞的俗套，可是，我就是喜欢在外面刮着风的夜晚，抱着小瓜子，看一出充满温暖情调和轻松幽默，并充满“个人英雄”和毫无新意的“Happy Ending”的轻喜剧。然后，笑中含泪，若有所得。\n\nP.S.：后来在海报里，看到这部片子的TagLine是“Life Is Waiting”；禁不住会心一笑。\n\n2011-12-27\n快6年了，没想到这部片子引发了大家如此多的感慨。或许世事充满无奈，Life is waiting多少有了些慰藉的意味吧。 回头再看，其实当年也未得真谛。虽然影片的tagline是Life is waiting，而所有的结果并不是空空等来的。所谓的等待，其实是指我们在努力的同时，必须付出一些patient capital（耐心资本）。貌似等待，而世事的布局在我们的心动和行动间已然转换。新年之际，祝各位“等待”终成正果；若尚未见灯火，just hang in there: )\n',
        NULL, 1, 1, 357, 0, 0, 6, '2019-09-19 08:12:29', '2019-09-20 15:42:03', 1, 0),
       (1290, 'NIO的学习总结',
        'NIO和BIO\n[TOC]\n什么是NIO以及其与传统IO的区别\nNIO即非阻塞I/O(Non-Blocking I/O)，NIO和传统IO（一下简称IO）之间第一个最大的区别是，IO是面向流的，NIO是面向缓冲区的。java IO面向流意味着每次从流中读一个或多个字节，直至读取所有字节，它们没有被缓存在任何地方。此外，它不能前后移动流中的数据。如果需要前后移动从流中读取的数据，需要先将它缓存到一个缓冲区。NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区，需要时可......',
        '# NIO和BIO\n\n[TOC]\n\n## 什么是NIO以及其与传统IO的区别\n\nNIO即非阻塞I/O(Non-Blocking I/O)，NIO和传统IO（一下简称IO）之间第一个最大的区别是，IO是面向流的，NIO是面向缓冲区的。java IO面向流意味着每次从流中读一个或多个字节，直至读取所有字节，它们没有被缓存在任何地方。此外，它不能前后移动流中的数据。如果需要前后移动从流中读取的数据，需要先将它缓存到一个缓冲区。NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区，需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是，还需要检查是否该缓冲区中包含所有您需要处理的数据。而且，需确保当更多的数据读入缓冲区时，不要覆盖缓冲区里尚未处理的数据。\n\n## NIO几个重要的部分\n\n### 1. Channel\n\n#### 介绍\n\nChannel（通道）：Channel是一个对象，可以通过它读取和写入数据。可以把它看做是IO中的流，不同的是：\n\nChannel是双向的，既可以读又可以写，而流是单向的\n\nChannel可以进行异步的读写\n\n对Channel的读写必须通过buffer对象\n\n正如上面提到的，所有数据都通过Buffer对象处理，所以，您永远不会将字节直接写入到Channel中，相反，您是将数据写入到Buffer中；同样，您也不会从Channel中读取字节，而是将数据从Channel读入Buffer，再从Buffer获取这个字节。因为Channel是双向的，所以Channel可以比流更好地反映出底层操作系统的真实情况。\n\n在Java NIO中的Channel主要有如下几种类型：\n\n- FileChannel：从文件读取数据的\n\n- DatagramChannel：读写UDP网络协议数据\n\n- SocketChannel：读写TCP网络协议数据\n\n- ServerSocketChannel：可以监听TCP连接\n\n#### 主要类\n\n##### Channel接口\n\n所有具体channel类均直接或间接实现了此接口\n\n```java\npublic boolean isOpen(); // 获取此channel是否是处于打开状态\n\npublic void close() throws IOException; // 关闭channel 且有io错误抛出\n```\n\n##### FileChannel类\n\n在使用FileChannel之前，必须先打开它。但是，我们无法直接打开一个FileChannel，需要通过使用一个FileInputStream、FileOutputStream或RandomAccessFile来获取一个FileChannel实例\n\n```java\n protected FileChannel() {\n  } //本类的构造器 ， 不可被new 只能使用open方法或者其他的类方法获取实例\n\n   /**\n    * 两个open方法 返回FileChannel的实例对象\n    * path:要打开或创建的文件的路径\n    * options：指定文件打开方式的选项\n    * attrs: 创建文件时要自动设置的文件属性的可选列表\n    */\n   public static FileChannel open(Path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {/**.......*/}\n   public static FileChannel open(Path, OpenOption... options) throws IOException {/**...*/}\n\n   /**\n    * 从该通道读取一个字节序列到给定的缓冲区\n    */\n   public abstract int read(ByteBuffer dst) throws IOException;\n   public abstract long read(ByteBuffer[] dsts, int offset, int length) throws IOException;\n   public final long read(ByteBuffer[] dsts) throws IOException {    /**.....*/}\n\n   /**\n    * 从给定缓冲区向该通道写入一个字节序列。\n    */\n   public abstract int write(ByteBuffer src) throws IOException;\n   public abstract long write(ByteBuffer[] srcs, int offset, int length) throws IOException;\n   public final long write(ByteBuffer[] srcs) throws IOException {  /**...*/}\n\n   // 获取当前channel的位置\n   public abstract long position() throws IOException;\n\n   // 根据 newPosition 参数得到的channel\n   public abstract FileChannel position(long newPosition) throws IOException;\n\n   // 返回此通道文件的当前大小。\n   public abstract long size() throws IOException;\n\n   //截断为指定大小\n   public abstract FileChannel truncate(long size) throws IOException;\n\n   // 是否强制写入存储设备\n   // 出于性能方面的考虑，操作系统会将数据缓存在内存中，所以无法保证写入到FileChannel里的数据一定会即时写到磁盘上。要保证这一点，需要调用force()方法。force()方法有一个boolean类型的参数，指明是否同时将文件元数据（权限信息等）写到磁盘上。\n   public abstract void force(boolean metaData) throws IOException;\n\n   // 将数据传给所给的Channel中\n   public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;\n\n   // 吧数据从所给Channel中转到当前通道\n   public abstract long transferFrom(ReadableByteChannel src, long position, long count) throws IOException;\n\n   // 将此Channel中的某段数据映射到内存中\n   public abstract MappedByteBuffer map(FileChannel.MapMode mode, long position, long size) throws IOException;\n\n   // 获取某个区域的FileLock(文件锁，保证线程安全)\n   public abstract FileLock lock(long position, long size, boolean shared) throws IOException;\n   public final FileLock lock() throws IOException {/**.....*/}\n\n   // 尝试获取FileLock区域  \n   public abstract FileLock tryLock(long position, long size, boolean shared) throws IOException;\n\n   // 获取整个文件的fileLock\n   public final FileLock tryLock() throws IOException {/**...*/}\n\n```\n\n##### DatagramChannel类\n\nDatagramChannel是一个能收发UDP包的通道\n\n```java\n   protected DatagramChannel(SelectorProvider provider) //不可通过new来获取实例对象\n\n   // 获取DatagramChannel实例 ProtocolFamily 为协议类型（可选IPv4，IPv6）\n   public static DatagramChannel open() throws IOException { /**....*/}\n   public static DatagramChannel open(ProtocolFamily family) throws IOException {/**...*/}\n\n   // 返回确定此频道支持的操作的操作集。\n   public final int validOps() {/**...*/}\n\n   // 用于socket的操作\n   public abstract DatagramChannel bind(SocketAddress local)throws IOException;\n   public abstract <T> DatagramChannel setOption(SocketOption<T> name, T value)throws IOException;\n   public abstract DatagramSocket socket();// 返回与此channel有关的socket\n   public abstract boolean isConnected();// socket是否连接\n   public abstract DatagramChannel connect(SocketAddress remote)throws IOException;// 连接socket\n   public abstract DatagramChannel disconnect() throws IOException;// 关闭连接\n   public abstract SocketAddress getRemoteAddress() throws IOException;// 获取已连接的socket的地址\n   public abstract SocketAddress receive(ByteBuffer dst) throws IOException;// 接受数据\n   public abstract int send(ByteBuffer src, SocketAddress target)throws IOException;// 发送数据\n\n   // 读取数据的Read Write方法\n   // 。。。。。。。。。\n\n   public abstract SocketAddress getLocalAddress() throws IOException;// 获取本地的socket地址\n```\n\n##### SocketChannel类\n\nSocketChannel主要是用来基于TCP通信的通道\n\n```java\nprotected SocketChannel(SelectorProvider provider)// 不可new\n\n// 获取实例\npublic static SocketChannel open() throws IOException {}\npublic static SocketChannel open(SocketAddress remote)throws IOException{}\n\n/**   同 DatagramChannnel 的Socket操作方法*/\n// ......zh\n\n   public abstract boolean isConnectionPending();// 判断管道上的连接操作是否正在进行\n\n// 在不关闭通道的情况下关闭连接以进行读取/写入。\npublic abstract SocketChannel shutdownInput() throws IOException;\npublic abstract SocketChannel shutdownOutput() throws IOException;\n\npublic abstract boolean finishConnect() throws IOException;//结束连接\n```\n\n##### ServerSocketChannel类\n\nServerSocketChannel是一个可以监听新进来的TCP连接的通道\n\nServerSocketChannel类的方法主要进行Socket的连接的监听，接受，方法与前面的DatagramChannel和SocketChannel内的方法基本一致。\n\n![ServerSocketChannel](https://oss.celess.cn/web/blogimg/ServerSocketChannel.png)\n\n### 2. Buffer\n\nBuffer是一个对象，它包含一些要写入或者读到Stream对象的。应用程序不能直接对 Channel 进行读写操作，而必须通过 Buffer 来进行，即 Channel 是通过 Buffer 来读写数据的。\n\n在NIO中，所有的数据都是用Buffer处理的，它是NIO读写数据的中转池。Buffer实质上是一个数组，通常是一个字节数据，但也可以是其他类型的数组。但一个缓冲区不仅仅是一个数组，重要的是它提供了对数据的结构化访问，而且还可以跟踪系统的读写进程。\n\n使用 Buffer 读写数据一般遵循以下四个步骤：\n\n1. 写入数据到 Buffer；\n\n2. 调用 flip() 方法；\n\n3. 从 Buffer 中读取数据；\n\n4. 调用 clear() 方法或者 compact() 方法。\n\n当向 Buffer 写入数据时，Buffer 会记录下写了多少数据。一旦要读取数据，需要通过 flip() 方法将 Buffer 从写模式切换到读模式。在读模式下，可以读取之前写入到 Buffer 的所有数据。\n\n一旦读完了所有的数据，就需要清空缓冲区，让它可以再次被写入。有两种方式能清空缓冲区：调用 clear() 或 compact() 方法。clear() 方法会清空整个缓冲区。compact() 方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处，新写入的数据将放到缓冲区未读数据的后面。\n\nBuffer主要有如下几种：\n\n- ByteBuffer\n\n- CharBuffer\n\n- DoubleBuffer\n\n- FloatBuffer\n\n- IntBuffer\n\n- LongBuffer\n\n- ShortBuffer\n\n#### Buffer类的主要方法\n\n```java\nBuffer(int mark, int pos, int lim, int cap) {  /**.......*/ }构造器，进行初始化\npublic final int capacity() // 获取缓冲区容量\npublic final int position() // 获取当前缓冲区位置\npublic final Buffer position(int newPosition) // 设置buffer的位置，并返回设置后的buffer\npublic final int limit() // 获取缓冲区的限制\npublic final Buffer limit(int newLimit) // 设置缓冲区的限制\npublic final Buffer mark() //在此位置设置此缓冲区的标记\npublic final Buffer reset() // 将缓冲位置设置为先去标记的位置\npublic final Buffer clear() // 清除缓冲区 position设置为0 limit设置为容量 mark被清除\npublic final Buffer flip() // 模式切换limit设置为当前position 且position设置为0 mark被清除\npublic final Buffer rewind() // 倒回这个缓冲区。position设置为零，mark被清除\npublic final int remaining() // 返回limit-position的元素数\npublic final boolean hasRemaining() // 是否有剩余的元素\npublic abstract boolean isReadOnly()// 判断此缓冲区是否为只读\npublic abstract boolean hasArray()// 判断此缓冲区是否由可访问数组支持\npublic abstract Object array()//返回支持此数组的数组\npublic abstract int arrayOffset()//返回第一个缓冲区的后备数组中的偏移量\npublic abstract boolean isDirect()//判断此字节缓冲区是否是直接的。\n```\n\n### 3. Scatter / Gather\n\nscatter / gather是通过通道读写数据的两个概念。\n\nScattering read指的是从通道读取的操作能把数据写入多个buffer，也就是sctters代表了数据从一个channel到多个buffer的过程。\n\ngathering write则正好相反，表示的是从多个buffer把数据写入到一个channel中。\n\nScatter/gather在有些场景下会非常有用，比如需要处理多份分开传输的数据。举例来说，假设一个消息包含了header和body，我们可能会把header和body保存在不同独立buffer中，这种分开处理header与body的做法会使开发更简明\n\n#### Scattering Read\n\n![Scattering Read](https://oss.celess.cn/web/blogimg/Scattering_Read.png)\n\n用代码表示\n\n```java \nByteBuffer header = ByteBuffer.allocate(128);\nByteBuffer body   = ByteBuffer.allocate(1024);\n\n//write data into buffers\n\nByteBuffer[] bufferArray = { header, body };\n\nchannel.read(bufferArray);\n```\n\n我们把多个buffer写在了一个数组中，然后把数组传递给channel.read()方法。read()方法内部会负责把数据按顺序写进传入的buffer数组内。一个buffer写满后，接着写到下一个buffer中。\n\n实际上，scattering read内部必须写满一个buffer后才会向后移动到下一个buffer，因此这并不适合消息大小会动态改变的部分，也就是说，如果你有一个header和body，并且header有一个固定的大小（比如128字节）,这种情形下可以正常工作。\n\n \n\n#### Gathering Writes\n\n![Gathering Writes](https://oss.celess.cn/web/blogimg/Gathering_Writes.png)\n\n用代码表示\n\n```java\nByteBuffer header = ByteBuffer.allocate(128);\nByteBuffer body   = ByteBuffer.allocate(1024);\n\n//write data into buffers\n\nByteBuffer[] bufferArray = { header, body };\n\nchannel.write(bufferArray);\n```\n\n类似的传入一个buffer数组给write，内部机会按顺序将数组内的内容写进channel，这里需要注意，写入的时候针对的是buffer中position到limit之间的数据。也就是如果buffer的容量是128字节，但它只包含了58字节数据，那么写入的时候只有58字节会真正写入。因此gathering write是可以适用于可变大小的message的，这和scattering reads不同。\n\n### 4. Selector\n\nSelector是Java NIO中的一个组件，用于检查一个或多个NIO Channel的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。\n\n#### 为什么使用Selector\n\n用单线程处理多个channels的好处是我需要更少的线程来处理channel。实际上，你甚至可以用一个线程来处理所有的channels。从操作系统的角度来看，切换线程开销是比较昂贵的，并且每个线程都需要占用系统资源，因此占用线程越少越好。\n\n需要留意的是，现代操作系统和CPU在多任务处理上已经变得越来越好，所以多线程带来的影响也越来越小。如果一个CPU是多核的，如果不执行多任务反而是浪费了机器的性能。不过这些设计讨论是另外的话题了。简而言之，通过Selector我们可以实现单线程操作多个channel。\n\n#### 主要方法\n\n```java\npublic static Selector open() throws IOException //获取Selector实例\npublic abstract boolean isOpen();// 获取此selector是否打开\npublic abstract SelectorProvider provider() // 返回SelectorProvider \npublic abstract Set<SelectionKey> keys();// 返回此selector的key的集合 这里面的key不能直接修改，key只有在他被取消或者channel被注销后才会被移除。同时这个set非线程安全\npublic abstract Set<SelectionKey> selectedKeys();//返回 selected-key的集合\n/**\n* select()方法在返回channel之前处于阻塞状态。 select(long timeout)和select做的事一样，不过他的阻塞有一个超时限制。\n* selectNow()不会阻塞，根据当前状态立刻返回合适的channel\n* select()方法的返回值是一个int整形，代表有多少channel处于就绪了。也就是自上一次select后有多少channel进入就绪\n*/\npublic abstract int selectNow() throws IOException;//根据当前状态立刻返回合适的channel\npublic abstract int select(long timeout)throws IOException;\npublic abstract int select() throws IOException;\n\npublic abstract Selector wakeup();//由于调用select而被阻塞的线程，可以通过调用Selector.wakeup()来唤醒即便此时已然没有channel处于就绪状态。具体操作是，在另外一个线程调用wakeup，被阻塞与select方法的线程就会立刻返回。\npublic abstract void close() throws IOException;// 关闭selector\n```\n\n### 5. Pipe\n\n一个Java NIO的管道是两个线程间单向传输数据的连接。一个管道（Pipe）有一个source channel和一个sink channel。我们把数据写到sink channel中，这些数据可以同过source channel再读取出来。\n\n管道的示意图\n\n![Pipe](http://oss.celess.cn/web/blogimg/Pipe.png)\n\n#### 向管道写入数据\n\n```java\n//向管道写入数据需要访问他的sink channel\nPipe.SinkChannel sinkChannel = pipe.sink();\n\n//接下来就是调用write()方法写入数据了：\nString newData = "New String to write to file "\n\nByteBuffer buf = ByteBuffer.allocate(48);\nbuf.clear();\nbuf.put(newData.getBytes());\n\nbuf.flip();\n\nwhile(buf.hasRemaining()) {\n    sinkChannel.write(buf);\n}\n```\n\n#### 从管道读取数据\n\n```java\n//访问他的source channel\nPipe.SourceChannel sourceChannel = pipe.source();\nByteBuffer buf = ByteBuffer.allocate(48);\n\n//调用read()方法读取数据\nint bytesRead = inChannel.read(buf);\n```\n\n',
        NULL, 1, 1, 262, 0, 0, 2, '2019-10-11 06:48:53', NULL, 1, 0);
INSERT INTO `article` (`a_id`, `a_title`, `a_summary`, `a_md_content`, `a_url`, `a_author_id`, `a_is_original`,
                       `a_reading_number`, `a_like`, `a_dislike`, `a_category_id`, `a_publish_date`, `a_update_date`,
                       `a_is_open`, `is_delete`)
VALUES (1291, 'Netty学习总结',
        'Netty\n[TOC]\n1.什么是Netty\nNetty是NIO客户端服务器框架，支持协议服务器和客户端等网络应用的快速简单开发。它极大地简化了网络编程，如TCP和UDP套接字服务器。Netty 是一个精心设计的框架，它从许多协议的实现中吸收了很多的经验比如 FTP、SMTP、HTTP、许多二进制和基于文本的传统协议。因此，Netty成功地找到了一种方法，在不失灵活性的前提下来实现开发、性能、稳定性和灵活性\n2.为什么是Netty\n\nThe problem\n今天，我们使用通用......',
        '# Netty\n[TOC]\n## 1.什么是Netty\n\nNetty是NIO客户端服务器框架，支持协议服务器和客户端等网络应用的快速简单开发。它极大地简化了网络编程，如TCP和UDP套接字服务器。Netty 是一个精心设计的框架，它从许多协议的实现中吸收了很多的经验比如 FTP、SMTP、HTTP、许多二进制和基于文本的传统协议。因此，Netty成功地找到了一种方法，在不失灵活性的前提下来实现开发、性能、稳定性和灵活性\n\n## 2.为什么是Netty\n\n### The problem \n\n今天，我们使用通用的应用程序或者类库来实现互相通讯，比如，我们经常使用一个 HTTP 客户端库来从 web 服务器上获取信息，或者通过 web 服务来执行一个远程的调用。\n\n然而，有时候一个通用的协议或他的实现并没有很好的满足需求。比如我们无法使用一个通用的 HTTP 服务器来处理大文件、电子邮件以及近实时消息，比如金融信息和多人游戏数据。我们需要一个高度优化的协议来处理一些特殊的场景。例如你可能想实现一个优化了的 Ajax 的聊天应用、媒体流传输或者是大文件传输器，你甚至可以自己设计和实现一个全新的协议来准确地实现你的需求。\n\n另一个不可避免的情况是当你不得不处理遗留的专有协议来确保与旧系统的互操作性。在这种情况下，重要的是我们如何才能快速实现协议而不牺牲应用的稳定性和性能。\n\n### The Solution\n\n[Netty](http://netty.io/) 是一个提供 asynchronous event-driven （异步事件驱动）的网络应用框架，是一个用以快速开发高性能、可扩展协议的服务器和客户端。\n\n换句话说，Netty 是一个 NIO 客户端服务器框架，使用它可以快速简单地开发网络应用程序，比如服务器和客户端的协议。Netty 大大简化了网络程序的开发过程比如 TCP 和 UDP 的 socket 服务的开发。\n\n“快速和简单”并不意味着应用程序会有难维护和性能低的问题，Netty 是一个精心设计的框架，它从许多协议的实现中吸收了很多的经验比如 FTP、SMTP、HTTP、许多二进制和基于文本的传统协议. 因此，Netty成功地找到了一种方法，在不失灵活性的前提下来实现开发、性能、稳定性和灵活性\n\n\n\n## 3.Netty组件\n\n### Bootstrap \n\nNetty 应用程序通过设置 bootstrap（引导）类的开始，该类提供了一个 用于应用程序网络层配置的容器。\n\nBootstrap有两种类型，一种是用于客户端的Bootstrap，一种是用于服务端的ServerBootstrap。不管程序使用哪种协议，无论是创建一个客户端还是服务器都需要使用“引导”。\n\n### Channel\n\n底层网络传输 API 必须提供给应用 I/O操作的接口，如读，写，连接，绑定等等。对于我们来说，这是结构几乎总是会成为一个“socket”。 Netty 中的接口 Channel 定义了与 socket 丰富交互的操作集：bind, close, config, connect, isActive, isOpen, isWritable, read, write 等等。 Netty 提供大量的 Channel 实现来专门使用。这些包括 AbstractChannel，AbstractNioByteChannel，AbstractNioChannel，EmbeddedChannel， LocalServerChannel，NioSocketChannel 等等。\n\n### ChannelHandler \n\nChannelHandler 支持很多协议，并且提供用于数据处理的容器。我们已经知道 ChannelHandler 由特定事件触发。 ChannelHandler 可专用于几乎所有的动作，包括将一个对象转为字节（或相反），执行过程中抛出的异常处理。\n\n常用的一个接口是 ChannelInboundHandler，这个类型接收到入站事件（包括接收到的数据）可以处理应用程序逻辑。当你需要提供响应时，你也可以从 ChannelInboundHandler 冲刷数据。一句话，业务逻辑经常存活于一个或者多个 ChannelInboundHandler。\n\n### ChannelPipeline\n\nChannelPipeline 提供了一个容器给 ChannelHandler 链并提供了一个API 用于管理沿着链入站和出站事件的流动。每个 Channel 都有自己的ChannelPipeline，当 Channel 创建时自动创建的。 ChannelHandler 是如何安装在 ChannelPipeline？ 主要是实现了ChannelHandler 的抽象 ChannelInitializer。ChannelInitializer子类 通过 ServerBootstrap 进行注册。当它的方法 initChannel() 被调用时，这个对象将安装自定义的 ChannelHandler 集到 pipeline。当这个操作完成时，ChannelInitializer 子类则 从 ChannelPipeline 自动删除自身。\n\n### EventLoop \n\nEventLoop 用于处理 Channel 的 I/O 操作。一个单一的 EventLoop通常会处理多个 Channel 事件。一个 EventLoopGroup 可以含有多于一个的 EventLoop 和 提供了一种迭代用于检索清单中的下一个。\n\n### ChannelFuture\n\nNetty 所有的 I/O 操作都是异步。因为一个操作可能无法立即返回，我们需要有一种方法在以后确定它的结果。出于这个目的，Netty 提供了接口 ChannelFuture,它的 addListener 方法注册了一个 ChannelFutureListener ，当操作完成时，可以被通知（不管成功与否）。\n\n## 方法解析\n\n### Bootstrap\n\n#### AbstractBootstrap类\n\nAbstractBootstrap是Bootstrap的基类, 类定义如下\n\n``public abstract class AbstractBootstrap*<*B extends AbstractBootstrap*<*B, C*>*, C extends Channel*>* implements Cloneable``\n\n其中泛型要求B为其本类的子类，C为channel的子类。\n\n```java\n    // 设置EventLoopGroup   在ServerBootstrap中调用此方法为设置parentGroup\n    public B group(EventLoopGroup group) {\n        // 空值检查\n        ObjectUtil.checkNotNull(group, "group");\n        if (this.group != null) {\n            throw new IllegalStateException("group set already");\n        }\n        this.group = group;\n        // self() 返回强转后的当前对象\n        return self();\n    }\n    \n    \n    // 设置Channel\n    public B channel(Class<? extends C> channelClass) {\n        // 通过ChannelFactory来获取Channel的实例对象\n        return channelFactory(new ReflectiveChannelFactory<C>(\n                ObjectUtil.checkNotNull(channelClass, "channelClass")\n        ));\n    }\n    \n    // 设置ChannelFactory\n    public B channelFactory(io.netty.channel.ChannelFactory<? extends C> channelFactory) {\n        return channelFactory((ChannelFactory<C>) channelFactory);\n    }\n    \n    // 设置localAddress\n    public B localAddress(SocketAddress localAddress) {\n        this.localAddress = localAddress;\n        return self();\n    }\n    public B localAddress(int inetPort) {\n        return localAddress(new InetSocketAddress(inetPort));\n    }\n    public B localAddress(String inetHost, int inetPort) {\n        return localAddress(SocketUtils.socketAddress(inetHost, inetPort));\n    }\n    public B localAddress(InetAddress inetHost, int inetPort) {\n        return localAddress(new InetSocketAddress(inetHost, inetPort));\n    }\n    \n    // 设置option的值 或者移除option\n    public <T> B option(ChannelOption<T> option, T value) {\n        ObjectUtil.checkNotNull(option, "option");\n        if (value == null) {\n            synchronized (options) {\n                options.remove(option);\n            }\n        } else {\n            synchronized (options) {\n                options.put(option, value);\n            }\n        }\n        return self();\n    }\n    \n    // 设置key的值或者移除key\n    public <T> B attr(AttributeKey<T> key, T value) {\n        ObjectUtil.checkNotNull(key, "key");\n        if (value == null) {\n            synchronized (attrs) {\n                attrs.remove(key);\n            }\n        } else {\n            synchronized (attrs) {\n                attrs.put(key, value);\n            }\n        }\n        return self();\n    }\n    \n    // 数据验证\n    public B validate() {\n        if (group == null) {\n            throw new IllegalStateException("group not set");\n        }\n        if (channelFactory == null) {\n            throw new IllegalStateException("channel or channelFactory not set");\n        }\n        return self();\n    }\n    \n    // 创建新的channel并注册到EventLoop上\n    public ChannelFuture register() {\n        validate();\n        return initAndRegister();\n    }\n    \n    //新建channnel并绑定它\n    public ChannelFuture bind() {\n        // 数据验证\n        validate();\n        SocketAddress localAddress = this.localAddress;\n        if (localAddress == null) {\n            throw new IllegalStateException("localAddress not set");\n        }\n        // 绑定\n        return doBind(localAddress);\n    }\n    public ChannelFuture bind(int inetPort) {\n        return bind(new InetSocketAddress(inetPort));\n    }\n    public ChannelFuture bind(String inetHost, int inetPort) {\n        return bind(SocketUtils.socketAddress(inetHost, inetPort));\n    }\n    public ChannelFuture bind(InetAddress inetHost, int inetPort) {\n        return bind(new InetSocketAddress(inetHost, inetPort));\n    }\n    public ChannelFuture bind(SocketAddress localAddress) {\n        validate();\n        return doBind(ObjectUtil.checkNotNull(localAddress, "localAddress"));\n    }\n    \n    // 设置handler\n    public B handler(ChannelHandler handler) {\n        this.handler = ObjectUtil.checkNotNull(handler, "handler");\n        return self();\n    }\n    \n    // 获取配置\n    public abstract AbstractBootstrapConfig<B, C> config();\n```\n\n#### ServerBootstrap类\n\n用于服务端简单的引导ServerChannel。\n\n```java\n    public ServerBootstrap() { } // 无参构造器\n    \n    // 单参数group方法 设置parentGroup和childGroup 都为group\n    public ServerBootstrap group(EventLoopGroup group) {\n        return group(group, group);\n    }\n    \n    // 设置parentGroup和childGroup\n    public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {\n        super.group(parentGroup);\n        // 空值检查\n        ObjectUtil.checkNotNull(childGroup, "childGroup");\n        // 判断是否已经设置过childGroup\n        if (this.childGroup != null) {\n            throw new IllegalStateException("childGroup set already");\n        }\n        this.childGroup = childGroup;\n        return this;\n    }\n    \n    // 设置ChannelOption\n    public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value) {\n        // 空值检查\n        ObjectUtil.checkNotNull(childOption, "childOption");\n        if (value == null) {\n            // 若为null则移除childOption\n            synchronized (childOptions) {\n                childOptions.remove(childOption);\n            }\n        } else {\n            // 设置childOption\n            synchronized (childOptions) {\n                childOptions.put(childOption, value);\n            }\n        }\n        return this;\n    }\n    \n    // 设置AttributeKey\n     public <T> ServerBootstrap childAttr(AttributeKey<T> childKey, T value) {\n        // 空值检查\n        ObjectUtil.checkNotNull(childKey, "childKey");\n        if (value == null) {\n            // 若为null则移除childKey\n            childAttrs.remove(childKey);\n        } else {\n            // 设置childKey\n            childAttrs.put(childKey, value);\n        }\n        return this;\n    }\n    \n    // 设置ChannelHandler\n    public ServerBootstrap childHandler(ChannelHandler childHandler) {\n        // 判空并赋值\n        this.childHandler = ObjectUtil.checkNotNull(childHandler, "childHandler");\n        return this;\n    }\n    \n    // 验证所有的参数\n    public ServerBootstrap validate() {\n        // 父类中有验证group（前面group方法设置的parentGroup） 和 channelFactory（用以创建Channel实例的工厂的实例对象）\n        super.validate();\n        // 验证childHandler （ChannelHandler实例对象）\n        if (childHandler == null) {\n            throw new IllegalStateException("childHandler not set");\n        }\n        //验证childGroup （group方法设置的 EventLoopGroup childGroup）\n        if (childGroup == null) {\n            // 未设置childGroup 默认使用parentGroup\n            logger.warn("childGroup is not set. Using parentGroup instead.");\n            childGroup = config.group();\n        }\n        return this;\n    }\n    \n    // 返回一个克隆后的实例对象\n    public ServerBootstrap clone() {\n        return new ServerBootstrap(this);\n    }\n    \n    // 返回配置信息\n    public final ServerBootstrapConfig config() {\n        return config;\n    }\n```\n\n#### ServerBootstrapConfig类\n\nServerBootstrap的配置类。ServerBootstrapConfig继承自AbstractBootstrapConfig，是一个配置类，可以通过各个方法获取对于的配置值\n\n![ServerBootstrapConfig](https://oss.celess.cn/web/blogimg/serverBootstrapConfig.png)\n\n#### Bootstrap类\n\n```java\npublic Bootstrap() { } // 无参构造方法\n\n// 默认设置为DefaultAddressResolverGroup.INSTANCE, 可以通过传入非null值来赋值:\npublic Bootstrap resolver(AddressResolverGroup<?> resolver) {\n    DEFAULT_RESOLVER : resolver);\n    return this;\n}\n\n// 设置连接的远程地址和端口\npublic Bootstrap remoteAddress(SocketAddress remoteAddress) {\n    this.remoteAddress = remoteAddress;\n    return this;\n}\npublic Bootstrap remoteAddress(String inetHost, int inetPort) {\n    remoteAddress = InetSocketAddress.createUnresolved(inetHost, inetPort);\n    return this;\n}\npublic Bootstrap remoteAddress(InetAddress inetHost, int inetPort) {\n    remoteAddress = new InetSocketAddress(inetHost, inetPort);\n    return this;\n}\n\n//进行连接\npublic ChannelFuture connect() {\n    // 参数验证\n    validate();\n    // 地址判空\n    SocketAddress remoteAddress = this.remoteAddress;\n    if (remoteAddress == null) {\n        throw new IllegalStateException("remoteAddress not set");\n    }\n    // 连接\n    return doResolveAndConnect(remoteAddress, config.localAddress());\n}\n/** 省略多个connect重载函数  参数均为不同形式的设置远程连接的地址和端口 */\n\n// 参数验证\npublic Bootstrap validate() {\n    //父类参数验证\n    super.validate();\n    handler非空验证\n    if (config.handler() == null) {\n        throw new IllegalStateException("handler not set");\n    }\n    return this;\n}\n// 返回此类实例的克隆实例\npublic Bootstrap clone() {\n    return new Bootstrap(this);\n}\npublic Bootstrap clone(EventLoopGroup group) {\n    Bootstrap bs = new Bootstrap(this);\n    bs.group = group;\n    return bs;\n}\n\n// 返回配置信息\npublic final BootstrapConfig config() {\n    return config;\n}\n```\n\n#### BootstrapConfig类\n\nBootstrap的配置类也继承自AbstractBootstrapConfig，类方法也几乎与ServerBootstrap相同，具体方法如下图\n\n![BootstrapConfig](https://oss.celess.cn/web/blogimg/BootstrapConfig.png)\n\n### EventLoop\n\n#### NioEventLoopGroup类\n\n多个构造函数以及设置子事件循环中I/O所需时间的百分比的setIoRatio方法和一个rebuildSelectors方法\n\n![NioEventLoopGroup](https://oss.celess.cn/web/blogimg/NioEventLoopGroup.png)\n\n#### EventLoopGroup类\n\n```java\n// 获取下一个EventLoop\n    @Override\n    EventLoop next();\n\n//注册channel\n    ChannelFuture register(Channel channel);\n\n    // 使用ChannelFuture注册一个channel\n    ChannelFuture register(ChannelPromise promise);\n```\n\n### Channel\n\n#### Channel接口\n\n```java\n//获取基础属性\nChannelId id()\nChannel parent()\nChannelConfig config()\nChannelMetadata metadata(): 获取TCP参数配置\n\n//获取运行时属性\nEventLoop eventLoop()\nChannel.Unsafe unsafe()\nChannelPipeline pipeline()\nByteBufAllocator alloc()\n\n//获取状态\nboolean isOpen()\nboolean isRegistered()\nboolean isActive()\nboolean isWritable()\n\n//获取和网络相关属性\nSocketAddress localAddress()\nSocketAddress remoteAddress()\n\n//获取各种Future和Promise\nChannelFuture closeFuture()\nChannelPromise newPromise()\nChannelProgressivePromise newProgressivePromise()\nChannelFuture newSucceededFuture()\nChannelFuture newFailedFuture(Throwable cause)\nChannelPromise voidPromise()\n\n//IO操作\nChannelFuture bind(SocketAddress localAddress)\nChannelFuture bind(SocketAddress localAddress, ChannelPromise promise)\nChannelFuture connect(SocketAddress remoteAddress)\nChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress)\nChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise)\nChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise)\nChannelFuture disconnect()\nChannelFuture disconnect(ChannelPromise promise)\nChannelFuture close()\nChannelFuture close(ChannelPromise promise)\nChannelFuture deregister()\nChannelFuture deregister(ChannelPromise promise)\nChannel read()\nChannelFuture write(Object msg)\nChannelFuture write(Object msg, ChannelPromise promise)\nChannel flush()\nChannelFuture writeAndFlush(Object msg)\nChannelFuture writeAndFlush(Object msg, ChannelPromise promise)\n```\n\n### Channel Handler\n\n#### ChannelHandler接口\n\n```java\n//  在ChannelHandler被添加到实际的context并准备处理事件后被调用\nvoid handlerAdded(ChannelHandlerContext ctx) throws Exception;\n\n// 在ChannelHandler被从实际的context中删除并不再处理事件后被调用\nvoid handlerRemoved(ChannelHandlerContext ctx) throws Exception;\n\n//标注一个channel handler可以被多个channel安全地共享。\n@Inherited\n@Documented\n@Target(ElementType.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\n@interface Sharable {\n    // no value\n}\n```\n\n#### ChannelInboundHandlerAdapter类\n\nChannelInboundHandlerAdapter是常规情况下用户实现ChannelHandler的继承类，其与ChannelHandler接口的关系如下ChannelInboundHandlerAdapter中主要实现了ChannelInboundHandler接口。\n\n![ChannelInboundHandlerAdapter](https://oss.celess.cn/web/blogimg/ChannelInboundHandlerAdapter.png)\n\n#### ChannelInboundHandler接口\n\n```java\nchannelRegistered(); ChannelHandlerContext的Channel被注册到EventLoop\nchannelUnregistered(); ChannelHandlerContext的Channel被从EventLoop中注销\nchannelActive(); ChannelHandlerContext的Channel被激活\nchannelInactive(); 被注册的ChannelHandlerContext的Channel现在被取消并到达了它生命周期的终点\nchannelRead(); 当Channel读取到一个消息时被调用\nchannelReadComplete(); 当前读操作读取的最后一个消息被channelRead()方法消费时调用. 如果ChannelOption.AUTO_READ 属性被设置为off, 不会再尝试从当前channel中读取inbound数据, 直到ChannelHandlerContext.read()方法被调用.\nuserEventTriggered(); 当用户事件被触发时调用\nchannelWritabilityChanged(); 当channel的可写状态变化时被调用, 可以通过Channel.isWritable()来检查状态.\nexceptionCaught(); 当Throwable被抛出时被调用\n```\n\n#### ChannelPipeline接口\n\nChannelPipeline用来管理ChannelHandler\n\n```java\n\n// 增加ChannelHandler\n\n/* addFirst()方法: 将handler添加到pipeline的第一个位置\n * 参数中, name是当前要插入的handler的名字, 如果设置为null则自动生成.\n * 注意: name不容许重复, 如果添加的时候发现已经存在同样name的handler, 则会抛出IllegalArgumentException.\n */\n ChannelPipeline addFirst(String name, ChannelHandler handler);\n ChannelPipeline addFirst(EventExecutorGroup group, String name, ChannelHandler handler);\n ChannelPipeline addFirst(ChannelHandlerInvoker invoker, String name, ChannelHandler handler);\n\n\n/* addLast()方法\n * 和addFirst()方法完全对应\n * addBefore()方法 \n * 参数中, name和addFirst()中一致, 但是多了一个baseName, 这个是插入的基准handler的名字, 即要插在这个名字的handle前面.\n */\n\n ChannelPipeline addBefore(String baseName, String name, ChannelHandler handler);\n ChannelPipeline addBefore(EventExecutorGroup group, String baseName, String name, ChannelHandler handler);\n ChannelPipeline addBefore(ChannelHandlerInvoker invoker, String baseName, String name, ChannelHandler handler);\n\n/**\n * addAfter()方法\n * 和addBefore()方法完全对应.\n * 另外以上方法还有可以参数的方法重载, 无非就是将参数中的一个handler变成ChannelHandler... handlers.\n */\n\n\n// 删除ChannelHandler\n/**\n * remove()方法查找给定参数的handler, 并从ChannelPipeline中删除它, 然后将被删除的handle返回:\n * 如果删除时发现找不到要删除的目标, 这些remove()方法会抛出NoSuchElementException, 这个要小心处理.\n */\nChannelPipeline remove(ChannelHandler handler);\nChannelHandler remove(String name);\n<T extends ChannelHandler> T remove(Class<T> handlerType);\nChannelHandler removeLast();\nChannelHandler removeFirst();\n \n\n// 替换ChannelHandler\n/**\n *replace()方法用于替换原来的handler为新的handler,\n * 如果替换时发现找不到要替换的目标, replace()方法会抛出NoSuchElementException.\n */\nChannelPipeline replace(ChannelHandler oldHandler, String newName, ChannelHandler newHandler);\nChannelHandler replace(String oldName, String newName, ChannelHandler newHandler);\n<T extends ChannelHandler> T replace(Class<T> oldHandlerType, String newName, ChannelHandler newHandler);\n\n\n// 获取ChannelHandler\n//如果pipeline为空, 则上面的方法返回null值.\nChannelHandler first();\nChannelHandler last();\nChannelHandler get(String name);\n<T extends ChannelHandler> T get(Class<T> handlerType);\n\n\n// 获取ChannelHandlerContext\n//如果pipeline为空或者pipeline中找不到要求的hanlder, 则上面的方法返回null值.\nChannelHandlerContext firstContext();\nChannelHandlerContext lastContext();\nChannelHandlerContext context(ChannelHandler handler);\nChannelHandlerContext context(String name);\nChannelHandlerContext context(Class< ? extends ChannelHandler> handlerType);\n\n// 其他管理handler的方法\nList<String> names();\nMap<String, ChannelHandler> toMap();\n//获取Channel\nChannel channel();\n// 如果当前pipeline 还没有绑定到channel, 则你这里返回null.\n\n//fire方法\n//fire开头的这些方法是给事件通知用的:\n\n// channel被注册到eventloop\nChannelPipeline fireChannelRegistered();\n\n// channel被从eventloop注销\nChannelPipeline fireChannelUnregistered();\n\n// channel被激活, 通常是连接上了\nChannelPipeline fireChannelActive();\n\n// channle被闲置, 通常是被关闭\nChannelPipeline fireChannelInactive();\n\n// channel收到一个Throwable, 比较有意思的是javadoc中明确指出发生的地点是"in one of its inbound operations"\nChannelPipeline fireExceptionCaught(Throwable cause);\n\n// channel收到一个用户自定义事件\nChannelPipeline fireUserEventTriggered(Object event);\n//还有几个方法是给channel读写的:\n\n// channel收到信息\nChannelPipeline fireChannelRead(Object msg);\n\nChannelPipeline fireChannelReadComplete();\nChannelPipeline fireChannelWritabilityChanged();\n//I/O操作方法\nChannelFuture bind(SocketAddress localAddress);\nChannelFuture connect(SocketAddress remoteAddress);\nChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress);\nChannelFuture disconnect();\nChannelFuture close();\nChannelFuture deregister();\n//以及带ChannelPromise的变体版本:\n\nChannelFuture bind(SocketAddress localAddress, ChannelPromise promise);\nChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise);\nChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise);\nChannelFuture disconnect(ChannelPromise promise);\nChannelFuture close(ChannelPromise promise);\nChannelFuture deregister(ChannelPromise promise);\n//I/O读写方法\nChannelPipeline read();\nChannelFuture write(Object msg);\nChannelFuture write(Object msg, ChannelPromise promise);\nChannelPipeline flush();\nChannelFuture writeAndFlush(Object msg, ChannelPromise promise);\nChannelFuture writeAndFlush(Object msg);\n```\n\n### Buffer\n\nByteBuf 有 2 部分：一个用于读，一个用于写。我们可以按顺序的读取数据，并且可以跳到开始重新读一遍。 所有的数据操作， 我们只需要做的是调整读取数据索引和再次开始读操作。 写入数据到 ByteBuf 后， 写入索引是增加的字节数量。 开始读字节后， 读取索引增加。\n\n使用 Netty 时会遇到 3 种不同类型的 ByteBuf：\n\n- **Heap Buffer( 堆缓冲区)** \n\n最常用的类型是 ByteBuf 将数据存储在 JVM 的堆空间，这是通过将数据存储在数组的实现。堆缓冲区可以快速分配，当不使用时也可以快速释放。它还提供了直接访问数组的方法，通过ByteBuf.array()来获取 byte[]数据。 访问非堆缓冲区 ByteBuf 的数组会导致 UnsupportedOperationException， 可以使用ByteBuf.hasArray()来检查是否支持访问数组。\n\n- **Direct Buffer( 直接缓冲区)** \n\n直接缓冲区，在堆之外直接分配内存。直接缓冲区不会占用堆空间容量，使用时应该考虑到应用程序要使用的最大内存容量以及如何限制它。直接缓冲区在使用 Socket 传递数据时性能很好，因为若使用间接缓冲区，JVM 会先将数据复制到直接缓冲区再进行传递；但是直接缓冲区的缺点是在分配内存空间和释放内存时比堆缓冲区更复杂， 而 Netty使用内存池来解决这样的问题，这也是 Netty 使用内存池的原因之一。直接缓冲区不支持数组访问数据，但是我们可以间接的访问数据数组\n\n- **Composite Buffer(复合缓冲区)** \n\n复合缓冲区，我们可以创建多个不同的 ByteBuf，然后提供一个这些 ByteBuf \n 组合的视图。复合缓冲区就像一个列表，我们可以动态的添加和删除其中的 ByteBuf，JDK 的ByteBuffer 没有这样的功能。Netty 提供了 CompositeByteBuf 类来处理复合缓冲区，CompositeByteBuf只是一个视图，CompositeByteBuf.hasArray()总是返回 false，因为它 可能包含一些直接或间接的不同类型的 ByteBuf。\n\n**常用方法：**\n\n- **读操作 ：get/read get不会改变读索引，read会改变读索引**\n\n![get/read](https://oss.celess.cn/web/blogimg/buffer_getread.png)              \n\n- **写操作 set/write**\n\n ![set/write](https://oss.celess.cn/web/blogimg/buffer_setwrite.png)\n\n- **索引管理**\n\n![索引](https://oss.celess.cn/web/blogimg/buffer_index.png)\n\n- **查找**\n\n![查找](https://oss.celess.cn/web/blogimg/buffer_find.png)\n\n- **副本**\n\n![副本](https://oss.celess.cn/web/blogimg/buffer_cop.png)\n\n- **其他**\n\n![其他](https://oss.celess.cn/web/blogimg/buffer_other.png)\n\n### Codec\n\ncodec的作用就是将原始字节数据与目标程序数据格式进行互转。网络中都是以字节码的数据形式来传输数据的，codec 由两部分组成：decoder(解码器)和encoder(编码器)\n\n#### Decoder(解码器)\n\n##### ByteToMessageDecoder类\n\nByteToMessageDecoder 是用于将字节转为消息（或其他字节序列）。\n\n你不能确定远端是否会一次发送完一个完整的“信息”,因此这个类会缓存入站的数据,直到准备好了用于处理\n\n类方法如下\n\n​         ![ByteToMessageDecoder](https://oss.celess.cn/web/blogimg/ByteToMessageDecoder.png)\n\n```java\npublic void setSingleDecode(boolean singleDecode)// 设置singleDecode\n\npublic boolean isSingleDecode() //如果为true，则每个channelRead（）调用仅解码一条消息。因为会影响性能,故默认值为false\n\npublic void setCumulator(Cumulator cumulator) // 设置ByteToMessageDecoder.Cumulator用于累积接收到的ByteBuf。\n\npublic void setDiscardAfterReads(int discardAfterReads) // 设置调用discardsomereadbytes（）的读取次数，以便释放内存\n```\n\n##### ReplayingDecoder类\n\nReplayingDecoder 是 byte-to-message 解码的一种特殊的抽象基类，读取缓冲区的数据之前需要检查缓冲区是否有足够的字节，使用ReplayingDecoder就无需自己检查；若ByteBuf中有足够的字节，则会正常读取；若没有足够的字节则会停止解码。\n\n**类方法：**\n\n![ReplayingDecoder](https://oss.celess.cn/web/blogimg/ReplayingDecoder.png)\n\n##### MessageToMessageDecoder类\n\n用于从一种消息解码为另外一种消息\n\n![MessageToMessageDecoder](https://oss.celess.cn/web/blogimg/MessageToMessageDecoder.png)\n\n#### Encoder(编码器)\n\n##### MessageToByteEncoder类\n\n将消息编码为字节\n\n![MessageToByteEncoder](https://oss.celess.cn/web/blogimg/MessageToByteEncoder.png)',
        NULL, 1, 1, 305, 0, 0, 2, '2019-10-14 14:42:29', NULL, 1, 0),
       (1292, 'Springboot 使用系统环境变量',
        'Springboot 使用系统环境变量\n\n需求\n\n集成测试\n需要使用系统的资源\n......\n\n\n\n使用\n格式 ${系统变量名:默认值}eg: ${JAVA_HOME:/java/home}\n在properties文件中  com.test=${JAVA_HOME:/java/home}在yml文件中 com.test: ${JAVA_HOME:/java/home}\n',
        '# Springboot 使用系统环境变量\n\n## 需求\n- 集成测试\n- 需要使用系统的资源\n- ......\n\n## 使用\n格式 ${系统变量名:默认值}\neg: `${JAVA_HOME:/java/home}`\n\n在properties文件中 ` com.test=${JAVA_HOME:/java/home}`\n在yml文件中` com.test: ${JAVA_HOME:/java/home}`\n\n',
        NULL, 1, 1, 340, 0, 0, 2, '2019-12-04 02:36:54', NULL, 1, 0),
       (1293, '教你动手写一个刷课脚本',
        '动手写自动挂机刷课脚本\n\n\n失踪人口又回来了，🤣🤣\n\n\n\n受疫情影响，所有的课程都是以网课形式存在，后又要自己选一个通识网课，这无疑让本就悲惨的认识雪上加霜，于是，我研究了一下智慧树的部分api接口，编写了一个较简单的挂机脚本。此篇文章以做记录。\n附上shi''p视频播放界面的api请求列表\n其中\n\n getLoginUserInfo 为get请求，\n\n附带参数    dateFormate： 时间戳，\n\n返回的数据中包含登录者的姓名和一个  uuid , 这个uuid 比......',
        '# 动手写自动挂机刷课脚本\n\n> 失踪人口又回来了，🤣🤣\n\n受疫情影响，所有的课程都是以网课形式存在，后又要自己选一个通识网课，这无疑让本就悲惨的认识雪上加霜，于是，我研究了一下智慧树的部分api接口，编写了一个较简单的挂机脚本。此篇文章以做记录。\n\n附上shi''p视频播放界面的api请求列表\n![api request](https://oss.celess.cn/web/QQ%E5%9B%BE%E7%89%8720200317004825.png)\n\n其中\n- ` getLoginUserInfo` 为get请求，\n  - 附带参数    **dateFormate**： 时间戳，\n - 返回的数据中包含登录者的姓名和一个 ` uuid` , 这个uuid 比较重要后面的几个请求都需要以它作为参数传递\n\n-  ` videolist` 为post请求，\n - 携带参数 **recruitAndCourseId**: 这个可以从地址栏中获取，目测是对课程号处理过后得到的值，**uuid**:上面提到过的uuid ，**dateFormate**:时间戳，\n - 返回数据为播放列表的信息  包含视频id  课程id  课程名称等等，本次用到的是其中的` name` `lessonId`  `id`内容。处理过滤后存储在一个map中\n\n- ` loadVideoPointerInfo` 为post请求\n - 携带参数 **lessonId**: 当前播放内容的小节课程id  非总的课程id ，**lessonVideoId**: 播放的视频id （* 此字段非必带，仅播放的小节课程处于小课程时才需要用以区分 例如当2.3作为单独一小节时 不需要此字段，若2.3 有子课程时，2.3.1 2.3.2....均需要该字段 *）**uuid**: 第一个api中的，**dateFormate**: 时间戳\n - 返回数据 返回此视频所有的答题时间点，即多少分多少秒会出现答题\n \n- `lessonPopupExam` 为post请求\n   - 携带参数 **lessonId**: 同上 **lessonVideoId**: 同上    **time**: 答题的时间点 可由上一个api返回数据中获取 **uuid**: 同上 **dateFormate**:同上\n   -  返回数据 该数据返回问题及答案 选项列表中有个result字段 该字段为1则说明该选项时答案之一。\n   \n\n基于以上四个api在对页面的标签进行选择，通过jquery对页面进行操作 模拟出用户的点击操作，api请求我没有过多的进行分析和使用，因为这个数据是直接发送回服务端，请求太多容易出纰漏，被发现出端倪，所以脚本中的网络请求则能省就省。\n于是有了如下的第一个版本\n代码质量有点低，各位就当看个乐吧，望各位轻点喷。\n\n``` typeScript\ndeclare let $;\nlet courseId: string;\nlet lessonVideoId: number;\nlet needVideoId: boolean;\nlet uid = null;\nlet map = new Map();\nlet runTime = 0;\nlet autoAnswerTimes = 0;\n\n// @ts-ignore\n\nclass Time {\n    private _hour: number;\n    private _minute: number;\n    private _second: number;\n\n    constructor(time: string) {\n        this._hour = parseInt(time.split(`:`)[0]);\n        this._minute = parseInt(time.split(`:`)[1]);\n        this._second = parseInt(time.split(`:`)[2]);\n    }\n\n    set hour(value: number) {\n        this._hour = value;\n    }\n\n    set minute(value: number) {\n        this._minute = value;\n    }\n\n    set second(value: number) {\n        this._second = value;\n    }\n\n    tosec() {\n        return this._hour * 3600 + this._minute * 60 + this._second;\n    }\n}\n\nclass Course {\n    name: string;\n    lessonId: number;\n    lessonVideoId: number;\n    isSmallLesson: boolean;\n\n\n    constructor(name: string, lessonId: number, lessonVideoId: number, isSmallLesson: boolean) {\n        this.name = name;\n        this.lessonId = lessonId;\n        this.lessonVideoId = lessonVideoId ? lessonVideoId : null;\n        this.isSmallLesson = isSmallLesson;\n    }\n}\n\n\nlet isPlaying: boolean = true;\nlet needAnswer: boolean = false;\n\nlet currentTime: Time;\nlet totalTime: Time;\nlet currentPlayIndex = -1;\n\ngetUuid();\n\nsetInterval(function () {\n    console.clear();\n    getCurrentCourseId();\n\n    updateRecTime();\n    checkNeedAnswer();\n    preporeInfo();\n    if (needAnswer) {\n        answerT();\n    }\n    if (currentTime.tosec() === totalTime.tosec()) {\n        // 点击播放下一节\n        clickNextNode();\n        getCourseId();\n    } else {\n        if (!isPlaying) {\n            clickScreenTocontinue();\n        }\n    }\n    record(8);\n}, 8000);\n\nfunction record(space: number) {\n    runTime += space;\n    console.log(`已使用：${runTime}秒\\n已答题：${autoAnswerTimes}次`)\n}\n\nfunction getCourseId() {\n\n    // 获取courseId\n    $.ajax({\n        type: "POST",\n        url: "https://studyservice.zhihuishu.com/learning/videolist",\n        data: {\n            recruitAndCourseId: GetQueryString(''recruitAndCourseId''),\n            uuid: uid,\n            dateFormate: Date.parse(Date())\n        },\n        contentType: "application/x-www-form-urlencoded",\n        xhrFields: {\n            withCredentials: true\n        },\n        success: function (data1) {\n            // 获取学习进度\n            data1.data.videoChapterDtos.forEach(item => {\n                item.videoLessons.forEach(item2 => {\n                    if (item2.ishaveChildrenLesson) {\n                        item2.videoSmallLessons.forEach(item3 => {\n                            map.set(item3.id, new Course(item3.name, item3.lessonId, item3.id, true))\n                        })\n                    } else {\n                        map.set(item2.id, new Course(item2.name, item2.id, item2.id, false))\n                    }\n                })\n            });\n        }\n    })\n}\n\nfunction getCurrentCourseId() {\n    let text = $(".list .current_play").text();\n    map.forEach(value => {\n        if (text.indexOf(value.name) != -1) {\n            courseId = value.lessonId;\n            lessonVideoId = value.lessonVideoId;\n            needVideoId = value.isSmallLesson;\n            return;\n        }\n    });\n    // console.log(`courseId=${courseId}`)\n}\n\n\nfunction getUuid() {\n    $.ajax({\n        type: "GET",\n        url: "https://onlineservice.zhihuishu.com/login/getLoginUserInfo",\n        xhrFields: {\n            withCredentials: true\n        },\n        success: function (data) {\n            if (data.result == null || data.result.uuid == null) {\n                return\n            }\n            uid = data.result.uuid;\n            getCourseId();\n        }\n    });\n}\n\nlet lastTimePlaying = new Time("00:00:00");\n\n/**\n * 记录播放时间和总时间\n */\nfunction updateRecTime() {\n    currentTime = new Time($(".currentTime").text());\n    if (lastTimePlaying) {\n        isPlaying = lastTimePlaying.tosec() != currentTime.tosec();\n        console.log(isPlaying ? "播放中" : "暂停中");\n    }\n    totalTime = new Time($(".duration").text());\n    // console.log(`lastTimePlaying : ${lastTimePlaying.tosec()} \\ncurrentTime: ${currentTime.tosec()} \\ntotalTime:  ${totalTime.tosec()}`);\n    lastTimePlaying = currentTime;\n\n}\n\nlet array;\n\nfunction preporeInfo() {\n    let list = $(".list .clearfix, .video").toArray();\n    array = Array();\n    list.forEach((item, index) => {\n        if (!$(item).hasClass(''video'')) {\n            // 删除title\n            return;\n        }\n        array.push(item);\n\n    });\n    array.forEach((item, index) => {\n        if ($(item).hasClass(''current_play'')) {\n            currentPlayIndex = index;\n        }\n    });\n}\n\n/**\n * 点击播放下一节\n */\nfunction clickNextNode() {\n    // 当前正在播放的item\n    preporeInfo();\n\n    $(array[currentPlayIndex + 1]).click();\n    clickScreenTocontinue();\n}\n\n\n/**\n * 检查是否需要做题\n */\nfunction checkNeedAnswer() {\n    $(".el-dialog__header h4").toArray().forEach(item => {\n        //console.log($(item).text());\n        if ($(item).text() === "弹题测验") {\n            needAnswer = true;\n        }\n    });\n}\n\n/**\n * 答题\n * 答案在api中\n */\nfunction answerT() {\n    let dataTime: string = null;\n    let request: {\n        lessonId: string,\n        uuid: string,\n        dateFormate: number,\n        lessonVideoId?: number;\n    } = {\n        lessonId: courseId,\n        uuid: uid,\n        dateFormate: Date.parse(Date()),\n    };\n    if (needVideoId) {\n        request.lessonVideoId = lessonVideoId;\n\n    }\n    // 获取暂停时间片\n    $.ajax({\n        type: "POST",\n        url: "https://studyservice.zhihuishu.com/popupAnswer/loadVideoPointerInfo",\n        data: request,\n        contentType: "application/x-www-form-urlencoded",\n        xhrFields: {\n            withCredentials: true\n        },\n        success: function (data) {\n            dataTime = data.data.questionPoint[0].timers;\n            if (dataTime == null) {\n                console.log("获取答题时间点失败");\n                return\n            }\n            let req: {\n                lessonId: string\n                time: string\n                uuid: string,\n                dateFormate: number,\n                lessonVideoId?: number\n            } = {\n                lessonId: courseId,\n                time: dataTime,\n                uuid: uid,\n                dateFormate: Date.parse(Date()),\n            };\n            if (needVideoId) {\n                req.lessonVideoId = lessonVideoId;\n\n            }\n            // 获取答案\n            $.ajax({\n                type: "POST",\n                url: "https://studyservice.zhihuishu.com/popupAnswer/lessonPopupExam",\n                data: req,\n                contentType: "application/x-www-form-urlencoded",\n                xhrFields: {\n                    withCredentials: true\n                },\n                success: function (data) {\n                    // 题目选项\n                    let answers = Array();\n                    let t = data.data.lessonTestQuestionUseInterfaceDtos[0].testQuestion.questionOptions;\n                    let ans = "";\n                    t.forEach((item, index) => {\n                        if (item.result === "1") {\n                            ans += (item.content + "\\t");\n                            answers.push(index);\n                        }\n                    });\n                    console.log("答案：" + ans);\n                    clickAnswer(answers);\n                }\n            })\n        }\n    });\n}\n\n\n// 点击选项\nfunction clickAnswer(answers) {\n    let liArray = $(".el-dialog .el-dialog__body .topic-list .topic-item").toArray();\n    answers.forEach((item) => {\n        // 点击选项\n        if (!$($(liArray[item])[0].lastChild).hasClass(''active'')) {\n            $(liArray[item]).click()\n        }\n    });\n    // 点击关闭\n    $(".el-dialog .el-dialog__footer .btn").toArray().forEach(item => {\n        if ($(item)[0].className === "btn") {\n            $(item).click()\n        }\n    });\n    autoAnswerTimes++;\n    needAnswer = false;\n    // 点击播放\n    clickScreenTocontinue();\n}\n\n// 点击播放\nfunction clickScreenTocontinue() {\n    $(".videoArea").click();\n}\n\n\nfunction GetQueryString(name) {\n    let query = window.location.href.substr(window.location.href.indexOf(''?'') + 1);\n    let vars = query.split("&");\n    for (let i = 0; i < vars.length; i++) {\n        let pair = vars[i].split("=");\n        if (pair[0] == name) {\n            return pair[1];\n        }\n    }\n    return;\n}\n\n```\n',
        NULL, 1, 1, 236, 0, 0, 5, '2020-03-16 17:22:35', NULL, 1, 0);


INSERT INTO `article_tag` (`at_id`, `a_id`, `t_id`)
VALUES (1, 3, 7),
       (2, 5, 8),
       (3, 5, 9),
       (4, 10, 9),
       (5, 8, 10),
       (6, 993, 10),
       (7, 10, 11),
       (8, 12, 12),
       (9, 12, 13),
       (10, 993, 13),
       (11, 1292, 13),
       (12, 47, 14),
       (13, 47, 15),
       (14, 1018, 16),
       (15, 1286, 17),
       (16, 1286, 18),
       (17, 1287, 19),
       (18, 1287, 20),
       (19, 1288, 21),
       (20, 1289, 22),
       (21, 1290, 23),
       (22, 1291, 24),
       (23, 1292, 25),
       (24, 1293, 26),
       (25, 1293, 27);


INSERT INTO `comment` (`co_id`, `co_page_path`, `co_content`, `co_date`, `co_status`, `co_pid`, `co_from_author_id`,
                       `co_to_author_id`)
VALUES (1, '/a', 'test comment', '2020-05-22 23:57:17', 0, -1, 1, -1),
       (2, '/a', 'test comment', '2020-05-22 23:57:31', 0, -1, 1, 1);



INSERT INTO `links` (`l_id`, `l_name`, `l_is_open`, `l_url`, `l_icon_path`, `l_desc`, `is_delete`)
VALUES (1, '程序猿张先生', 1, 'https://zhyocean.cn', '', '', 0),
       (2, '陈晓雷的技术博客', 1, 'http://www.csxll.top', '', '', 0),
       (3, '罗炜杰个人博客', 1, 'http://www.lwjppz.cn', '', '', 0),
       (4, '强子博客', 1, 'https://www.lqnb.xyz', '', '', 0),
       (5, '原创博客技术联盟', 1, 'http://techblog.pub/', '', '', 0),
       (6, 'WarlockMT', 1, 'https://www.warlock.live/', '', '', 0);



INSERT INTO `visitor` (`v_id`, `v_date`, `v_ip`, `v_user_agent`, `is_delete`)
VALUES (1, '2019-04-16 15:13:47', '183.94.121.106',
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36',
        0),
       (17, '2019-04-16 15:44:46', '183.94.121.106',
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36',
        0),
       (18, '2019-04-16 15:45:36', '183.94.121.106',
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36',
        0),
       (19, '2019-04-16 17:28:31', '111.206.221.27',
        'Mozilla/5.0 (compatible; Baiduspider-render/2.0; +http://www.baidu.com/search/spider.html)', 0),
       (20, '2019-04-17 01:24:55', '111.206.221.35',
        'Mozilla/5.0 (compatible; Baiduspider-render/2.0; +http://www.baidu.com/search/spider.html)', 0),
       (21, '2019-04-17 09:20:06', '111.206.198.72',
        'Mozilla/5.0 (compatible; Baiduspider-render/2.0; +http://www.baidu.com/search/spider.html)', 0),
       (22, '2020-07-10 00:45:30', '220.168.55.117',
        'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
        0),
       (23, '2020-07-10 04:45:11', '220.168.55.117',
        'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
        0),
       (24, '2020-07-10 09:04:45', '183.67.60.166',
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36',
        0),
       (25, '2020-07-10 09:08:58', '120.208.182.220',
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36',
        0);

INSERT INTO `web_update` (`wu_id`, `wu_info`, `wu_time`, `is_delete`)
VALUES (1, '1.新增网站更新接口api \n2.新增友链api \n3.优化了文章操作api的逻辑', '2019-05-12 09:10:35', 0),
       (2, '新增更新和友链页面', '2019-05-12 15:58:13', 0),
       (3, '站点接入https', '2019-05-13 00:16:33', 0),
       (4, '1.弃用Amaze ~ 妹子 UI ，拥抱angular和ant design\n2.移动端适配', '2019-05-17 15:37:04', 0),
       (5, '1.优化了文章页面的细节\n2.发布文章时对文章进行暂存（仍待优化）\n3.修改ant design的引入方式', '2019-05-21 15:32:21', 0),
       (6, '新增回复功能，文章的评论和留言都可以回复啦~~', '2019-05-25 12:42:18', 0),
       (7, '偷偷的放置了一个404页面', '2019-05-30 11:53:28', 0),
       (8, '修改"分类","标签"页面的布局样式', '2019-06-27 09:17:00', 0),
       (9, '1.调整后端接口\n2.修改后端的dao层逻辑', '2019-07-04 14:20:30', 0),
       (10, '1. 新增邮箱验证\n2. 新增找回密码', '2019-07-13 03:13:48', 0),
       (11, '1. 前台展示页面重构，规范化代码\n2. 修改部分细节\n3. 修复无法留言/评论的bug', '2019-07-20 03:18:12', 0),
       (12, '1.修改登录页面的显示方式\n2.移除fontawesome', '2019-07-28 12:04:05', 0),
       (13, '新增用户管理', '2019-09-07 14:41:18', 0),
       (14, '增加对emoji的支持,评论，留言，文章的标题和文章内容均支持emoji表情', '2019-09-17 10:27:58', 0),
       (15, '新增toc目录，并适时显示', '2019-10-15 03:13:47', 0),
       (16, '登陆处理过程变更，登陆时长修改至5天', '2019-11-22 11:39:03', 0),
       (17, '界面改版v2.0', '2020-04-06 11:00:53', 0);

