沉静

沉静 查看完整档案

长沙编辑中南大学  |  信息科技 编辑海鹚科技  |  后端开发工程师 编辑 www.chenjingtalk.com 编辑
编辑

在因准备面试复习知识的时候,发现很多原理都是生活中现象的体现,随即想到如果能用生活中的例子来讲技术原理是不是更容易理解?将来会考虑往这个方向去写一些文章。也算是致敬刘欣老师。

个人动态

沉静 关注了问题 · 2020-09-02

解决spring apectj 切面 控制台日志提示问题

2018-07-30 16:51:46,842 DEBUG [org.springframework.aop.aspectj.AspectJExpressionPointcut] - PointcutExpression matching rejected target method
java.lang.NullPointerException
    at org.aspectj.weaver.ResolvedType.lookupResolvedMember(ResolvedType.java:627)
    at org.aspectj.weaver.JoinPointSignatureIterator.findSignaturesFromSupertypes(JoinPointSignatureIterator.java:192)
    at org.aspectj.weaver.JoinPointSignatureIterator.hasNext(JoinPointSignatureIterator.java:68)
    at org.aspectj.weaver.patterns.SignaturePattern.matches(SignaturePattern.java:317)
    at org.aspectj.weaver.patterns.KindedPointcut.matchInternal(KindedPointcut.java:197)
    at org.aspectj.weaver.patterns.Pointcut.match(Pointcut.java:137)
    at org.aspectj.weaver.patterns.AndPointcut.matchInternal(AndPointcut.java:56)
    at org.aspectj.weaver.patterns.Pointcut.match(Pointcut.java:137)
    at org.aspectj.weaver.internal.tools.PointcutExpressionImpl.getShadowMatch(PointcutExpressionImpl.java:319)
    at org.aspectj.weaver.internal.tools.PointcutExpressionImpl.matchesExecution(PointcutExpressionImpl.java:129)
    at org.aspectj.weaver.internal.tools.PointcutExpressionImpl.matchesMethodExecution(PointcutExpressionImpl.java:110)
    at org.springframework.aop.aspectj.AspectJExpressionPointcut.getShadowMatch(AspectJExpressionPointcut.java:426)
    at org.springframework.aop.aspectj.AspectJExpressionPointcut.matches(AspectJExpressionPointcut.java:281)
    at org.springframework.aop.support.AopUtils.canApply(AopUtils.java:241)
    at org.springframework.aop.support.AopUtils.canApply(AopUtils.java:279)
    at org.springframework.aop.support.AopUtils.findAdvisorsThatCanApply(AopUtils.java:311)
    at org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator.findAdvisorsThatCanApply(AbstractAdvisorAutoProxyCreator.java:119)
    at org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator.findEligibleAdvisors(AbstractAdvisorAutoProxyCreator.java:89)
    at org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator.getAdvicesAndAdvisorsForBean(AbstractAdvisorAutoProxyCreator.java:70)
    at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:346)
    at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.postProcessAfterInitialization(AbstractAutoProxyCreator.java:298)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:423)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1633)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)

这个是报错吗?但是项目没有任何影响。

关注 3 回答 2

沉静 赞了回答 · 2020-09-02

解决spring apectj 切面 控制台日志提示问题

我的aspectj是 1.8.10,换成 1.8.9就可以了

关注 3 回答 2

沉静 赞了文章 · 2020-07-30

程序员写技术博客选平台还是自己手动搭建?

很多程序员也在积累到一定阶段之后,会开始写自己的博客,一方面可以帮助自己沉淀平时工作中的知识;另一方面可以分享知识给别人,帮助其他同行在遇到相同问题时提供思路。

有人会说,都什么年代了,还写博客,大家都在写公众号啦!其实博客有公众号代替不了的地方,就是可以通过搜素引擎搜索到,比如「如何在CentOS上安装SS?」这种问题是肯定不会在公众号上能搜到的。

很多朋友选择在哪里写博客犯了难?下面是我个人觉得是比较适合程序员写个人博客的8个选择:(排名不分先后)

  • 阿里云云栖社区
  • 腾讯云+社区
  • 掘金
  • SegmentFault
  • 博客园
  • CSDN
  • 简书
  • Github/Gitlab Pages 自己搭建静态网站

云栖社区

阿里云云栖社区是近年来内容逐渐丰富的平台,其中阿里的同学很多都会在上面发布内容,质量也比较的高。不过其中的内容更偏向于服务器运维,后端开发方面。

腾讯云+社区

腾讯云+社区跟云栖社区基本上属于同一类型的,主要内容也是更多关于云方面的内容,很专业。

掘金

掘金是近两年来比较火爆的社区,掘金上面的内容分类做的非常的好,知识面涵盖了几乎全部的行业,之前有朋友在掘金上写起了短篇小说,可以说掘金上的阅读体验非常的好。

优点

  • 用户粘性强
  • 支持 markdown 和富文本两种编辑器
  • 阅读体验好

SegmentFault

SegmentFault 是前些年比较火爆的类似于 StackOverflow 的问答类网站,目前感觉已经有些不那么火了,但是网站的月活大约在 2000W左右,它的流量也几乎跟 StackOverflow 差不多,都是搜索流量。如果你的文章是解决方案之类的,那么 SegmentFalut 是非常适合的!

