论文图表自动编译


前情提要

老板特别喜欢在论文里面加图加表...加到最后整个论文堆了二三十张图, 一张一张手动处理我显然不太能受得了, 所以就写了个脚本来批量生成图表.

我的图表主要分为两类, 一类是柱状图, 这个主要是利用>$LaTeX$<的pgfplot包来画的, 为了批量修改图标样式, 所以利用\input给每张图配置了统一的模板; 另一类就是Visio了, 很多复杂的样式单纯使用>$LaTeX$<根本画不出来, 或者画起来很麻烦. 不过说起来, 使用>$LaTeX$<画的柱状图比起Excel生成的不知道好看到哪里去了(x

正片开始

我要实现的主要目标是, 能够自动识别我的源文件, 然后只要简单的一个make就能编译出我想要的图表.

对于>$LaTeX$<问题不大, 因为texlive本来就是命令行工具, 但是对于Visio来说就很麻烦了, 不过在万能的github上搜了搜发现了这个, 用了用发现这个本质上还是调起Visio然后自动另存为PDF= =它并不能脱离Visio单独使用
不过好在我的环境是Windows Linux Subsystem, 所以调起win32原生应用并不是一个太大的问题.

Makefile模板

# Common Command Alias
ECHO := @echo -e
MKDIR := mkdir -p
CP := cp
RMF := rm -f
RMD := rm -rf

# Function Command Alias
LATEXMK := latexmk -interaction=nonstopmode -file-line-error --outdir=output
VSDX2PDF := ../utils/OfficeToPDF.exe
PDFCROP := pdfcrop
PDF2EPS := pdftops -eps
LATEXINDENT := latexindent

主要是定义了一些常用的变量, 方便调用和修改.

  • LATEXMK是LateX的编译控制器, 会默认调用pdflatex去编译, 当然你可以通过参数选择你实际需要的编译器;
  • VSDX2PDF是之前介绍的, 可以将Visio自动转成PDF的工具;
  • PDFCROP用于自动裁剪白边, 无论是直接编译出的PDF还是VISIO转成的PDF都有白边, 手动去白边很累的;
  • PDF2EPS用于将PDF的图片转成EPS;
  • LATEXINDENT用于格式化tex文件(可能是我的强迫症

编译LATEX图表

include ../../mk/template.mk

OUTPUTDIR := output

SOURCES := .
TEXFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.tex)))
TARGETS := $(TEXFILES:.tex=)

