问题以及解决方案

今天使用Intellj IDEA部署web项目时,spring web容器上下文(ContextLoader)加载非常慢,慢到完全启动一个项目需要耗时20分钟左右。

打开debug日志发现不停的加载注入的各种类,并且还有打印类中方法的详细信息。

最终定位问题是因为打开了Java Method Breakpoints(如下图),将其取消即可快速启动项目。img

探究原因

Google上找到同样的JAVA METHOD BREAKPOINTS ARE EVIL(借用了这篇文章的标题,因为这个标题太符合Java Method Breakpoints了,就是邪恶),从其中找到了jetbrains team在jetbrains官网上给出的针对这个问题的回应参考原链接 (链接加载略慢)

然后继续查了几个搜索结果发现各大IDE厂商的bug list里都有报类似问题,参考NetBeans BugEclipse Bug

他们基本都说明了一点:这个问题的产生和JVM的设计有关系。

具体是指的JVM规定了虚拟机必须支持一组标准的调试API,这组标准的调试API指的是JPDA (Java Platform Debugger Architecture),JDPA是一组精心设计的接口与协议,所有的IDE基于JPDA进行封装,提供友好的用户调试接口。

当你设置断点时,IDE告诉JVM’s Tool Interface(JDPA的一部分)断点所属的源文件和行号,JVM找到这个断点在源文件中位置对应的编译后class文件中的位置,当JVM运行到一个断点,它将会中断当前执行的线程并且告诉IDE这个断点的源文件和行号,然后由IDE呈现定位给用户。

这是基本的行断点调试的机制,而Java Method Breakpoints是方法断点调试,它的机制与行断点调试略有不同,它将给方法注册进入/退出事件,需要用到MethodEntryRequest这个类来进行注册。

然后我在JAVA官方找到了关于JPDA性能问题的BUG清单,其中有如下这样一段描述涉及到了MethodEntryRequest及其带来的性能问题

MethodEntryRequest and MethodExitRequest unreasonably slow down whole
performance. We use them to track calls to method in a particular class
or to any overriding methods. But it seems the performance slows down
for any method calls, not even to that particular class.

大致的意思就是说当用MethodEntryRequest来追踪方法调用时(可以理解为方法断点调试),它将会让任何方法的调用性能都下降,而不只是特定的类(打了方法断点的那个类)。

找到这里基本上明了了,因为我打了一个方法断点没有放开,spring web context在进行依赖注入时构造各种各样的类需要调用大量的方法(构造方法、静态方法、解析方法等等),每个方法的调用性能都下降了,所以整个项目加载起来自然就非常慢了。

案发背景

同事最近在对项目的hibernate框架进行升级,从3.6.10.Final升级至5.2.10.Final。升级期间出现了配置文件(xml)解析报错。主要报错堆栈如下

1
2
3
4
19:07:41 ERROR - Context initialization failed x org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'serviceManager' defined in class path resource [spring/applicationContext.xml]: Cannot resolve reference to bean 'xService' while setting bean property 'xSerivce'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'xService' defined in class path resource [spring/dao.xml]: Cannot resolve reference to bean 'transactionManager' while setting bean property 'transactionManager'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'transactionManager' defined in class path resource [spring/dataSource-common.xml]: Cannot resolve reference to bean 'sessionFactory' while setting bean property 'sessionFactory'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sessionFactory' defined in class path resource [spring/dataSource-common.xml]: Invocation of init method failed; nested exception is