博客园

博客园是很古老的博客平台,从它的主页,到博客页都能看出上古遗迹的感觉。不过他们的SEO也是做的非常的好。他们的编辑器对 markdown 支持不友好。

优点:

  • SEO做的好
  • 阅读量有保证

缺点:

  • 太旧了
  • 没有适配移动端

CSDN

CSDN也是一个很有历史的博客平台,CSDN 的 SEO 效果非常的好,但是CSDN不知道什么原因,它展示的广告内容太多了,我个人不太喜欢,而且CSDN上有太多相似的内容,内容质量不高。

优点:

  • SEO非常好
  • 月活 2亿左右的用户,阅读量有保证

缺点:

  • 广告太多
  • 内容质量不太高
  • 阅读体验不好

简书

简书是一个类似于 Medium 的博客平台,风格是十分简洁的,对 Markdown 支持非常的好,并且在手机上展示的效果也是非常的好。不过它的SEO 效果不是特别的好,而且无法进行分类等。

优点:

  • 页面风格简洁
  • markdown 友好

缺点:

  • 平台内容太杂,技术博客较少

自己搭建

自己搭建,这种方式比较复杂,适合想自己动手,不愿意受限制于平台,致力于打造个人名片的朋友。自己搭建的选择有很多种,选择付费产品?不存在的,作为一个程序员,怎么能花钱在这些上面!

Github Pages 或者 Gitlab Pages 都是可以免费使用的。如果对 Jekyll 不感冒,可以选择 Gitlab Pages 进行搭建,因为 Gitlab 上支持很多种静态网站生成器,而 Github 只支持 Jekyll,Gitlab 支持比如 Jekyll,Hexo,Hugo,VuePress等等。Gitlab + Netlify 可以搭建一套完全自动编译自动发布的个人博客网站,参考:Gitlab Pages + Netlify 免费搭建自动化发布网站

总结

以上是现阶段比较好的博客平台,不代表以后也不代表以前。其实最终,最重要的是博客的内容,而不是载体,或许再过几年之后,又会有更新更时髦更好用的平台出来了。

最后,没有最好的平台,只有最好的博客内容。希望各位看完不需要再纠结选哪个平台了,而是开始写起自己的博客吧!

查看原文

赞 1 收藏 0 评论 0

沉静 发布了文章 · 2020-07-26

用故事讲解四种主要的IO模型

今天小明入职的第一天,由于事情不多,下班的时候小明早早地就准备走了。

没想到公司所在的楼层人也太多了,所有人必须排队一个一个的上电梯,电梯满了之后,剩下的人就得在楼道里干等着,虽然可以刷刷手机,但是等待的时间还是挺无聊的。

画外音:公司所在楼层代表服务器,小明和同事代表服务器需要传送的数据,而电梯代表公司的 I/O 端口。小明和同事排队等电梯,是一种同步阻塞模型(Blocking I/O)。

第二天,小明觉得等电梯的时间也太无聊了,而且现在是夏天,天气热起来一群人挤在楼道里,也太难受了。

为了充分利用自己的时间,快下班的时候,小明决定找个能看到楼道的地方,带上自己的笔记本,时不时地扫一眼楼道,看看电梯有没有到,人多的时候,就干脆继续工作下,修修福报,早日成为福娃。虽然时不时地扫楼道,挺消耗眼力,但是想到能继续做下还没做完地工作,小明感觉很欣慰。

画外音:不去等待 I/O 响应,而是不断轮询数据是否已经准备好。这就是同步非阻塞(None Bloking I/O)。

第三天,小明觉得自己搬着电脑到楼道边上也太 2 了,公司的妹子们路过的时候,总觉得他有点傻傻的。而且小明才发现公司后门那里也有电梯,虽然人也不少,但是多一个选择总归不错。更重要的,小明发现电梯间里都有监控,随即想到,我干脆找 IT 部门申请下监控的读取权限,开发一个自动识别软件!

申请监控权限可不容易,需要公司层面的支持,好在互联网公司的领导比较人性化,同意了小明的要求。而且作为 Xinux 集团的公司,提供了更强的 epoll 接口,能够安全的读取公司的电梯间监控信息。本着能够充分利用公司的电梯资源目的,领导就让小明放手去做了。

经过一个星期的努力,小明终于完成了这个软件,现在可以直接联通公司的监控数据,将所有电梯间的监控展示在一个页面上,然后还可以通过图像识别,来精准监控多个电梯是否到达,小明现在可以一眼就能看到多个电梯是否到了!

画外音:多路复用 I/O 模型(I/O multiplexing)与前面的 NIO 模型,是相似的。不过可以利用操作系统提供的能力,通过一次 select/epoll 系统调用,就查询到可以读写的一个甚至成百上千的网络连接。相比 NIO 模型,节省了系统的资源。

经过了很长一段时间,这个监控软件运行地很好。同事们都对这个软件赞不绝口。可是小明可不满足,因为现在是互联网时代,随着 5G 的到来,马上万物互联都不是太大的问题了。听说隔壁家 Xindows 集团早就实现了智能电梯了,可不是嘛,小明所在的公司的在电梯厂商的一波营销之下,采购了智能电梯,每台电梯都可以通过 Wifi 连接到公司的网络当中。甚至可以通过接口进行一定的编程。

