平台在2018.11.27发布了第一版,第一版部署到了阿里云上,demo地址:http://39.105.29.134:8080,在寒假的时候对平台进行了一次比较大的改动,主要是一些设计和代码层面的优化,最新的代码目前放在github上,

https://github.com/buptchk/LitctfPlatform,本篇文章主要是对开发过程中遇到的一些知识点进行总结

配置

数据库连接池的配置

这里由于需要一些监控功能,所以选择了阿里系的Druid。使用配置类进行配置,使用@configuration标记一个配置类,被@configuration标记的类中的被@bean标记的方法会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化Spring容器,可以看到这里需要定义DataSource,使用druiddatasource,同时,这里使用@configurationProperties注解来标记,指定prefix后,可以在配置文件(application.yaml)中进行更详细的配置。同时由于需要druid开启web显示监控,所以使用了ServletRegistrationBean来注册了一个servlet(StatViewServlet),同理由于需要过滤器,也注册了一个webStatFilter过滤器

@Configuration
public class DruidConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druid(){
        return DruidDataSourceBuilder.create().build();
    }
    @Bean
    public ServletRegistrationBean statViewServlet(){
        ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
        ...
        return bean;
    }
    @Bean
    public FilterRegistrationBean webStatFilter(){
        FilterRegistrationBean bean = new FilterRegistrationBean(new WebStatFilter());
        ...
        return bean;
    }
}

Shiro的配置及使用

详细的分析见上一篇文章:shiro简单使用以及身份认证如何实现的探索

ORM及数据库

mybatis的使用

配置:注意开启驼峰映射,这时候就会将数据库中aaa_bbb自动映射成aaaBbb。

mybatis:
  configuration:
    map-underscore-to-camel-case: true
基本使用:

使用@Mapper注解标注对应的mapper接口,使用@Select,@Delete搭配对应的SQL语句标记对应的方法,需要的时候,将这个mapper注入,接着调用对应的方法就行。

