博客

  • 网站乱码和ISO-8859-1与UTF-8

    今天没事看看博客访问量,发现每天有大量流量来自于关于Spring Boot乱码问题的关键字。基本上达到了网站流量的30%以上。十分费解乱码问题为什么这么多人遇到。

    Spring Boot中的乱码问题解决方案非常简单,修改配置中的force为true即可,这里不再叙述。Spring Boot和其中的Spring MVC等只是java世界构建网站的一种选项,还有很多其他语言和其他框架,不过乱码问题是一个很泛的问题,并不是绑定在任何一种语言和框架上的。

    我们常说的网站乱码其实是指http请求的编码有问题。原因很简单,默认的编码是ISO-8859-1,而这种编码是一种80年代的遗留标准,它只能表示256个字符,即便对于西方英语用户,这种编码也有很多符号不能正确解决,当然中文更不用说了。

    UTF-8是UTF家族的,相对要新,而解决的问题更多,对于绝大部分文字都有很好的支持,属于事实上的标准。

    因为ISO-8859-1是历史遗留的标准,所以绝大部分容器仍然都是将其作为默认编码,包括Apache,Tomcat,WebSphere等,而Spring Boot是内嵌Tomcat运行,所以默认编码也是ISO-8859-1,虽然复写了编码为UTF-8,但是并没有设置为force encoding,所以一些版本依然会乱码。

    因为默认编码问题在很多地方都存在,所以标准也是相互影响的。

    简单来看历史进程是这样的:

    1. 最开始是ASCII,今天也有大量遗留系统使用着
    2. 在WIndow流行时,默认编码是ANSI,也叫Windows-1252
    3. 后来就是ISO-8859-1,这是是随着HTML 2.0标准固定下来的
    4. 再到后来的UTF-8,HTML5的默认编码已经是UTF-8了

    参考

    http://www.w3schools.com/charsets/

    https://www.w3.org/International/articles/http-charset/index

    https://en.wikipedia.org/wiki/UTF

    https://en.wikipedia.org/wiki/UTF-8

  • Spark快速获得CrossValidator的最佳模型参数

    Spark快速获得CrossValidator的最佳模型参数

    Spark提供了便利的Pipeline模型,可以轻松的创建自己的学习模型。

    但是大部分模型都是需要提供参数的,如果不提供就是默认参数,那么怎么选择参数就是一个比较常见的问题。Spark提供在org.apache.spark.ml.tuning包下提供了模型选择器,可以替换参数然后比较模型输出。

    目前有CrossValidator和TrainValidationSplit两种,比如一个文本情感预测模型。

    Pipeline只有三步,第一步切词,第二部Hashing TF,第三部NB分类

    Pipeline pipeline = new Pipeline()
                    .setStages(new PipelineStage[]{tokenizer, hashingTF, naiveBayes});
    
    ParamMap[] paramMaps = new ParamGridBuilder()
                    .addGrid(hashingTF.numFeatures(), new int[]{10000, 100000, 500000, 1000000})
                    .build();
    CrossValidator cv = new CrossValidator()
                    .setEstimator(pipeline)
                    .setEvaluator(new BinaryClassificationEvaluator())
                    .setEstimatorParamMaps(paramMaps);

    其中Hashing TF的参数选择非常重要,我们这里就随便尝试几种,然后放在CrossValidator中去。

    最后我们会获得一个CrossValidatorModel类,这里有两种选择。

    第一种是自己手动获取其中的参数,因为bestModel的参数就是我们最后选择的参数

    Pipeline bestPipeline = (Pipeline) model.bestModel().parent();
    PipelineStage stage = bestPipeline.getStages()[1];
    stage.extractParamMap().get(stage.getParam("numFeatures"));

    这种方法可以获得值,但是需要根据你模型情况修改获取的位置。

    如果你只是想知道最佳参数是多少,并不是需要在上下文中使用,那还有一个更简单的方法。

    修改log4j的配置,添加

    log4j.logger.org.apache.spark.ml.tuning.TrainValidationSplit=INFO
    log4j.logger.org.apache.spark.ml.tuning.CrossValidator=INFO

    效果如下:

    spark-best-model-params

  • 使用ThrowOnFailure属性捕获Tomcat启动时的错误

    Tomcat是一个广泛使用的容器,不仅仅独立使用对外暴露服务,也可以用集成模式运行在其他程序内部或者支持测试。

    Tomcat的Connector是一个重要的部分,如果它启动失败,那么意味着容器无法正常对外暴露服务。

    在Tomcat中如果Connector启动失败,日志中会打印相关信息(两次),这种情况对于作为独立服务对外暴露的情况还好,查查日志解决问题。如果是作为集成模式的话就有点麻烦了。比如Spring Boot内嵌了Tomcat,为了在启动时给用户提供更好的体验,Spring Boot有一个错误分析器,也就是说当有错误发生的时候给出更友好的提示,而不是简单的打印日志。

    自己的代码当然好控制,但是内嵌的Tomcat就是一个问题,外部没法简单获得信息。

    Tomcat最近有一个更改解决了这个问题,在Connector中新增了一个throwOnFailure属性,当这个属性为真时,就会抛出LifecycleException错误。

    try {
    Connector c = new Connector("foo.Bar");
    c.setThrowOnFailure(throwOnFailure);
    c.start();
    }catch (LifecycleException ex){
      // 分析错误
    }

     

     

    参考:

    http://svn.apache.org/viewvc?view=revision&revision=1763769

    https://bz.apache.org/bugzilla/show_bug.cgi?id=60152

  • 再谈Spring Boot中的乱码和编码问题

    再谈Spring Boot中的乱码和编码问题

    编码算不上一个大问题,即使你什么都不管,也有很大的可能你不会遇到任何问题,因为大部分框架都有默认的编码配置,有很多是UTF-8,那么遇到中文乱码的机会很低,所以很多人也忽视了。

    Spring系列产品大量运用在网站开发中,而Spring Boot是为了简化配置而出现的,理论上讲Spring Boot应该默认配置UTF-8为默认编码,但是网络上依然可以看到很多关于Spring Boot乱码的文章,大部分解决方案沿用Spring MVC的方案,自定义EncodingFilter。

    但是仔细查看Spring Boot的文档,可以看到默认的编码的确是UTF-8

    spring.http.encoding.charset=UTF-8 # Charset of HTTP requests and responses. Added to the "Content-Type" header if not set explicitly.
    spring.http.encoding.enabled=true # Enable http encoding support.

    而相关的配置会在HttpEncodingAutoConfiguration中使用

    @Bean
    	@ConditionalOnMissingBean(CharacterEncodingFilter.class)
    	public CharacterEncodingFilter characterEncodingFilter() {
    		CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
    		filter.setEncoding(this.properties.getCharset().name());
    		filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST));
    		filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE));
    		return filter;
    	}

    而这里你其实可以看到,默认情况下forceRequestEncoding和forceResponseEncoding是为false的。

    在配置中自己加上一行

    spring.http.encoding.force=true

    除了常见的http encoding,Spring Boot中还可以控制这些编码

    banner.charset
    spring.freemarker.charset
    server.tomcat.uri-encoding
    spring.mail.default-encoding
    spring.messages.encoding
    spring.thymeleaf.encoding

    只不过这些值默认就设置为UTF-8,而且并需要搭配其他配置开关使用,所以一般不需要管。

  • 在AWS Lambda中运行Spring Boot应用

    在AWS Lambda中运行Spring Boot应用

    AWS Lambda 是一种计算服务。首先将代码上传到 AWS Lambda ,它按使用时间收费,有自动伸缩等功能,也就是说你上传了代码,其他事情你就不用管了。

    从字面上看你上传的代码是一个函数,那么就需要一个触发器来调用你的函数。AWS提供了若干触发器,其中就有API Gateway。这意味着你可以暴露一个HTTP或者HTTPS的调用地址,然后执行对应的函数。

    因为AWS Lambda有自己的一些要求,所以很明显是无法直接运行Spring Boot应用的,需要修改一下入口才能实现对应的功能。

    首先在依赖中加入aws的依赖

    compile group: 'com.amazonaws', name: 'aws-java-sdk-core', version: '1.11.48'

    这个依赖中包含了我们必须实现的接口RequestHandler

    package com.amazonaws.services.lambda.runtime;
    
    import com.amazonaws.services.lambda.runtime.Context;
    
    /**
     * 
     * Lambda request handlers implement AWS Lambda Function application logic using plain old java objects 
     * as input and output.
     *
     * @param <I> The input parameter type
     * @param <O> The output parameter type
     */
    public interface RequestHandler<I, O> {
        /**
         * Handles a Lambda Function request
         * @param input The Lambda Function input
         * @param context The Lambda execution environment context object.
         * @return The Lambda Function output
         */
        public O handleRequest(I input, Context context);
    }
    

    所以新建一个Runner类

    @Configuration
    @EnableAutoConfiguration
    @ComponentScan("你自己的包名")
    public class Runner implements RequestHandler<Void, PersonsVO> {
        private Logger logger = LoggerFactory.getLogger(this.getClass());
        private static ConfigurableApplicationContext applicationContext;
    
        @Override
        public PersonsVO handleRequest(Void input, Context context) {
            StopWatch stopWatch = new StopWatch();
            stopWatch.start();
            logger.info("Start !");
            ApplicationContext applicationContext = getApp();
            logger.info("Spring boot load finished.");
            stopWatch.stop();
            logger.info("Time is {}", stopWatch.toString());
            T t = applicationContext.getBean(你需要的类.class);
            return t.function();
        }
    
        private ConfigurableApplicationContext getApp() {
            if (applicationContext == null) {
                applicationContext = new SpringApplicationBuilder()
                        .main(getClass())
                        .bannerMode(Banner.Mode.OFF)
                        .web(false)
                        .sources(getClass())
                        .addCommandLineProperties(false)
                        .build()
                        .run();
            }
            return applicationContext;
        }
    }

    其实原理很简单,因为入口变了,所以只有自己启动Spring上下文,然后调用。

    AWS Lambda标榜的是自己调用了就会回收,但是从观察来看,似乎会保持当前的环境和状态一段时间,所以我们可以再调用的时候先判断一下有没有Spring上下文,如果有就直接使用,没有才加载。

    首次请求真的很慢,因为Spring需要生成上下文,如果你还用了hibernate这些,那启动速度轻轻松松达到10s,但是这之后的时间就很快了,因为环境中已经有Spring上下文了。如果很久之后你的函数再被触发,那么又需要重新加载了。

    aws-lambda

    总的来说一个普通的Spring Boot应用是很难直接迁移到AWS Lambda的,虽然能够工作,但是响应时间和包大小很难接受。

  • 使用AWS Elastic Beanstalk发布spring boot应用

    Elastic Beanstalk是AWS提供的快速使用AWS的部署和管理应用程序。

    Elastic Beanstalk提供了很多常用语言的快速上手的模板,可以直接从中启动。以一般的Spring Boot应用为例,进入以后选择“Web 服务器”套餐,语言选择JAVA,默认语言版本为JAVA 8。

    然后选择上传代码。

    Elastic Beanstalk提供了两个自定义选项,一个是源码的编译,一个是环境的自定义。

    先来看看Buildfile。选择在服务器编译的原因有很多种,有些是真的必须要本地编译,有的是包实在太大,对于Spring Boot应用来说,50M以上的jar真的很常见,上传上去真的很麻烦。

    所以Buildfile中直接调用gradle打包即可

    build: gradle build -x test

    打包之后jar包会放在build/libs下面,这里就还需要指定在EC2上运行的命令,不然没法定位jar包位置,这个就用Procfile文件

    web: java -jar build/libs/your.jar
    

    Elastic Beanstalk默认会提供一个Nginx,然后默认端口是转发到5000。

    Procfile是可以写几行的,每一行的端口依次加100,比如第一个是5000,第二个就是5100。

    Elastic Beanstalk是支持直接选择免费使用套餐的,但是默认免费套餐用的t1,而不是t2可以自己改一下。

    当然,过了免费期的就随意了。

  • AWS RDS无法访问的问题

    在AWS起了一个MariaDB的数据库,虽然设置了公开访问,但是不知道为什么总是连接不上。

    仔细检查一下才发现创建的时候一路Next,对于安全组规则选择了默认的一个,其中允许的IP不对,马上改一下,允许所有IP

    aws-group

  • 集中式认证服务

    CAS全称Central Authentication Service,中文名集中式认证服务。

    CAS是一种针对万维网的单点登录协议。它的目的是允许一个用户访问多个应用程序,而只需提供一次凭证(如用户名和密码)。它还允许web应用程序在没有获得用户的安全凭据(如密码)的情况下对用户进行身份验证。CAS即指协议,也指实现了该协议的软件包。

    CAS最早由Yale发起,目前有三个版本,即v1,v2,v3。

    相关定义

    Client

    终端用户或者是 WEB 浏览器

    Server

    统一认证服务所在的服务器

    Service

    终端用户或者 WEB 浏览器试图访问的应用

    Proxy

    作为代理的服务,用户通过该服务(代理)访问Back-end service(后端应用)

    Back-end service

    用户通过代理访问的应用,这个应用就被称为后端服务(Back-end service) 。它也被称作“target service”目标服务

    TGT

    Ticket Grangting Ticket 。TGT是CAS为用户签发的登录票据,拥有了TGT,用户就可以证明自己在CAS成功登录过。TGT封装了Cookie值以及此Cookie值对应的用户信息。当HTTP请求到来时,CAS以此Cookie值为key查询缓存中有无TGT ,如果有的话,则相信用户已登录过。

    ST

    Service Ticket 。ST是CAS为用户签发的访问某一service的票据。用户访问service时,service发现用户没有ST,则要求用户去CAS获取ST。用户向CAS发出获取ST的请求,CAS发现用户有TGT,则签发一个ST,返回给用户。用户拿着ST去访问service,service拿ST去CAS验证,验证通过后,允许用户

    TGC

    Ticket Grangting Cookies 保持TGT信息的cookies,保存在使用浏览器的客户端

    PGT

    Proxy TicketGranting Ticket。Proxy Service认证成功后,CAS会生成PGT,并将值回传给Proxy Service 。Proxy Service拿到PGT后,就可以为Target Service做代理,为其申请PT。

    PGTIOU

    Proxy TicketGranting Ticket IOU。PGTIOU是CAS协议中定义的一种附加票据,它增强了传输、获取PGT的安全性。

    PT

    Proxy Ticket。PT是用户访问Target Serivce的票据。用户经由Proxy Service去CAS获取到PT后,再访问Target Serivce,Target Serivce去CAS验证PT成功后,才允许用户访问。

    Web登录流程图

    web-cas

    网址定义

    CAS作为一个规范,规定了一系列需要暴露的服务端

    /login 登录
    /logout 登出
    /validate 验证ST
    /serviceValidate 验证ST [CAS 2.0]
    /proxyValidate 验证PT [CAS 2.0]
    /p3/serviceValidate ST验证 [CAS 3.0]
    /p3/proxyValidate PT验证 [CAS 3.0]

     

    参考文档

    CAS标准:https://apereo.github.io/cas/4.2.x/protocol/CAS-Protocol-Specification.html

    CAS实现:https://www.apereo.org/projects/cas

  • Android和Intel HAXM的问题

    今天把本机的ionic升级到2,然后使用教程中的代码启动项目来测试一下环境。

    不知道出了什么问题,没有使用android启动了,报错为

    emulator: Requested console port 5584: Inferring adb port 5585.
    emulator: ERROR: x86_64 emulation currently requires hardware acceleration!
    Please ensure Intel HAXM is properly installed and usable.
    CPU acceleration status: HAXM must be updated (version 1.1.1 < 6.0.1).

    想想的确有印象装过一次,版本估计太老了。

    打开android-sdk升级HAXM,然后再试。。。错误依旧,反复重试依然如此。

    结果在android home目录下的intel\Hardware_Accelerated_Execution_Manager中,找到了安装文件。

    需要自己安装!

    仔细看一下因为ionic2默认支持android 4.4以上,我直接删除了以前旧的模拟器,然后建了一个5.1的模拟器,这才出现了依赖变化。

    不得不说这个android sdk也是坑,HAXM是提供静默安装的。

  • 自定义SpringBootCondition

    Spring Boot中一个很重要的特性就是自动配置,可以根据很多条件决定是否生成相应的bean或者其他操作。

    大部分情况下自动配置的条件是ConditionalOnMissingBean,也就是没有这个bean,那就用这个自动配置配置一个。

    有时候自动配置条件并不是那么直接,有时候还涉及到一些自己的逻辑,这个时候就可以自定义SpringBootCondition来处理复杂一些的情况。

    继承SpringBootCondition并将自己的逻辑实现在getMatchOutcome方法中。

    自定义的逻辑大部分有两种,第一种是自动配置针对一类情况,需要检测多个独立类的存在性,比如EmbeddedDatabaseCondition,就会自动检测所有可能的数据库驱动,包括H2,DERBY,HSQL

    @Override
    public ConditionOutcome getMatchOutcome( ConditionContext context,
    					 AnnotatedTypeMetadata metadata )
    {
    	if ( anyMatches( context, metadata, this.pooledCondition ) )
    	{
    		return(ConditionOutcome.noMatch( "supported DataSource class found" ) );
    	}
    	EmbeddedDatabaseType type = EmbeddedDatabaseConnection
    				    .get( context.getClassLoader() ).getType();
    	if ( type == null )
    	{
    		return(ConditionOutcome.noMatch( "no embedded database detected" ) );
    	}
    	return(ConditionOutcome.match( "embedded database " + type + " detected" ) );
    }

    还有一种就是配置之间相互依赖,比如虽然提供了一个配置名为feature.enable,并且设置为true,但是相关的必要参数,比如feature.username没有提供,这种情况下自动配置并没有用处,不仅要不自动配置,还要提供有用的信息给用户,指出缺失的参数

    @Override
    public ConditionOutcome getMatchOutcome( ConditionContext context,
    					 AnnotatedTypeMetadata annotatedTypeMetadata )
    {
    	if ( !isEnabled( context.getEnvironment() ) )
    	{
    		return(ConditionOutcome.noMatch(
    			       "Spring Boot Client is disabled, because 'spring.boot.admin.client.enabled' is false." ) );
    	}
    
    	if ( isUrlEmpty( context.getEnvironment() ) )
    	{
    		return(ConditionOutcome.noMatch(
    			       "Spring Boot Client is disabled, because 'spring.boot.admin.url' is empty." ) );
    	}
    
    	return(ConditionOutcome.match() );
    }