小明马上盯准了这个接口。如果能够将每个人的微信注册到电梯的控制系统当中,通过预约排队的方式来安排好每个人下楼的时间,当到了时间之后,电梯可以通过微信提醒的方式发送通知,岂不是再也不用每天下班盯着软件看电梯了!

经过一段时间的努力,终于完成了,这下各个同事彻底解放双手,每天下电梯的时间按照微信的通知来就行了,美滋滋。

画外音:异步IO模型(Asynchronous I/O),通过回调的方式,彻底解放了用户线程的等待问题

后记:小明在领导的指示下,“为了不断改善电梯的使用体验”。终于,公司的每个同事都发现自己的下班时间越来越晚……

查看原文

赞 0 收藏 0 评论 0

沉静 赞了文章 · 2020-06-30

如何在MyBatis中优雅的使用枚举

问题

在编码过程中,经常会遇到用某个数值来表示某种状态、类型或者阶段的情况,比如有这样一个枚举:

public enum ComputerState {
    OPEN(10),         //开启
    CLOSE(11),         //关闭
    OFF_LINE(12),     //离线
    FAULT(200),     //故障
    UNKNOWN(255);     //未知

    private int code;
    ComputerState(int code) { this.code = code; }
}

通常我们希望将表示状态的数值存入数据库,即ComputerState.OPEN存入数据库取值为10

探索

首先,我们先看看MyBatis是否能够满足我们的需求。
MyBatis内置了两个枚举转换器分别是:org.apache.ibatis.type.EnumTypeHandlerorg.apache.ibatis.type.EnumOrdinalTypeHandler

EnumTypeHandler

这是默认的枚举转换器,该转换器将枚举实例转换为实例名称的字符串,即将ComputerState.OPEN转换OPEN

EnumOrdinalTypeHandler

顾名思义这个转换器将枚举实例的ordinal属性作为取值,即ComputerState.OPEN转换为0,ComputerState.CLOSE转换为1
使用它的方式是在MyBatis配置文件中定义:

<typeHandlers>
    <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="com.example.entity.enums.ComputerState"/>
</typeHandlers>

以上的两种转换器都不能满足我们的需求,所以看起来要自己编写一个转换器了。

方案

MyBatis提供了org.apache.ibatis.type.BaseTypeHandler类用于我们自己扩展类型转换器,上面的EnumTypeHandlerEnumOrdinalTypeHandler也都实现了这个接口。

1. 定义接口

我们需要一个接口来确定某部分枚举类的行为。如下:

public interface BaseCodeEnum {
    int getCode();
}

该接口只有一个返回编码的方法,返回值将被存入数据库。

2. 改造枚举

就拿上面的ComputerState来实现BaseCodeEnum接口:

public enum ComputerState implements BaseCodeEnum{
    OPEN(10),         //开启
    CLOSE(11),         //关闭
    OFF_LINE(12),     //离线
    FAULT(200),     //故障
    UNKNOWN(255);     //未知

    private int code;
    ComputerState(int code) { this.code = code; }

    @Override
    public int getCode() { return this.code; }
}

3. 编写一个转换工具类

现在我们能顺利的将枚举转换为某个数值了,还需要一个工具将数值转换为枚举实例。

public class CodeEnumUtil {

    public static <E extends Enum<?> & BaseCodeEnum> E codeOf(Class<E> enumClass, int code) {
        E[] enumConstants = enumClass.getEnumConstants();
        for (E e : enumConstants) {
            if (e.getCode() == code)
                return e;
        }
        return null;
    }
}

4. 自定义类型转换器

准备工作做的差不多了,是时候开始编写转换器了。
BaseTypeHandler<T> 一共需要实现4个方法:

  1. void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType)

用于定义设置参数时,该如何把Java类型的参数转换为对应的数据库类型

  1. T getNullableResult(ResultSet rs, String columnName)

用于定义通过字段名称获取字段数据时,如何把数据库类型转换为对应的Java类型

  1. T getNullableResult(ResultSet rs, int columnIndex)

用于定义通过字段索引获取字段数据时,如何把数据库类型转换为对应的Java类型

  1. T getNullableResult(CallableStatement cs, int columnIndex)

用定义调用存储过程后,如何把数据库类型转换为对应的Java类型

我是这样实现的:

public class CodeEnumTypeHandler<E extends Enum<?> & BaseCodeEnum> extends BaseTypeHandler<BaseCodeEnum> {

    private Class<E> type;

    public CodeEnumTypeHandler(Class<E> type) {
        if (type == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        }
        this.type = type;
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, BaseCodeEnum parameter, JdbcType jdbcType)
            throws SQLException {
        ps.setInt(i, parameter.getCode());
    }