传入参数及结果集的映射:

  1. 返回类型如果是包装类型,会把查到的数据按照列名映射到对应的成员变量中,同理如果我们传入的是一个pojo,就会自动解析其中的成员变量按照对应的参数传入。

  2. 如果结果集是多条值,返回类型一般使用List或者是数组,mybatis也会自动帮我们处理,如果传入的值有多个,我们需要用@Param(“参数名”)标记,int insertNewSolve(@Param("cid") int *cid*,@Param("uid") int *uid*);这样可以清楚区分不同变量对应的参数。

  3. 自定义ResultHandler对结果集进行处理,这次遇到的一个需求是,我想用一个类似哈希表的数据结构来存储每个用户已解出的题,想要查询的时候直接返回一个HashMap<Integer,List<Challenge>>类型的数据。我们这时候需要自己写一个自定义的TypeChallengeResultHandler,这个自定义的类需要实现ResultHandler接口,实现这个接口要实现public void handleResult(ResultContext resultContext)方法,这个方法用于自定义如何处理resultContext,也就是结果集。这里可以先将resultContext的类型进行转化,转化成我们想要的类型,例如将其转化成Map,也就是(列名,值)的键值对,在处理的时候都是一行一行的处理,也就是说我们resultObject.get(“列名1”),resultObject.get(“列名2”)得到的都是同一行的不同列的数据。

    Map<String,Object> resultObject=(Map<String,Object>)resultContext.getResultObject();
    

    然后我们再进行我们想要的逻辑处理。

    在使用的时候,我们需要让这个方法可以传入一个ResultHandler,用于传入我们自定义的ResultHandler,这个方法返回值必须是void,而且必须用@ResultType(例子:Map.class)来指定我们将结果集转化成的类型。

        @Select("SELECT cid,type FROM challenge where cid in(SELECT cid FROM solve where uid=#{uid})")
        @ResultType(Map.class)
        void listTypeGroupSolvedByUserId(@Param("uid")Integer uid,ResultHandler resultHandler);
    

    这样,我们在使用的时候,只要调用这个方法,并且将我们自己的TypeChallengeResultHandler传进去就可以了

  4. 实现一对多,多对多,优化之前的程序设计的时候,想要查询user的时候可以一同查出这个用户已解出题目的一个List,也就是实现一对多。

    @Select("Select * from user where uid=#{uid}")
        @Results({
                @Result(column = "uid",property = "uid"),
                @Result(column = "uid",property = "solvedWebChallenges",many = @Many(select = "com.litchi.litchi_ctf.mapper.Solvemapper.SelectWebSolvedByUser")),
                @Result(column = "uid",property = "solvedNumber",one = @One(select = "com.litchi.litchi_ctf.mapper.Solvemapper.SelectCountSolvedByUser"))
        })
        public User getUserById(Integer uid);
    

    只需要用一个@Results()注解标记结果集合,注解中使用{@Result(),@Result()}来标记结果映射关系,column代表数据库中的列名,property表示成员变量名,[email protected]()或者[email protected]()表示一对一或者是一对多,@one或者@many()注解中要指明select = “指明实现方法的完全限定名”,但是这样就有一个问题,我们在加载的时候,加载了太多的东西,占用了过多的内存,但是这些信息并不是马上需要用到,或者压根用不到,这时候我们就应该开启mybatis的延迟加载(lazyloadingenabled)

    lazy-loading-enabled: true
    

    数据库的设计

    这次数据库的设计感觉很普通,也没有做什么优化。有几个比较需要注意的点,NULL 需要额外的空间,并且,在你进行比较的时候,你的程序会更复杂。

    1. 尽量把字段设置成NOT NULL,NULL需要额外的空间
    2. 不要用SELECT * 而是应该采用你需要什么就查什么,并且写明,这样在更改数据库的时候,就不需要更改数据库查询。
    3. 统计行数的时候使用COUNT(*)来统计行数,清晰而且性能比较好
    4. 为除了关联表之外的所有表建立id主键

    模板引擎使用

    这次选用的模板引擎是Thymeleaf4,Thymeleaf的坑不少,这次也踩了挺多的,而且看文章对比好像性能也一般,不过是spring推荐的模版引擎。官方文档给出了不少实例,较为详细。

    1. th:each可以用于遍历==List,数组,Map==,遍历方式都一样,eg:th:each="allChallengeMap : ${typeChallengeMap}"Map遍历的是每一个键值对,List和数组遍历的是每一个值。Map可以使用getValue获得值。allChallengeMap.getValue(),每一个each都包换一个***Stat对象,使用这个对象的index可以获得下标,eg:allChalengeMapStat.index

    2. Thymeleaf内置了一系列的工具内置对象,可以通过#访问,这一系列包括dates,calendars,numbers,strings,objects,bools,arrays,lists,sets,map,包括一些常用的方法,详细的可以去thymeleaf看官方文档,有详细说明

    3. 判断是否有null,这个在循环中经常可以遇见,要是有null可能就会出现异常,比较快捷的方法${allchallenge?.getCid()}防止NPE

    4. 路径一般都选用@{/相对context的路径}表达式来使用,这个表达式在渲染时会自动添加上当前Web应用的Context名字

    5. 如何内联js?使用th:inline=”javascript”,在js中定义这些变量使用var example=[[${expression}]]来取值。在接下来的js中再来调用这些变量

      <script th:inline="javascript">
          var web = [[${WEB}]];
          var misc =[[${MISC}]];
          var pwn =[[${PWN}]];
          var cypto =[[${CYPTO}]];
          var re=[[${RE}]];
      </script>
      <div th:replace="js"></div>
      

    结构设计

    设计的时候主要遇到的几个问题:

    1. 有许多变量是系统级的,是所有用户共享的,比如用户排名,系统注册人数,通知,开始设计的时候考虑的是使用单例模式,集合到一个Local类中管理,然后所有用户共享这个实例,这样比较节省内存,而且也省去了一直去数据库查的时间,但是这样就有一个问题,由于多个用户共享一个变量,当都要改变这个变量的时候,由于是多线程,就可以出现错误,需要加锁,但是加上锁之后,也会影响性能。
    2. 在最开始的版本中,由于所有的Challenge有不同的type,需要将题目按照type划分,之前的设计是写了一个ChallengeList类,里面有6个List代表6种type,然后在初始化的时候,直接将六个List查出来,优化的时候,感觉这种设计将所有的Challenge分散了,在需要得到所有的Challenge的时候比较不方便,而且不好控制同步,所以优化的时候就使用了一个ConcurrentHashMap<Integer,List<Challenge>>,然后在Mybatis使用了ResultHandler来处理对应。不用担心同步问题。但是现在总结的时候觉得,这种有一个不好的地方,就是一次性加载了所有类型的题目。可以使用反射延迟加载,但是本工程都是同时获取,所以效果也一般。
    3. 第一次设计的时候,表示Challenge的Type,是没有定义,直接使用1,2,3…导致在使用的时候不知道分别是如何对应的,在优化的时候使用枚举类来处理。