TEMPLATES := template
TEMPLATEFILES := $(foreach dir,$(SOURCES),$(wildcard $(dir)/$(TEMPLATES)/*.tex))

.PHONY: all install clean clean_aux $(TARGETS)
.PRECIOUS: $(OUTPUTDIR)/%.pdf $(OUTPUTDIR)/%.dvi $(OUTPUTDIR)/%.eps

all: $(TARGETS)
    $(ECHO) "\033[36mBuilding $@...\033[0m"

$(OUTPUTDIR):
    $(MKDIR) $(OUTPUTDIR)

$(TARGETS): %: $(OUTPUTDIR)/%.pdf $(OUTPUTDIR)/%.dvi $(OUTPUTDIR)/%.eps | $(OUTPUTDIR)
    $(ECHO) "\033[36mBuilding $@...\033[0m"
    @$(LATEXMK) -c $@.tex >> /dev/null 2>&1

$(OUTPUTDIR)/%.pdf: %.tex $(TEMPLATEFILES) | $(OUTPUTDIR)
    $(ECHO) "\033[36mBuilding $@...\033[0m"
    $(LATEXMK) -pdf $<

$(OUTPUTDIR)/%.dvi: %.tex $(TEMPLATEFILES) | $(OUTPUTDIR)
    $(ECHO) "\033[36mBuilding $@...\033[0m"
    $(LATEXMK) -dvi $<

$(OUTPUTDIR)/%.eps: $(OUTPUTDIR)/%.pdf | $(OUTPUTDIR)
    $(ECHO) "\033[36mBuilding $@...\033[0m"
    $(PDFCROP) $(OUTPUTDIR)/$(*F).pdf $(OUTPUTDIR)/$(*F).pdf
    $(PDF2EPS) $(OUTPUTDIR)/$(*F).pdf

install: $(TARGETS:%=../pdf/%.pdf) $(TARGETS:%=../dvi/%.dvi) $(TARGETS:%=../eps/%.eps)
    $(ECHO) "\033[36mInstalling Files...\033[0m"

$(TARGETS:%=../pdf/%.pdf): ../pdf/%.pdf: $(OUTPUTDIR)/%.pdf
    $(CP) $< $@

$(TARGETS:%=../dvi/%.dvi): ../dvi/%.dvi: $(OUTPUTDIR)/%.dvi
    $(CP) $< $@

$(TARGETS:%=../eps/%.eps): ../eps/%.eps: $(OUTPUTDIR)/%.eps
    $(CP) $< $@

clean_aux:
    $(ECHO) "\033[36mCleaning aux files...\033[0m"
    $(RMF) \
        $(OUTPUTDIR)/*.fdb_latexmk \
        $(OUTPUTDIR)/*.aux \
        $(OUTPUTDIR)/*.fls \
        $(OUTPUTDIR)/*.log \
        $(OUTPUTDIR)/*.synctex.gz

clean:
    $(ECHO) "\033[36mCleaning all files...\033[0m"
    $(RMD) $(OUTPUTDIR)

流程主要是这样子的:

  1. 扫描SOURCES目录下的.tex文件获得已有的TARGETS
  2. 扫描TEMPLATES目录下.tex文件获得已有的TEMPLATEFILES
  3. TARGETS目标转换为OUTPUTDIR/TARGET.pdf触发实际编译
  4. $(OUTPUTDIR)/%.pdf同时依赖源文件和TEMPLATEFILES, 实现源文件/模板修改后触发所有图表自动更新
  5. $(TARGETS:%=../pdf/%.pdf)的目标是为了将OUTPUTDIR下的输出文件安装到父目录的对应文件夹下

这样就可以实现自动识别目录下的源文件并自动编译了, 同时修改文件可以及时且不遗漏的触发增量编译.

编译VISIO图表

include ../../mk/template.mk

OUTPUTDIR := output

SOURCES := .
VSDXFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.vsdx)))
TARGETS := $(VSDXFILES:.vsdx=)

.PHONY: all install clean $(TARGETS)
.PRECIOUS: $(OUTPUTDIR)/%.pdf $(OUTPUTDIR)/%.eps

all: $(TARGETS)
    $(ECHO) "\033[36mBuilding $@...\033[0m"

$(OUTPUTDIR):
    $(MKDIR) $(OUTPUTDIR)

$(TARGETS): %: $(OUTPUTDIR)/%.pdf $(OUTPUTDIR)/%.eps | $(OUTPUTDIR)
    $(ECHO) "\033[36mBuilding $@...\033[0m"

$(OUTPUTDIR)/%.pdf: %.vsdx | $(OUTPUTDIR)
    $(VSDX2PDF) $< $@

$(OUTPUTDIR)/%.eps: $(OUTPUTDIR)/%.pdf | $(OUTPUTDIR)
    $(PDFCROP) $< $<
    $(PDF2EPS) $<

install: $(TARGETS:%=../pdf/%.pdf) $(TARGETS:%=../eps/%.eps)
    $(ECHO) "\033[36mInstalling Files...\033[0m"

$(TARGETS:%=../pdf/%.pdf): ../pdf/%.pdf: $(OUTPUTDIR)/%.pdf
    $(CP) $< $@

$(TARGETS:%=../eps/%.eps): ../eps/%.eps: $(OUTPUTDIR)/%.eps
    $(CP) $< $@

clean:
    $(ECHO) "\033[36mCleaning all files...\033[0m"
    $(RMD) $(OUTPUTDIR)

VISIO的编译流程与LATEX的类似, 只不过没有模板和DVI文件.

图表根目录

include ../mk/template.mk

SOURCES := BarChart DiscTree Visio 
OUTPUTS := pdf dvi eps

.PHONY: all clean $(SOURCES) $(SOURCES:%=clean_%)

all: $(SOURCES)
    $(ECHO) "\033[36mBuilding $@...\033[0m"

$(SOURCES): | $(OUTPUTS)
    $(ECHO) "\033[36mBuilding $@...\033[0m"
    make -C $@ all 
    make -C $@ install

$(OUTPUTS):
    $(MKDIR) $@

clean: $(SOURCES:%=clean_%)
    $(ECHO) "\033[36mCleaning all files...\033[0m"
    $(RMD) pdf dvi eps

$(SOURCES:%=clean_%):
    make -C $(@:clean_%=%) clean

这里的$(SOURCES:%=clean_%)是为了能使用类似于make clean_BarChart来单独清理BarChart子目录.

将各个子目录编译出的文件合并到根目录上并没有在根目录上来控制而是在各个子目录里install到根目录上来. 因为在根目录上很难做到这一点, 因为在编译开始前我对每个子目录到底有哪些目标文件是未知的, 只有在实际的编译流程开始以后才能知道, make(gnu)提供的控制指令有着很大的局限性, 如果用python编写类似的脚本是可以做到类似的需求的.

总结与思考

虽然说调试整个一套编译脚本花费了一定的时间(还包括论文的自动编译以及其他的一些, 不过都很类似), 但是对于整体效率的提升是非常巨大的.

在这之前, 对于LaTeX图表我需要单独每个编译出PDF, 对于Visio我需要每一个文件都点开然后另存为PDF. 同时, 对于裁白边, 都是通过Adobe Illustrator CC打开PDF, 然后全选内容导出资源为PDF, 非常累, 还可能碰到字体的问题. 此外, 还要再用Adobe Acrobat DC打开裁掉白边的PDF将其另存为eps.

图表比较少, 修改不大的时候, 这一套流程下来还可以接受, 但是图表一多, 简直要疯. 然后, 现在只要打开WSL然后make, 只要花上个厕所的时间, 就可以一键自动编译图表及论文了w

思考

  • WSL能够在运行Linux程序的前提下运行win32原生程序是一大优势
  • 以前编译OpenCV以及ROS的时候不知道为什么还要用cmakepython来控制编译, 现在深入用过一遍make以后觉得主要还是make存在着比较大的局限性

最后:

CLI大法好!

一些容易混淆的时间复杂度分析


前言

时间复杂度分析简单来说大概分为这么几种情况:

  • 一层循环就是一层O(N), 循环套循环需要相乘
  • 如果是树状结构, 比如说有分治(二叉搜索, 快排)的时候, 那么就是O(logN)
  • 如果有递归:

    • 如果是尾递归, 那么其实是和循环等价的
    • 如果是树状递归, 比如说斐波拉契数列, 那么就是O(2^N), 确切的说是O(branches^depth)

Example1

Suppose we had an algorithm that took in an array of strings, sorted each string, and then sorted the full array. What would the runtime be?

Let's define new terms-and use names that are logical.

  • Let s be the length of the longest string.
  • Let a be the length of the array.

Now we can work through this in parts:

  • Sorting each string is O(s log s).
  • We have to do this for every string (and there are a strings), so that's O(a*s log s).
  • Now we have to sort all the strings. There are a strings, so you'll may be inclined to say that this takes O(a log a) time. This is what most candidates would say. You should also take into account that you need to compare the strings. Each string comparison takes O(s) time. There are O(a log a) comparisons, therefore this will take O(a*s log a) time.

If you add up these two parts, you get 0(a*s (log a + log s)).

这个很容易误认为是O(N^2 logN), 一方面是string的长度和array的长度并不一样, 另外一方面是在array排序的时候很容易忽略字符串比较还需要花费时间.

Example2

The following code prints all Fibonacci numbers from 0 to n. What is its time complexity?
void allFib(int n) {
    for (int i= 0; i < n; i++) {
        System.out.println(i + ": "+ fib(i));
    }
}

int fib(int n) {
    if (n <= 0) return 0;
    else if (n == 1) return 1;
    return fib(n - 1) + fib(n - 2);
} 

Instead, let's walk through each call.

  • fib(!) -> 2^1 steps
  • fib(2) -> 2^2 steps
  • fib(3) -> 2^3 steps
  • fib(4) -> 2^4 steps
  • fib(n) -> 2^n steps

Therefore, the total amount of work is: 2^1 + 2^2 + 2^3 + 2^4 + ... + 2^N = 2^(N+1)

这个很容易误认为是O(N*2^N), 主要是fib(n)这里的n每次循环都是变化的...很容易被误导

总结

时间复杂度的求法并不是很难, 基本就那么几种情况, 需要注意的是处理字符串时不能忽略掉字符串的长度.

Reference

Cracking the Coding Interview 6th Edition


基于注解的简单权限控制实现


前情提要

这个其实是很早的时候在做本科毕设的时候实现的, 当时因为权限结构简单, 懒得上一个类似于Spring Security这么重的框架, 以及主要是想尝试一下自己实现注解就简单做了一下基于方法注解的权限控制.

设计思路

因为整个系统只有管理员/登录用户/未登录用户三种不同的角色, 并同时考虑到有些接口(比如修改/删除数据)只有作者才能访问, 所以一共设计了@AdminOnly, @CreatorOnly@LoginOnly三种不同的注解.

注解部分实现

注解的实现代码很简单, 主要是注意需要标记@Retention(RetentionPolicy.RUNTIME)才能在运行时通过反射访问到这个注解:

package weplay.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by sl on 17/4/18.
 * check the accessor is admin
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface AdminOnly {
    /**
     * exclude method's name when mark class
     */
    String[] exclude() default {};
    /**
     * indicate the check is valid
     */
    boolean isValid() default true;
}

因为需要@AdminOnly的方法很多, 为了简单所以除了方法注解以外也设置了类注解, 添加exclude属性以排除某一个类中不需要使用该注解过滤的方法, isValid是为了临时禁用该注解, 方便调试.

package weplay.annotation;

import weplay.enums.EntityType;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by sl on 17/4/18.
 * check the accessor is creator
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface CreatorOnly {
    /**
     * indicate EntityType of Entity
     */
    EntityType type();

    /**
     * indicate the position of EntityId in URI
     */
    int position();

    /**
     * indicate the check is valid
     */
    boolean isValid() default true;

    /**
     * indicate admin can access this handler
     */
    boolean allowAdmin() default true;
}

这里的设计比较纠结, 因为我需要验证当前用户对当前访问的数据条目有没有权限, 所以需要使用type来获得访问数据条目的业务类型, 以及需要通过position来知道具体的数据条目的id, 这样我才能查询到当前访问的数据条目的创建者, 从而判断是否允许访问, allowAdmin表示除了创建者管理员是否也能访问该接口.

package weplay.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by sl on 17/4/22.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface LoginOnly {
    /**
     * indicate the check is valid
     */
    boolean isValid() default true;
}

这个表示是否只有登录用户才能访问该接口.

注解解析实现

注解的解析放在SpringMVC的Interceptor中, 因为HandlerInterceptor是单例需要可重入, 所以使用了HandlerInterceptor封装RequestContextHolder.currentRequestAttributes()在当前请求的上下文中临时保存值.
fetchInfo()用于从请求中读取后续解析需要的数据, processAdminOnly(), processCreateOnly()processLoginOnly()分别解析三个不同的注解, 这里可以考虑做一个公共接口, 然后不同的注解做不同的实现.
preHandle()中, 通过handlerMethod.getBeanType().getAnnotations()handlerMethod.getMethod().getAnnotations()分别获取类注解和方法注解, 这里使用了Spring提供的HandlerMethod类, 如果直接使用反射的话, 可以通过class.getAnnotations()class.getMethod(name, parameterTypes).getAnnotations()分别获取到类注解和方法注解.
获取到注解对象以后, 可以通过直接之前定义的方法来获取到注解配置时设置的值, 比如adminOnly.exclude(), 然后根据业务需要判断是否放行还是抛出异常中止请求响应.

package weplay.aop;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import weplay.annotation.AdminOnly;
import weplay.annotation.CreatorOnly;
import weplay.annotation.LoginOnly;
import weplay.enums.AccountState;
import weplay.enums.AccountType;
import weplay.enums.EntityType;
import weplay.enums.ErrorCode;
import weplay.exception.CustomException;
import weplay.helperBean.viewBean.UserInfo;
import weplay.resource.Settings;
import weplay.service.IActivityService;
import weplay.service.IAuthService;
import weplay.service.IEntityService;
import weplay.service.IUserService;
import weplay.utils.RequestContextUtil;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Created by sl on 17/4/18.
 */
public class AuthorityInterceptor implements HandlerInterceptor {
    private IAuthService authService;
    private IUserService userService;
    private IActivityService activityService;
    private IEntityService entityService;

    /**
     * fetch basic info from request
     */
    private void fetchInfo(HttpServletRequest req) throws CustomException {
        String uri = req.getRequestURI();
        String token = req.getHeader("token");
        Integer currentUid;
        AccountType accountType;
        UserInfo userInfo;
        if (token != null) {
            currentUid = authService.readUidByToken(token);
            if (currentUid == Settings.ROOT_ADMIN_ID) {
                accountType = AccountType.ADMIN;
            } else {
                accountType = AccountType.NORMAL;
            }
            userInfo = userService.readUserInfoByUid(currentUid);
        } else {
            token = "";
            currentUid = -1;
            accountType = AccountType.TOURISM;
            userInfo = new UserInfo();
        }
        // save to RequestContextUtil
        RequestContextUtil.setAttribute("uri", uri);
        RequestContextUtil.setAttribute("token", token);
        RequestContextUtil.setAttribute("uid", currentUid);
        RequestContextUtil.setAttribute("accountType", accountType);
        RequestContextUtil.setAttribute("userInfo", userInfo);
    }

    /**
     * process @AdminOnly
     */
    private void processAdminOnly(Annotation annotation, HandlerMethod handlerMethod) throws CustomException {
        AccountType accountType = (AccountType) RequestContextUtil.getAttribute("accountType");

        if (annotation instanceof weplay.annotation.AdminOnly) {
            AdminOnly adminOnly = (AdminOnly) annotation;
            // check isValid
            if (!adminOnly.isValid()) {
                return;
            }
            // check exclude method
            for (String s : adminOnly.exclude()) {
                if (s.equals(handlerMethod.getMethod().getName())) {
                    return;
                }
            }
            // check current user is admin
            if (accountType != AccountType.ADMIN) {
                throw new CustomException(ErrorCode.NO_PERMISSION, "ADMIN ONLY");
            }
        }
    }

    /**
     * process @CreateOnly
     */
    private void processCreateOnly(Annotation annotation) throws CustomException {
        String uri = (String) RequestContextUtil.getAttribute("uri");
        Integer currentUid = (Integer) RequestContextUtil.getAttribute("uid");
        AccountType accountType = (AccountType) RequestContextUtil.getAttribute("accountType");

        if (annotation instanceof CreatorOnly) {
            CreatorOnly creatorOnly = (CreatorOnly) annotation;

            // read EntityType and EntityId
            String[] uriPartition = uri.split("/");
            String entityIdStr = uriPartition[creatorOnly.position()];
            Integer entityId;
            if (entityIdStr.contains(".")) {
                entityId = Integer.valueOf(entityIdStr.split("\\.")[0]);
            } else {
                entityId = Integer.valueOf(entityIdStr);
            }
            EntityType entityType = creatorOnly.type();

            // read CreatorId
            Integer creatorId = -1;
            switch (entityType) {
                case ACTIVITY:
                    creatorId = activityService.readActivityById(entityId).getCreator();
                    break;
                case NOTIFICATION:
                    creatorId = entityService.readNotificationById(entityId).getCreator();
                    break;
                case PHOTO:
                    creatorId = entityService.readPhotoById(entityId).getCreator();
                    break;
            }

            // update accountType
            if (currentUid.equals(creatorId)) {
                accountType = AccountType.CREATOR;
                RequestContextUtil.setAttribute("accountType", accountType);
            }

            // check current User is creator or admin if admin allow access
            if (creatorOnly.isValid() && accountType != AccountType.CREATOR &&
                    !(creatorOnly.allowAdmin() && accountType == AccountType.ADMIN)) {
                throw new CustomException(ErrorCode.NO_PERMISSION, "CREATOR ONLY");
            }
        }
    }

    /**
     * process @CreateOnly
     */
    private void processLoginOnly(Annotation annotation) throws CustomException {
        AccountType accountType = (AccountType) RequestContextUtil.getAttribute("accountType");
        UserInfo userInfo = (UserInfo) RequestContextUtil.getAttribute("userInfo");

        if (annotation instanceof LoginOnly) {
            LoginOnly loginOnly = (LoginOnly) annotation;
            
            // check isValid
            if (!loginOnly.isValid()) {
                return;
            }
            if (accountType == AccountType.TOURISM) {
                throw new CustomException(ErrorCode.NO_PERMISSION, "LOGIN USER ONLY");
            }
        }
    }

    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object o) throws Exception {
        // static resource handler
        if (!(o instanceof HandlerMethod)) {
            return true;
        }
        // convert handler into current Type
        HandlerMethod handlerMethod = (HandlerMethod) o;
        // fetch basic info
        this.fetchInfo(req);
        // process Annotations
        List<Annotation> annotations = new ArrayList<Annotation>();
        annotations.addAll(Arrays.asList(handlerMethod.getBeanType().getAnnotations()));
        annotations.addAll(Arrays.asList(handlerMethod.getMethod().getAnnotations()));
        for (Annotation a : annotations) {
            this.processAdminOnly(a, handlerMethod);
            this.processCreateOnly(a);
            this.processLoginOnly(a);
        }
        return true;
    }
}
package weplay.utils;