    @Override
    public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
        int code = rs.getInt(columnName);
        return rs.wasNull() ? null : codeOf(code);
    }

    @Override
    public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        int code = rs.getInt(columnIndex);
        return rs.wasNull() ? null : codeOf(code);
    }

    @Override
    public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        int code = cs.getInt(columnIndex);
        return cs.wasNull() ? null : codeOf(code);
    }

    private E codeOf(int code){
        try {
            return CodeEnumUtil.codeOf(type, code);
        } catch (Exception ex) {
            throw new IllegalArgumentException("Cannot convert " + code + " to " + type.getSimpleName() + " by code value.", ex);
        }
    }
}

5. 使用

接下来需要指定哪个类使用我们自己编写转换器进行转换,在MyBatis配置文件中配置如下:

<typeHandlers>
    <typeHandler handler="com.example.typeHandler.CodeEnumTypeHandler" javaType="com.example.entity.enums.ComputerState"/>
</typeHandlers>

搞定! 经测试ComputerState.OPEN被转换为10,ComputerState.UNKNOWN被转换为255,达到了预期的效果。

6. 优化

在第5步时,我们在MyBatis中添加typeHandler用于指定哪些类使用我们自定义的转换器,一旦系统中的枚举类多了起来,MyBatis的配置文件维护起来会变得非常麻烦,也容易出错。如何解决呢?
Spring中我们可以使用JavaConfig方式来干预SqlSessionFactory的创建过程,来完成转换器的指定。
思路

  1. 再写一个能自动匹配转换行为的转换器
  2. 通过sqlSessionFactory.getConfiguration().getTypeHandlerRegistry()取得类型转换器注册器
  3. 再使用typeHandlerRegistry.setDefaultEnumTypeHandler(Class<? extends TypeHandler> typeHandler)将第一步的转换器注册成为默认的

首先,我们需要一个能确定转换行为的转换器:
AutoEnumTypeHandler.java


public class AutoEnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {

    private BaseTypeHandler typeHandler = null;

    public AutoEnumTypeHandler(Class<E> type) {
        if (type == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        }
        if(BaseCodeEnum.class.isAssignableFrom(type)){
            // 如果实现了 BaseCodeEnum 则使用我们自定义的转换器
            typeHandler = new CodeEnumTypeHandler(type);
        }else {
            // 默认转换器 也可换成 EnumOrdinalTypeHandler
            typeHandler = new EnumTypeHandler<>(type);
        }
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
        typeHandler.setNonNullParameter(ps,i, parameter,jdbcType);
    }

    @Override
    public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return (E) typeHandler.getNullableResult(rs,columnName);
    }

    @Override
    public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return (E) typeHandler.getNullableResult(rs,columnIndex);
    }

    @Override
    public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return (E) typeHandler.getNullableResult(cs,columnIndex);
    }
}

接下来,我们需要干预SqlSessionFactory的创建过程,将刚刚的转换器指定为默认的:

@Configuration
@ConfigurationProperties(prefix = "mybatis")
public class MyBatisConfig {


    private String configLocation;

    private String mapperLocations;


    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dataSource);


        // 设置配置文件及mapper文件地址
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        factory.setConfigLocation(resolver.getResource(configLocation));
        factory.setMapperLocations(resolver.getResources(mapperLocations));


        SqlSessionFactory sqlSessionFactory = factory.getObject();


        // 取得类型转换注册器
        TypeHandlerRegistry typeHandlerRegistry = sqlSessionFactory.getConfiguration().getTypeHandlerRegistry();
        // 注册默认枚举转换器
        typeHandlerRegistry.setDefaultEnumTypeHandler(AutoEnumTypeHandler.class);

        return sqlSessionFactory;
    }

    // ... getter setter
}

搞定! 这样一来,如果枚举实现了BaseCodeEnum接口就使用我们自定义的CodeEnumTypeHandler,如果没有实现BaseCodeEnum接口就使用默认的。再也不用写MyBatis的配置文件了!

结束了

以上就是我对如何在MyBatis中优雅的使用枚举的探索。如果你还有更优的解决方案,请一定在评论中告知,万分感激。

查看原文

赞 32 收藏 42 评论 15

沉静 收藏了文章 · 2020-06-30

如何在MyBatis中优雅的使用枚举

问题

在编码过程中,经常会遇到用某个数值来表示某种状态、类型或者阶段的情况,比如有这样一个枚举:

public enum ComputerState {
    OPEN(10),         //开启
    CLOSE(11),         //关闭
    OFF_LINE(12),     //离线
    FAULT(200),     //故障
    UNKNOWN(255);     //未知

    private int code;
    ComputerState(int code) { this.code = code; }
}

通常我们希望将表示状态的数值存入数据库,即ComputerState.OPEN存入数据库取值为10

探索

首先,我们先看看MyBatis是否能够满足我们的需求。
MyBatis内置了两个枚举转换器分别是:org.apache.ibatis.type.EnumTypeHandlerorg.apache.ibatis.type.EnumOrdinalTypeHandler

EnumTypeHandler

这是默认的枚举转换器,该转换器将枚举实例转换为实例名称的字符串,即将ComputerState.OPEN转换OPEN

EnumOrdinalTypeHandler

顾名思义这个转换器将枚举实例的ordinal属性作为取值,即ComputerState.OPEN转换为0,ComputerState.CLOSE转换为1
使用它的方式是在MyBatis配置文件中定义:

<typeHandlers>
    <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="com.example.entity.enums.ComputerState"/>
</typeHandlers>

以上的两种转换器都不能满足我们的需求,所以看起来要自己编写一个转换器了。

方案

MyBatis提供了org.apache.ibatis.type.BaseTypeHandler类用于我们自己扩展类型转换器,上面的EnumTypeHandlerEnumOrdinalTypeHandler也都实现了这个接口。

1. 定义接口

我们需要一个接口来确定某部分枚举类的行为。如下:

public interface BaseCodeEnum {
    int getCode();
}

该接口只有一个返回编码的方法,返回值将被存入数据库。

2. 改造枚举

就拿上面的ComputerState来实现BaseCodeEnum接口:

public enum ComputerState implements BaseCodeEnum{
    OPEN(10),         //开启
    CLOSE(11),         //关闭
    OFF_LINE(12),     //离线
    FAULT(200),     //故障
    UNKNOWN(255);     //未知

    private int code;
    ComputerState(int code) { this.code = code; }

    @Override
    public int getCode() { return this.code; }
}

3. 编写一个转换工具类

现在我们能顺利的将枚举转换为某个数值了,还需要一个工具将数值转换为枚举实例。

public class CodeEnumUtil {

    public static <E extends Enum<?> & BaseCodeEnum> E codeOf(Class<E> enumClass, int code) {
        E[] enumConstants = enumClass.getEnumConstants();
        for (E e : enumConstants) {
            if (e.getCode() == code)
                return e;
        }
        return null;
    }
}

4. 自定义类型转换器

准备工作做的差不多了,是时候开始编写转换器了。
BaseTypeHandler<T> 一共需要实现4个方法:

  1. void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType)

用于定义设置参数时,该如何把Java类型的参数转换为对应的数据库类型

  1. T getNullableResult(ResultSet rs, String columnName)

用于定义通过字段名称获取字段数据时,如何把数据库类型转换为对应的Java类型

  1. T getNullableResult(ResultSet rs, int columnIndex)

用于定义通过字段索引获取字段数据时,如何把数据库类型转换为对应的Java类型

  1. T getNullableResult(CallableStatement cs, int columnIndex)

用定义调用存储过程后,如何把数据库类型转换为对应的Java类型

我是这样实现的:

public class CodeEnumTypeHandler<E extends Enum<?> & BaseCodeEnum> extends BaseTypeHandler<BaseCodeEnum> {

    private Class<E> type;

    public CodeEnumTypeHandler(Class<E> type) {
        if (type == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        }
        this.type = type;
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, BaseCodeEnum parameter, JdbcType jdbcType)
            throws SQLException {
        ps.setInt(i, parameter.getCode());
    }

    @Override
    public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
        int code = rs.getInt(columnName);
        return rs.wasNull() ? null : codeOf(code);
    }

    @Override
    public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        int code = rs.getInt(columnIndex);
        return rs.wasNull() ? null : codeOf(code);
    }

    @Override
    public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        int code = cs.getInt(columnIndex);
        return cs.wasNull() ? null : codeOf(code);
    }

    private E codeOf(int code){
        try {
            return CodeEnumUtil.codeOf(type, code);
        } catch (Exception ex) {
            throw new IllegalArgumentException("Cannot convert " + code + " to " + type.getSimpleName() + " by code value.", ex);
        }
    }
}

5. 使用

接下来需要指定哪个类使用我们自己编写转换器进行转换,在MyBatis配置文件中配置如下:

<typeHandlers>
    <typeHandler handler="com.example.typeHandler.CodeEnumTypeHandler" javaType="com.example.entity.enums.ComputerState"/>
</typeHandlers>

搞定! 经测试ComputerState.OPEN被转换为10,ComputerState.UNKNOWN被转换为255,达到了预期的效果。

6. 优化

在第5步时,我们在MyBatis中添加typeHandler用于指定哪些类使用我们自定义的转换器,一旦系统中的枚举类多了起来,MyBatis的配置文件维护起来会变得非常麻烦,也容易出错。如何解决呢?
Spring中我们可以使用JavaConfig方式来干预SqlSessionFactory的创建过程,来完成转换器的指定。
思路

  1. 再写一个能自动匹配转换行为的转换器
  2. 通过sqlSessionFactory.getConfiguration().getTypeHandlerRegistry()取得类型转换器注册器
  3. 再使用typeHandlerRegistry.setDefaultEnumTypeHandler(Class<? extends TypeHandler> typeHandler)将第一步的转换器注册成为默认的

首先,我们需要一个能确定转换行为的转换器:
AutoEnumTypeHandler.java


public class AutoEnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {

    private BaseTypeHandler typeHandler = null;

    public AutoEnumTypeHandler(Class<E> type) {
        if (type == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        }
        if(BaseCodeEnum.class.isAssignableFrom(type)){
            // 如果实现了 BaseCodeEnum 则使用我们自定义的转换器
            typeHandler = new CodeEnumTypeHandler(type);
        }else {
            // 默认转换器 也可换成 EnumOrdinalTypeHandler
            typeHandler = new EnumTypeHandler<>(type);
        }
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
        typeHandler.setNonNullParameter(ps,i, parameter,jdbcType);
    }