java.lang.NoSuchMethodError:org.apache.xerces.impl.xs.XMLSchemaLoader.loadGrammar([Lorg/apache/xerces/xni/parser/XMLInputSource;)V

寻找线索

可以看到最后的一段日志java.lang.NoSuchMethodError:org.apache.xerces.impl.xs.XMLSchemaLoader.loadGrammar([Lorg/apache/xerces/xni/parser/XMLInputSource;)V

说明XMLSchemaLoader类的loadGrammar方法没有找到,这个方法的特征(L)是有一个数组XMLInputSource类型的数组参数。

寻找第一案发现场

顺着这个线索我们来看下找一下这个方法。

搜索源文件发现有两个jar包(见下图)都包含XMLSchemaLoader#loadGrammar

两者的XMLSchemaLoader类都有loadGrammar方法。__重点来了__,不同的是前者没有参数为XMLInputSource数组的loadGrammar方法,而后者有。这与报错堆栈完全吻合,可见这就是第一案发现场。

寻找真凶

有了上面的线索,很自然想要去maven仓库中看看是不是xerces包太老需要升级,搜索xerces后发现的确有问题,xerces已经建议使用xercesImpl替换了,看来真凶就是这个xerces包。

于是回到项目中去掉xerces:2.4.0的依赖,重启启动项目,依旧报错。

这时考虑是不是有其他jar包也存在对该包的引用,去生成的依赖库中一看,果然,xerces的依赖版本从2.4.0变为了2.0.2依然存在于依赖之中

这时基本可以断定就是其他包依赖了xerces,于是使用gradle的命令查看jar包依赖关系

1
2
# 查看gradle依赖关系
gradle dependencies

从依赖关系从看到commons-dbcp:1.2.1``commons-pool:1.2这两个包依赖了xerces:xerces:2.0.2包,导致依旧引入了xerces依赖。去除commons-dbcp:1.2.1并升级commons-pool:1.2commons-pool:1.6后刷新,xerces包依赖完全被去除。重启项目,一切顺畅,问题得以解决。

结案

这次问题的本质就是jar包冲突,只不过冲突的jar包依赖关系藏得较为隐蔽。

解决的手段其实较为简单,就是调和冲突的jar包(删除、升级、替换等)。

注意

上面我们为了解决jar包冲突直接删除了commons-dbcp依赖,这是因为我的项目已经不需要用到这个包了,但如果这个依赖包不能删除,如何单独删除其中冲突的jar包依赖呢?

这里提供一种gradle具体的调和手段

可以使用exclude去除jar包对第三方jar包的依赖,从而达到解决冲突的目的,具体写法如下。

1
2
3
4
5
dependencies {
compile('commons-dbcp:commons-dbcp:1.2.1') {
exclude module: 'xerces'
}
}

支付能力申请流程

一、注册支付宝开放平台

支付宝开放平台使用支付宝账号。

如果是支付能力开发商,建议使用企业邮箱注册。

如果是个人开发者,不能直接在支付宝开放平台开通支付能力,可使用第三方平台接入,如ping++

二、创建支付应用、开发配置

  1. 创建应用

  2. 设置基础环境 获取支付宝网关、设置应用网关(己方)、设置授权回调地址

    注意

    蚂蚁金服开放平台将对您填写的授权回调地址进行安全扫描,扫描结果请在1个小时后,前往“开发者中心-安全中心”查看。另外,请如实、准确填写授权回调网址,否则所有责任(包括但不限于赔偿损失)概由您负责。

  3. 设置接口加签方式 RSA2(SHA256)密钥(推荐)

    MAC_OSX_RSA密钥生成工具

    密钥生成工具使用方式

  4. 开发测试

三、上线前签约

上线前签约APP支付能力,即可在线上调用开发好的APP支付功能

  1. 资料审核 填写企业信息、经营信息、银行账户等信息,上传相关证件。资料提交后,审核将在3个工作日内完成。
  2. 验证账户 核实您提交的银行账户信息,以验证账户。
  3. 签署产品协议 在线签署产品协议,完成签约。

支付开发流程

服务端接口

一、签名下单接口

二、支付成功的验签接口

三、接收异步支付结果通知的接口

四、查询支付结果的接口

服务端支付DEMO

SDK1.5 适用于Java语言、jdk版本1.5及以上的开发环境

代码示例仅供参考和测试,实际请根据业务来处理。异步通知处理过程中请对信息做确认,例如:订单号是否在商户您的系统中存在,订单金额是否匹配

支付流程

流程涉及微信公众平台、微信商户平台、微信开放平台三大平台

服务端需要开发三个接口

一、统一下单接口

商户后台收到用户支付单,调用微信支付统一下单接口。参见【统一下单API

二、接收支付通知接口

商户后台接收支付通知。api参见【支付结果通知API

三、查询支付结果的接口

商户后台查询支付结果。api参见【查询订单API

参考github项目https://github.com/Seanid/wechatPay

APP端主要开发调起微信支付的接口

商户APP调起微信支付。api参见本章节【app端开发步骤说明

简介

jenkins是一个持续集成(CI)工具,使用java编写

CI continuous integration 持续集成

SCM source code management 源码管理

下载jenkins

有多种安装方式,我们选择war包部署,war包下载地址 https://jenkins.io/download/

下载的war包为jenkins.war

部署至tomcat

这里采用一台单独的tomcat部署jenkins

1
2
3
4
5
6
7
8
9
10
11
12
# 因为此tomcat只用来部署jenkins,所以先删除webapps下其他应用
rm -rf ${tomcat_path}/webapps/*
# 部署jenkins的war包
mv ${download_path}/jenkins.war ${tomcat_path}/webapps/ROOT.war
# 指定jenkins工作目录(如有必要),默认为~/.jenkins
vim ${download_path}/bin/catalina.sh
CATALINA_OPTS="-DJENKINS_HOME=~/jenkins-cluster-1/"
#
vim ${download_path}/conf/context.xml
<Context ...>
<Environment name="JENKINS_HOME" value="~/jenkins-cluster-1/" type="java.lang.String"/>
</Context>

配置jenkins

tomcat部署启动完jenkins后,访问对应端口可以看到如下jenkins的初始界面

查看/root/.jenkins/secrets/initialAdminPassword文件可以获得管理员密码,输入后可以解锁Jenkins,开始使用。

安装插件

初始化界面会提示我们安装插件,我们选择推荐插件

推荐的插件如下图所示

我们主要需要身份认证 源码管理 打包构建 管道命令 几个方面的插件

身份认证

ssh plugin SSH Slaves plugin Credentials Binding Plugin

源码管理

git plugin github plugin GitHub Branch Source Plugin gitlab plugin subversion Plug-in

打包构建

Gradle Plugin

管道命令

Pipeline Pipeline:State View Plugin

pipeline构建参考

其它插件

Role-based Authorization Strategy基于角色的的用户权限控制

设置全局工具

系统设置->Global Tool Configuration->JDK、Git、Maven

系统设置->Shell->Shell executable->/bin/zsh

Jenkins内部shell UTF-8 编码设置

参数化构建

Pipeline构建

创建一个新任务,任务类型选择pipeline

拉取源码

勾选GitHub hook trigger for GITScm polling 参考Trigger Jenkins builds by pushing to Github

  1. 在jenkins所在服务器上生成ssh key
  2. 在gitlab的Deploy Key中添加public key
  3. 在jenkins创建项目,添加gitlab的项目地址,选择添加Credential,填入private key

编写构建打包部署的pipeline脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
node {
stage('checkout') {
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: '{git_credentialsId}', url: '{git_url}']]])
}
stage('build') {
sh 'mvn clean package'
}
stage('send war to server') {
sh 'scp {user_root_path}/.jenkins/workspace/{you_project_name}/target/{you_project_packaged_name}.war {user}@{server_ip}:{tomcat_path}/webapps/javavirtual.war'
}
stage('restart tomcat server'){
sh 'echo "wait restart shell"'
}
}

基于角色的任务访问控制

  1. 系统管理->管理插件->安装Role-based Authorization Strategy插件
  2. 系统管理->Configure Global Security->访问控制->授权策略->Role-Based Strategy->保存
  3. 系统管理->Manage and Assign Roles->Manage Roles->Global roles->在admin角色外创建其他角色分配Overall/Read权限
  4. 指定角色访问特定项目

系统管理->Manage and Assign Roles->Manage Roles->Project roles->添加一个角色,然后指定项目Pattern(注意:patter不是正则表达式,其中*要用.*代替)

FAQ

pipeline使用nohup命令启动后台进程无效

参考ProcessTreeKiller

使用Boolean类型参数化构建时,参数逻辑判断总是true

jenkins传入groovy的boolean parameter是对象类型,非null总是true,所以判断总为true,进行一次类型转换就好

1
def booleanParameter = Boolean.valueOf(jenkinsBooleanParameter);

容器的编码问题

Your container doesn’t use UTF-8 to decode URLs. If you use non-ASCII characters as a job name etc, this will cause problems. See Containers and Tomcat i18n for more details.

资源下载

CAS server

CAS client

一、CAS server

1. 生成证书

生成一个别名为castest的证书。

此处需要特别注意口令(后续导入导出证书、CAS服务器端均要用到此口令)和“名字与姓氏”(为CAS跳转域名,否则会报错)

1
keytool -genkey -alias castest -keyalg RSA -keystore {certificate_path}/{certificate_name}

2. 导出证书

1
keytool -export -file {certificate_path}/{certificate_name}.crt -alias castest -keystore {certificate_path}/{certificate_name}

3. 安装证书

将证书导入到客户端JRE中(注意、是导入JRE中),如果security中已经存在cacerts,需要先将其删除。

1
keytool -import -keystore "{jdk_path}\jre\lib\security\cacerts" -file {certificate_path}/cas-test.crt -alias cas-test

4. 配置tomcat

修改%TOMCAT_HOME%/conf/server.xml文件,支持https证书访问

1
2
3
<Connector SSLEnabled="true" clientAuth="false" keystoreFile="{certificate_path}/{certificate_name}" 
keystorePass="{certificate_password}"
maxThreads="150" port="8443" protocol="org.apache.coyote.http11.Http11Protocol" scheme="https" secure="true" sslProtocol="TLS"/>

5. 测试证书

访问https://localhost:8443/,成功则说明配置证书成功

6. 部署CAS server

http://developer.jasig.org/cas/上下载cas服务器端cas-server-4.0.0-release.zip,在modules目录下找到cas-server-webapp-4.0.0.war,将其复制到%TOMCAT_HOME%\webapps下,并将名称改为cas.war

7. 测试CAS server

输入账号和密码

  • casuser
  • Mellon

登录成功则说明CAS server配置成功,可通过https://localhost:8443/logout退出登录

二、CAS client

1. 添加证书映射域名

1
127.0.0.1 sso.cas.com

2. 创建CAS client项目

配置web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<display-name>Archetype Created Web Application</display-name>

<!-- 用于单点退出,该过滤器用于实现单点登出功能,可选配置-->
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>
<!-- 该过滤器用于实现单点登出功能,可选配置。 -->
<filter>
<filter-name>CAS Single Sign Out Filter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS Single Sign Out Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<!-- 该过滤器负责用户的认证工作,必须启用它 -->
<filter>
<filter-name>CASFilter</filter-name>
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<param-value>https://sso.cas.com:8443/cas-server-webapp-4.0.0/login</param-value>
<!--这里的server是服务端的IP-->
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://localhost:8081</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CASFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<!-- 该过滤器负责对Ticket的校验工作,必须启用它 -->
<filter>
<filter-name>CAS Validation Filter</filter-name>
<filter-class>
org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter
</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>https://sso.cas.com:8443/cas-server-webapp-4.0.0/
</param-value><!-- 此处必须为登录url/cas/,带有任何其它路径都会报错,如“https://sso.castest.com:8443/cas/login”,这样也会报错。 -->
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://localhost:8081</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Validation Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<!--
该过滤器负责实现HttpServletRequest请求的包裹,
比如允许开发者通过HttpServletRequest的getRemoteUser()方法获得SSO登录用户的登录名,可选配置。
-->
<filter>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<filter-class>
org.jasig.cas.client.util.HttpServletRequestWrapperFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<!--
该过滤器使得开发者可以通过org.jasig.cas.client.util.AssertionHolder来获取用户的登录名。
比如AssertionHolder.getAssertion().getPrincipal().getName()。
-->
<filter>
<filter-name>CAS Assertion Thread Local Filter</filter-name>
<filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS Assertion Thread Local Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<!-- session超时定义,单位为分钟 -->
<session-config>
<session-timeout>2</session-timeout>
</session-config>
</web-app>

2. 导入CAS client核心jar包

http://developer.jasig.org/cas-clients/上下载cas-client-3.1.12-release.zip,在modules目录下找到cas-client-core-3.1.12.jar、commons-collections-3.2.jar、commons-logging-1.1.jar复制到项目WEB-INF/lib下

3. 测试CAS client

配置好web容器后,启动cas client,访问http://localhost:8081/index.jsp,重定向至cas server(https://sso.cas.com:8443)认证页面,输入用户名casuser及密码Mellon,认证成功后跳回访问页。同时可以在CAS server日志上看到如下的验证信息

访问https://sso.cas.com:8443/cas-server-webapp-4.0.0/logout可以退出单点登录

三、其他认证方式

以上验证的用户名和密码是配置在{CAS_server}/webapp/WEB-INF/deployerConfigContext.xml中的

实际生产中用户认证信息往往配置在数据库中,下面介绍利用数据库配置认证方式

配置数据库认证方式

FAQ

1. service是如何存储的

Configuring CAS client for java in the web.xml

本文介绍使用在php storm上进行断点调试

安装xdebug

1
2
3
4
# 安装xdebug
brew install php56-xdebug
# 查看安装结果
php -i

php storm设置

  1. 配置debug环境,注意端口号要与php.ini中配置的相同(下图为9000端口)

  2. 设置PHP Web Application

    注意要设置下图中的Absolute path on the server

开始使用断点

FAQ

Q1:断点进入时出现了Remote file path is not mapped to any file path in project

这是由于PhpStorm不能确定哪个本地文件与debug监听的文件对应,应该不能定位本地的断点。

可以点击__Click to set up path mappings__按钮设置上文提到的Absolute path on the server,设置完毕即可解决该问题

0%