import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Created by sl on 17/4/22.
 * Utils for wrapping RequestContextHolder
 */
public class RequestContextUtil {
    /**
     * prefix for user custom Attribute
     */
    private static String prefix = "u_";

    /**
     * set Attribute to Context
     */
    public static void setAttribute(String key, Object obj) {
        RequestContextHolder.currentRequestAttributes().setAttribute(prefix + key, obj, RequestAttributes.SCOPE_REQUEST);
    }

    /**
     * get Attribute from Context
     */
    public static Object getAttribute(String key) {
        return RequestContextHolder.currentRequestAttributes().getAttribute(prefix + key, RequestAttributes.SCOPE_REQUEST);
    }

    /**
     * remove Attribute at Context
     */
    public static void removeAttribute(String key) {
        RequestContextHolder.currentRequestAttributes().removeAttribute(prefix + key, RequestAttributes.SCOPE_REQUEST);
    }

    /**
     * get Current Request
     */
    public HttpServletRequest currentRequest() {
        return ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
    }

    /**
     * get Current Response
     */
    public HttpServletResponse currentResponse() {
        return ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getResponse();
    }
}

注解的使用

配置好注解类并在拦截器中处理注解以后, 那么只需要在相应的地方进行注解标注就可以了, 如同使用Java自带或Spring提供的注解一样. 例如:

@RestController
@AdminOnly(exclude = {"createReport"})
public class AdminController {
    \\...
}
@CreatorOnly(type = EntityType.NOTIFICATION, position = 2)
@RequestMapping(path = "/notification/{id:[0-9]+}", method = RequestMethod.DELETE)
public ResponseEntity<String> deleteNotification(@PathVariable("id") Integer id) throws CustomException {
    \\...
}

总结与回顾

总的来说, 注解本质上相当于只是给类或者方法打了一个标记, 具体想要知道一个类或者一个方法是否有这个标记需要通过反射来获得, 并进行对应的处理.
通过反射对于类进行操作, 总体来说是比较慢的, 目前各大框架的配置逐渐从XML形式转变为注解形式, 这一过程是否会造成比较大的性能损失是一个需要思考的问题.
在这一次的例子中, 每一次请求都对其所有的注解进行遍历是比较慢而且进行了很多操作, 可以考虑增加一个HashMap用于记录一个类/方法对应了哪几个注解, 这样下次访问同一个类/方法时直接查表就可以而不用再通过反射来获取. 或者考虑倒排索引的形式, 记录每一个注解都对应哪些方法, 从而避免重复的查询和处理.


简明ArchLinux安装教程


概述

自用的简明ArchLinux安装教程, 主要参考官方Guide, 部分组件按照自己的喜好来

  • 引导管理采用grub2
  • 网络管理采用systemd-networkd
  • 文件系统用的btrfs