    @Override
    public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return (E) typeHandler.getNullableResult(rs,columnName);
    }

    @Override
    public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return (E) typeHandler.getNullableResult(rs,columnIndex);
    }

    @Override
    public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return (E) typeHandler.getNullableResult(cs,columnIndex);
    }
}

接下来,我们需要干预SqlSessionFactory的创建过程,将刚刚的转换器指定为默认的:

@Configuration
@ConfigurationProperties(prefix = "mybatis")
public class MyBatisConfig {


    private String configLocation;

    private String mapperLocations;


    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dataSource);


        // 设置配置文件及mapper文件地址
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        factory.setConfigLocation(resolver.getResource(configLocation));
        factory.setMapperLocations(resolver.getResources(mapperLocations));


        SqlSessionFactory sqlSessionFactory = factory.getObject();


        // 取得类型转换注册器
        TypeHandlerRegistry typeHandlerRegistry = sqlSessionFactory.getConfiguration().getTypeHandlerRegistry();
        // 注册默认枚举转换器
        typeHandlerRegistry.setDefaultEnumTypeHandler(AutoEnumTypeHandler.class);

        return sqlSessionFactory;
    }

    // ... getter setter
}

搞定! 这样一来,如果枚举实现了BaseCodeEnum接口就使用我们自定义的CodeEnumTypeHandler,如果没有实现BaseCodeEnum接口就使用默认的。再也不用写MyBatis的配置文件了!

结束了

以上就是我对如何在MyBatis中优雅的使用枚举的探索。如果你还有更优的解决方案,请一定在评论中告知,万分感激。

查看原文

沉静 发布了文章 · 2020-05-27

WebSocket初探-实战经验分享

背景

笔者一直以来做的都是普通的CRUD业务。近期产品经理突然奇想,想要在我们当前的产品中整合一个答题对战小游戏。对于常年只会CRUD的我还是提出了一些挑战。时至今日,项目已经接近开发完毕,至此总结下项目中的一些收获。

本文主要是总结我在做这个项目中的实战经验,所以各个模块均从业务面出发,毕竟脱离了业务的技术设计都是瞎扯淡。如果你想在这里找到能够拿来就用的代码,显然是没有的。并且我默认大家都已经知道了基础知识。不过我在文末补充了一些我在做项目过程中参考的不错的文章和博客,欢迎取阅。

通讯模式选择

这个答题对战小游戏姑且可以算作是一个网络游戏。在一个网络游戏中,一个很重要的点就是保持服务端与客户端、客户端与客户端之间的同步。游戏业界已经有了两个常用的同步模型:帧同步和状态同步。

帧同步

我们都很熟悉的王者荣耀使用的就是帧同步。简单来说,服务端规定了一个时序,在一个时序内,所有的客户端必须把自己这个时序内发送的动作都传递到服务端,服务端在收到所有客户端的动作之后,再把这些动作转发到所有当前战局内的客户端。客户端根据接收到的动作信号,在本地进行重现。

可以看得出,帧同步有着非常完整的时间轴控制,在客户端数量比较少的情况下,相互之间需要发送的消息数据量很小(只需要传递每一个客户端在这个时序内的动作)。

状态同步

状态同步就是客户端发送操作到服务端,服务端进行计算,并把结果传递给其他客户端。由于时序控制不严格,所以比较适合回合制游戏。

总结

帧同步和状态同步的实现策略不同,导致他们分别适合于不同的游戏类型,具体本文不深究。我们要做答题对战小游戏,属于回合制游戏,所以选择了状态同步。

通讯方式实现

无论采用哪种后端设计方案,我们都需要考虑一个问题,如何通过服务端向客户端发送消息。我之前很熟悉的HTTP接口,请求只能客户端发起,服务端只能对客户端的请求做响应。所以这时候必须得引入WebSocket了。

在Java里,常用的WebSocket框架有Spring和Netty,考虑到我对Spring相对更了解一些,所以采用了Spring。

Spring提供了两种不同的方案来使用WebSocket,有一种是STOMP,是一种特定消息协议,使用起来很清爽;另一种是原生WebSocket,虽然使用起来复杂一点,但是更加灵活。我选择了后者,因为他跟我们的小程序前端对接起来较为方便一些。

虽然WebSocket接入起来可能比较复杂,但是实际上它提供的功能就那么几个:

  • 连接建立
  • 消息监听
  • 消息发送

也就是说,无论业务是怎样的,我们都需要把他们归纳为以上三个功能。

鉴权

对于网络上任何请求我们都需要做鉴权。WebSocket在建立连接之前,有个通过HTTP请求握手的过程。在Spring中,我们可以通过设置org.springframework.web.socket.server.HandshakeInterceptor来对握手请求进行拦截。

所以我们可以在建立连接请求时,带上鉴权参数,比如生成的Token或者是用户的账号密码。我们还可以在鉴权的时候做一些其他的事情,比如检查当前已经建立的连接数量,控制服务器连接数量,以免耗尽了公网带宽。

由于这个环节时HTTP请求,所以可以响应特定的HTTP状态码给前端。

接收消息

对消息的监听就比较简单了,org.springframework.web.socket.WebSocketHandler#handleMessage提供了消息监听的方法。我们可以在消息中带上对应的action动作,根据不同的action调用不同的业务方法即可。

消息发送

在Spring的原生WebSocket使用中,如果想要给客户端发消息,必须使用org.springframework.web.socket.WebSocketSession。这本身问题不大,但是在分布式环境下就麻烦了。

比如我有两台服务器,服务器1接收到了客户端1的消息,然后做了响应处理,需要把消息转发到客户端2,但是客户端2当前与服务器2保持着连接,那么服务器1怎么直到客户端2于哪台服务器保持连接?即使知道了时服务器2,服务器1又怎样把消息转发到服务器2呢?

分布式环境下的WebSocket集群方案

现在随便一个业务都需要考虑在分布式环境下的使用和实现方案,单点永远都是不可靠的。在HTTP服务器上,我们可以将HTTP Session持久化到Redis中来实现分布式Session,或者是完全抛弃Session,采用Token的解决方案。但是WebSocket的Session并不能保存到Redis。这很好理解,WebSocket的连接其实是有状态的,客户端于服务端是建立的长连接,所以即使我把session持久化了,也不可能在另一台服务器上获取了这个session就可以向对应的客户端发送消息。

那么到底应该如何解决呢?

最简单的方法就是利用广播消息,服务器发现需要给客户端发送消息的时候,直接发送一个广播消息,所有的服务器都可以接收这个广播消息。当服务器收到此消息时,检查要推送的客户端是否于自己保持连接,如果未保持,直接丢弃,如果保持着,就推送消息给相应的客户端。广播消息的实现也很简单,可以直接使用MQ,目前主流的MQ都提供了广播消息的功能,我使用的是RocketMQ。也可以使用Redis的订阅发布功能,原理其实是一样的。

当然,广播消息显然会比较浪费性能。更加高逼格的策略是使用Hash路由,让具有一些特征的客户端连接到指定的服务端,服务端需要发消息的时候,只需要根据路由表找到与这个客户端保持连接的服务端即可。但是这样的实现需要运维的支撑和配合,如果请求量不大或者是自己的服务器节点本身就不多,使用广播消息完全就够了。

扩展阅读

学习WebSocket基础知识:WebSocket教程-阮一峰

如果在Spring项目中增加WebSocket实现:springmvc+websocket的全部实现方式

分布式WebSocket实现:分布式WebSocket集群解决方案

了解一些游戏后端设计思路:Play Cards: 探索通用的游戏后端方案

查看原文

赞 0 收藏 0 评论 1

沉静 赞了文章 · 2019-07-01

老 Android 手机装 Ubuntu 用做低功耗服务器

老 Android 手机装 Ubuntu 用做 低功耗服务器

作 zuō

最近寻思着弄一台低功耗 linux 服务器用,只运行一些小程序,例如 Python 写的爬虫或者定时任务。网上看了一圈,什么树莓派呀啥的,一套下来要两三百了,有点不划算。突然想到我还有一个小米3触控不灵,闲置着。看了一下它的配置,ac 的 wifi + 2.3GHz 的 cpu,很强啦!(比树莓派强)

装个 Ubuntu core

本安装方法基于开源项目 Linux on Android,该项目让你能够在安卓手机上运行很多 Linux 发行版。

当然了本篇文章只讲解如何安装 Ubuntu 13.10 core,也就是只有命令行的版本(作为服务器,不需要图形界面,最大化性能)

注:此方法理应适用于 android 版本大于4.3的手机,并且必须要 ROOT

注2: 如果你这手机只用作服务器的话,在条件允许的情况下,建议你再把手机系统刷成AOSP(安卓官方开源项目,无任何捆绑、后台软件,最大限度释放你老手机的性能),

注3?: (没想到吧,还特么有注3。。)如果你只是轻度折腾一族,强烈建议不要刷AOSP,不然中途放弃了,就不能看下面的教程了

准备

我们需要下载的文件有这么几个:

文件名作用下载地址
ubuntu.imgUbuntu 镜像core 种子文件
ubuntu.sh安装 Ubuntu 的脚本文件百度云 密码: td75
androidterm.apk能在安卓上敲命令行的应用百度云 密码: crzy
busybox.apk增加更多命令行命令百度云 密码:tizn

当然这些我也都放在了百度盘里面,你可以打包下载 密码:tiut

下载好后,把 androidterm.apkbusybox.apk 安装到手机上,然后在手机存储(非外置内存卡)根目录下新建一个文件夹 ubuntu ,把 ubuntu.imgubuntu.sh 都复制进去。

安装

准备工作都已经完成,开始安装吧!

  1. Busybox
    先打开 busybox 弹出的第一个窗口点叉关掉,然后在主界面中选择安装版本:1.26.2 或更高,安装位置选择为:/system/bin ,最后点击左下角的 Install (期间会弹出请求 ROOT 授权,请选择允许)等待安装完成即可;
  2. Androidterm
    等 Busybox 安装好后,我们就打开 Terminal(也叫 终端) 这个 App,出现在我们眼前的是命令行界面,我们键入 cd /sdcard/ubuntu 点击键盘上的回车按钮(或者是 确认),他将会切换目录到我们之前存放 ubuntu.imgubuntu.sh 的文件目录下。然后输入 su 并确定,将会切换为 ROOT 用户,接着键入 sh ./ubuntu.sh 即可开始安装 Ubuntu 了。