启动ArchISO

略 (这都不会还是立刻右上角吧

网络配置

ArchLinux的安装需要网络, 如果是DHCP的动态网络应该默认就配置好了.

如果是静态配置, 那么将需要手动进行配置, 下面将使用systemd-networkd作为网络管理器, 网络配置采用Arch官方推荐的iproute2而不是经典的net-tools.

检测网络是否联通

ping ip.cn

静态网络配置

确认网卡名称并启动网卡(如果没有启动的话)

ip addr show
ip route show
ip link set $interface up

配置systemd-networkd

'/etc/systemd/network/$interface.network'

[Match]
Name=$interface

[Network]
Address=$address/$prefix
Gateway=$gateway
DNS=$dns_server
systemctl start systemd-networkd.service
systemctl start systemd-resolved.service

临时配置静态IP, 路由和DNS(如果不想用systemd-networkd的话)

ip address add $address/$prefix broadcast + dev $interface
ip route add default via $gateway dev $interface
echo 'nameserver $dns_server' >> /etc/resolv.conf

启用NTP服务(TLS连接需要正确的时间)

timedatectl set-ntp true
timedatectl status

磁盘分区

  • 确定DiskName
fdisk -l
  • 磁盘分区

我一般习惯会分一个swap, 因为使用btrfs所以还要再分一个根卷/, 如果使用EFI则需要再分一个 /efi.

fdisk $disk
g: create GPT partition table
n: add a new partition
d: delete a partition
t: change a partition type
p: print the partition table
w: write table to disk and exit
如果使用的是传统BIOS的话, 需要使用t命令将启动分区的类型设置为BIOS boot(4).
  • 格式化分区
mkfs.ext4 $partition # /
mkswap $partition && swapon $partition # swap
mkfs.btrfs -L $label $partition # / (btrfs)
mkfs.vfat $partition # /efi
  • 挂载分区
mount $partition /mnt
mkdir /mnt/efi && mount $partition /mnt/efi # EFI分区

解压基本系统

  • 修改软件源

/etc/pacman.d/mirrorlist

  • 解压基本系统和安装基本包
pacstrap /mnt base grub openssh sudo # 安装sudo和ssh
pacstrap /mnt vim wget zsh git btrfs-progs  # 其他的一些基本包
  • 生成fstab
genfstab -U /mnt >> /mnt/etc/fstab
  • 复制网络配置(如果之间配置了systemd-networkd的话)
cp /etc/systemd/network/$interface.network /mnt/etc/systemd/network/$interface.network
  • Chroot
arch-chroot /mnt

配置基本系统

  • 配置网络并默认启动SSH(如果之间配置了systemd-networkd的话)
systemctl enable systemd-networkd.service
systemctl enable systemd-resolved.service
systemctl enable sshd.service
  • 配置Root密码
passwd
  • 添加新用户并配置Sudo
groupadd sudoers
useradd -m -G sudoers $user
passwd $user
cat '%sudoers ALL=(ALL) ALL' >> /etc/sudoers
  • 生成Initramfs
mkinitcpio -p linux
  • 配置grub
grub-install --target=i386-pc /dev/sdX
grub-mkconfig -o /boot/grub/grub.cfg
至此, 重启就可以进入到Arch的系统里了

额外的系统配置

  • 设置时区和硬件时钟
ln -sf /usr/share/zoneinfo/$Region/$City /etc/localtime
hwclock --systohc
  • 配置Locale

/etc/locale.gen

locale-gen
echo 'LANG=en_US.UTF-8' > /etc/locale.conf
  • 配置主机名和Hosts
echo $hostname > /etc/hostname

/etc/hosts

127.0.0.1   localhost
::1     localhost

尾声

这样就配置好了ArchLinux的基本系统, 其他的图形配置可以重启以后在SSH里慢慢配置.


MTJ-N + Intel MKL的配置和使用


前情提要

众所周知, 在python的世界, 矩阵库最好用的莫过于numpy, 类似于Matlab的语法, 让很多人从Matlab迁移过来毫无压力.

然而, 在Java的世界, 虽然以Spring为主的Web框架非常强大, 但是科学计算库的支持相比之下就相当感人. 倒并不是缺乏支持, 而是你很难找到一个足够好用效率又高支持又全的矩阵库.

当然实际上, 对于矩阵库的使用还是以效率为主, 所以今天要介绍的是Java世界中效率最高的矩阵库之一matrix-toolkits-java, 虽然作者已经不再维护, 但是性能经过实测依然吊打其他所有.

这个库性能高的主要原因在于它使用了Native库, 相比同样使用Native库的JBLAS, 这个库因为能够支持Intel MKL和Nvidia CUDA, 使其性能更高, 相关的Java矩阵库比较可以在Java Matrix Benchmark这里看到详细的比较.

正片开始

MTJ配置使用

MTJ在Maven中心仓库里, 所以如果使用Maven来管理依赖的话, 那么只要简单的添加即可:

<dependency>
    <groupId>io.github.andreas-solti.matrix-toolkits-java</groupId>
    <artifactId>mtj</artifactId>
    <version>1.0.7</version>
</dependency>

这里用的是另外一个Fork, 似乎是修正了原作者版本的一些问题, 不过没仔细看. 关于MTJ的文档可以参考这个: http://www.javadoc.io/doc/io.github.andreas-solti.matrix-toolkits-java/mtj/1.0.7.

使用这个库务必需要注意的是很多操作都是不复制而是在原矩阵上修改的, 例如:

public Matrix add(double alpha, Matrix B) {
    checkSize(B);

    if (alpha != 0)
        for (MatrixEntry e : B)
            add(e.row(), e.column(), alpha * e.get());

    return this;
}

public void add(int row, int column, double value) {
    set(row, column, value + get(row, column));
}

所以使用时如果要符合一般的使用习惯请务必注意:

/**
 * Simplify Some DenseMatrix Operation
 */
private static class DenseMatrixUtil{
    // return C=A+B
    public static DenseMatrix add(DenseMatrix A, DenseMatrix B){
        return (DenseMatrix)A.copy().add(B);
    }

    // return A=A+B
    public static DenseMatrix addi(DenseMatrix A, DenseMatrix B){
        return (DenseMatrix)A.add(B);
    }

    // return C=A-B
    public static DenseMatrix sub(DenseMatrix A, DenseMatrix B){
        return (DenseMatrix)A.copy().add(-1, B);
    }

    // return A=A-B
    public static DenseMatrix subi(DenseMatrix A, DenseMatrix B){
        return (DenseMatrix)A.add(-1, B);
    }

    // return C=s*A
    public static DenseMatrix mul(DenseMatrix A, double s){
        return (DenseMatrix)A.copy().scale(s);
    }

    // return A=s*A
    public static DenseMatrix muli(DenseMatrix A, double s){
        return (DenseMatrix)A.scale(s);
    }

    // return C=A*B
    private static DenseMatrix mmul(DenseMatrix A, DenseMatrix B){
        DenseMatrix C = new DenseMatrix(A.numRows(), B.numColumns()); // C.rows = A.rows, C.cols = B.cols
        return (DenseMatrix)A.mult(B, C);
    }

    // Compute \sum{alpha[i] * matrix[i]}
    private static DenseMatrix linear(double[] alphas, DenseMatrix[] matrixs){
        if(alphas.length != matrixs.length){
            throw new IllegalArgumentException("alpha's length should equal matrix's length");
        }

        DenseMatrix result = (DenseMatrix)new DenseMatrix(matrixs[0].numRows(), matrixs[0].numColumns()).zero();
        for(int i = 0; i < alphas.length; i++){
            result.add(alphas[i], matrixs[i]); // result = alpha * matrix + result
        }
        return result;
    }
}

与Intel MKL整合

Intel MKL全称是Intel® Math Kernel Library, 是Intel为其处理器设计的一套高度优化的Native矩阵与线性运算函数库, 相比OpenBLAS有着高得多的性能. 至于AMD处理器能不能使用, 这个我就不知道了, 但是我还是要喊出 AMD YES! (逃

Intel MKL安装

在Linux下, 只要直接从源里直接安装即可, 不同发行版不一样搜索一下即可, 以ArchLinux为例:

pacman -S intel-mkl

Arch的官方源里是没有MKL的, 如果需要的话, 请使用ArchlinuxCN或者Arch4Edu源.

在Windows下, 只能从官网注册开发者然后申请, 几天之后会给你发邮件然后提供下载连接下载Windows版的安装包.
这个安装包是包含devel部分的开发包, 事实上我们只需要redist部分即可, 为了方便使用我打包了2018.3.210的redist包(x86/x64). 事实上, Anaconda里面也是带Intel MKL Redist的, 完全可以从里面扒出来...

Intel MKL配置

在安装好Intel MKL以后还需要做两件事情:

  1. mkl\mkl_rt.dll软链接或者复制为libblas3.dllliblapack3.dll
  2. mklcompiler两个文件夹添加到系统的PATH环境变量中

这样, 才能够被MTJ-N所识别并使用.

如果不使用Intel MKL, MTJ似乎会自动使用OpenBLAS的实现, 其输出是下面这样的:

一月 14, 2019 11:31:15 上午 com.github.fommil.netlib.BLAS <clinit>
警告: Failed to load implementation from: com.github.fommil.netlib.NativeSystemBLAS
一月 14, 2019 11:31:15 上午 com.github.fommil.jni.JniLoader liberalLoad
信息: successfully loaded C:\Users\$USER\AppData\Local\Temp\jniloader7616017108689444760netlib-native_ref-win-x86_64.dll
一月 14, 2019 11:31:33 上午 com.github.fommil.netlib.LAPACK <clinit>
警告: Failed to load implementation from: com.github.fommil.netlib.NativeSystemLAPACK
一月 14, 2019 11:31:33 上午 com.github.fommil.jni.JniLoader load
信息: already loaded netlib-native_ref-win-x86_64.dll

性能虽然会比纯Java实现要好, 但是与MKL相比之下就要逊色很多了.

如果正确识别到MKL的话, 应该会类似的输出:

一月 14, 2019 11:38:25 上午 com.github.fommil.jni.JniLoader liberalLoad
信息: successfully loaded C:\Users\$USER\AppData\Local\Temp\jniloader3275505562389233726netlib-native_system-win-x86_64.dll
一月 14, 2019 11:38:26 上午 com.github.fommil.jni.JniLoader load
信息: already loaded netlib-native_system-win-x86_64.dll

微不足道的总结

如果要做科学计算方面的工作的话, 还是:

人生苦短, 快用Python (并不