安装的时候会让你输入新建的 ubuntu root 用户密码,并再次输入以确认,然后会问你是否开启 VNC 服务,我没有图形界面,则输入 n 并确定,接着他又会问你,是否开启 ssh 服务,这个必须要啊!! 输入 y 并确定,最后会问你是否将刚才的输入保存为默认,输入 y 点确定即可。

等命令行开头的部分变成:root@localhost 时,就说明安装并启动完成啦!

使用

在使用前,要确保你的手机是连接了 WiFi 网络的(不然有啥用呢),然后在手机的命令行中,输入 ifconfig ,看里面能看到你手机的 IP 地址,你在其他设备上,用 root 用户 ssh 连接这个 IP 就行了!

注意

作为服务器,是不允许机器自动挂掉的,所以,你需要将 terminal 这个程序锁住,不让系统自动关闭它,并保持着为手机充电的状态,这样,一个自带 ups 的低功耗服务器就搭建成功了。

后记

长期更新,后记则是用作后期优化以及问题解决方案提供的一个板块

- 改 ARM 专用源

Ubuntu改源怎么操作不用这里说明了吧,下面列出两个比较快的源:

中科大源

deb http://mirrors.ustc.edu.cn/ubuntu-ports trusty main universe restricted multiverse 
deb http://mirrors.ustc.edu.cn/ubuntu-ports trusty-security main universe restricted multiverse 
deb http://mirrors.ustc.edu.cn/ubuntu-ports trusty-updates main universe restricted multiverse 
deb-src http://mirrors.ustc.edu.cn/ubuntu-ports trusty main universe restricted multiverse 
deb-src http://mirrors.ustc.edu.cn/ubuntu-ports trusty-security main universe restricted multiverse 
deb-src http://mirrors.ustc.edu.cn/ubuntu-ports trusty-updates main universe restricted multiverse

清华源

deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ trusty main universe restricted multiverse 
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ trusty-security main universe restricted multiverse 
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ trusty-updates main universe restricted multiverse 
deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ trusty main universe restricted multiverse 
deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ trusty-security main universe restricted multiverse 
deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ trusty-updates main universe restricted multiverse

- Python3 安装

有了 Python 这服务器才像样,哈哈。

我这里选择的是安装 Python3, 以下方式进行安装:

# 安装 python3
sudo apt-get install python3 

# 安装 python3 对应的 pip
sudo apt-get install python3-pip

- crontab 任务

应用场景:每天自动签到的爬虫程序。

经过反复测试,好像这个版本中的crontab(通过 sudo apt-get install cron 安装)无法正常执行任务,在进程列表(ps -e | grep cron)中也看不到其踪影。所以放弃之,用 python 来解决(如果你有其他解决方法,请不惜赐教):

下面例子是每秒打印一次 hello world 到标准输出

hello.py

# -*- coding: UTF-8 -*-
import threading

# 任务执行间隔时间,下面是 1s 也就每秒执行一次
INTERVAL_TIME = 1

def task():
    # 在这里写下你要执行的命令,例如打印 HelloWorld
    print('Hello World!\n')

def cron():
    task()
    threading.Timer(INTERVAL_TIME, cron).start()

# 调用 cron 函数,即开始任务
cron()

执行的话就这样:

# 使用系统默认 python2.7 执行
python hello.py

# 使用新装的 python3 执行
python hello.py

我们就能看见控制台每秒都打印出字符了。

进阶:让脚本在后台执行,_即使我们关闭当前 shell,它也执行_

# 当然这句执行后,除了返回一个 pID 啥都没有的,如果你以后的脚本要输出信息,
# 就只需要将信息写入指定的 log 文件中即可
nohup python ./hello.py &

以上命令执行后会返回一个 pID,如果你想结束这个后台程序,只需要这样

kill 对应的pID

如果 pID 记不得了,下面方法能帮助你:

# 如果是用 python 执行的后台程序,就输入以下命令
ps -e|grep python

就能获得对应的列表,列表第一列就是 pID, kill 掉你想关的后台程序即可

- 中文乱码

应用场景:是个中国人就可能碰到。。。

嗯,这个问题我现在用的方式是:不用中文?。

理由是:

  1. 对中文输出要求不那么强烈
  2. 正则匹配中可以略过中文,牺牲一丁点性能算不了啥事儿
  3. 支持中文的话要装一大堆包,秉承能不装则不装的心态。
查看原文

赞 8 收藏 15 评论 3

沉静 关注了用户 · 2019-04-14

公众号_芋道源码 @gongzhonghao_yudaoyuanma

个人博客 http://www.iocoder.cn?sf
公众号:芋道源码。如果你喜欢阅读源码,欢迎关注我的公众号。

关注 51

沉静 关注了专栏 · 2019-03-20

code-craft

spring boot , docker and so on 欢迎关注微信公众号: geek_luandun

关注 725

认证与成就

  • 获得 28 次点赞
  • 获得 5 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-07-20
个人主页被 774 人浏览