SegmentFault 苟且偷生最新的文章
2021-11-27T15:56:00+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
Elasticsearch修改字段类型方案
https://segmentfault.com/a/1190000041027143
2021-11-27T15:56:00+08:00
2021-11-27T15:56:00+08:00
大树
https://segmentfault.com/u/mshu
0
<blockquote>Elasticsearch的mapings相当于数据库的表结构,在使用过程中可以新增和删除字段,但是不支持修改字段类型,可以通过以下四个步骤来实现</blockquote><ol><li>创建新的目标index</li><li>将源index的数据复制到目标index</li><li>删除源index</li><li>给目标index设置别名,别名为源index的名称。<br> 或者再建一个名称为源index的目标index2,数据从目标index复制到目标index2</li></ol><hr><p>下面举个例子<img src="/img/bVcWjbi" alt="举个栗子.gif" title="举个栗子.gif"></p><p>源index: my-index-order-1 <br>包含两个字段:</p><ul><li>createTime: 下单时间,类型:long</li><li>orderNo: 订单号,类型:text</li></ul><p>以下操作都是在Kinbana的控制台中执行.</p><pre><code>PUT /my-index-order-1?pretty
{
"mappings": {
"properties": {
"createTime": {
"type": "long"
},
"orderNo": {
"type": "text"
}
}
}
}</code></pre><p>插入三条数据</p><pre><code>PUT /my-index-order-1/_doc/1?pretty
{
"createTime": 1637992973517,
"orderNo": "7d7d5495-4db9-4513-a2c9-c5fb0454517e"
}
PUT /my-index-order-1/_doc/2?pretty
{
"createTime": 1637993092000,
"orderNo": "fb337ede-6e1d-4422-8e2b-453148064bba"
}
PUT /my-index-order-1/_doc/3?pretty
{
"createTime": 1640585092000,
"orderNo": "54ccb3a9-c168-487e-8594-893a2b7803bf"
}</code></pre><h4>需求分析:把my-index-order-1的createTime字段类型从long类型修改成date类型</h4><h4>1. 创建新的目标index</h4><pre><code>PUT /my-index-order-2?pretty
{
"mappings": {
"properties": {
"createTime": {
"type": "date"
},
"orderNo": {
"type": "text"
}
}
}
}</code></pre><h4>2. 将源index的数据复制到目标index</h4><p>reindex 命令可以实现两个index之间数据的拷贝,</p><p>两个index的mappings不同,只会拷贝互相兼容的数据。</p><p>如果复制的数据量比较大,_reindex请求会超时,不要着急,数据拷贝还在继续,<br>可以通过<code>GET _tasks?detailed=true&actions=*reindex</code>命令查询正在执行的任务,<br><code>GET _tasks/taskId:number</code>查询某一个任务的执行详情。</p><p>reindex更多参数参考官方文档:<br><a href="https://link.segmentfault.com/?enc=hxvwhWQAlDXp3Xs4l7cJEw%3D%3D.RG6w1IKcqpnoiHHoTvDUDGNe8uBLvHNCHT2qMBthWMcfLzjJoMODPy73Jpm5bynZRXgi1BR3wzTM9SQqcubx8G1mksicv4kAp91pkzuLPN8%3D" rel="nofollow">https://www.elastic.co/guide/...</a></p><pre><code>POST _reindex
{
"source": {
"index": "my-index-order-1"
},
"dest": {
"index": "my-index-order-2"
}
}</code></pre><h4>3. 删除源index</h4><p><code>DELETE /my-index-order-1/</code></p><h4>4. 给目标index设置别名,别名为源index的名称</h4><p>给my-index-order-2加上my-index-order-1的别名后,可以直接通过my-index-order-1来操作my-index-order-2</p><pre><code>POST _aliases
{
"actions":[
{
"add":{
"index":"my-index-order-2",
"alias":"my-index-order-1"
}
}
]
}</code></pre><hr><p>到此完成mappings字段类型的修改。可以愉快的对createTime做时间的统计查询了</p><p>比如统计每个月的下单量:</p><pre><code>GET /my-index-order-2/_search?pretty
{
"size": 0,
"aggs": {
"orderCount": {
"date_histogram": {
"field": "createTime",
"calendar_interval": "1M",
"format": "yyyy-MM"
}
}
}
}</code></pre><p>查询结果:</p><pre><code>{
"took": 27,
"timed_out": false,
"_shards": {
....
},
"hits": {
....
},
"aggregations": {
"orderCount": {
"buckets": [
{
"key_as_string": "2021-11",
"key": 1635724800000,
"doc_count": 2
},
{
"key_as_string": "2021-12",
"key": 1638316800000,
"doc_count": 1
}
]
}
}
}</code></pre><hr><p>Elasticsearch 版本号: 7.15.2<br>Kibana 版本号: 7.15.2<br>Elasticsearch 中文官网 <a href="https://link.segmentfault.com/?enc=%2BM2%2BlwqU0KYSD%2FlygIWxDQ%3D%3D.rM3ZaAByo3OR9d2Al2%2BHt9dUG0v6pYM4rWpC3H%2BElSQ%3D" rel="nofollow">https://www.elastic.co/cn/</a></p>
Mybatis新手进阶知识点,老鸟请走开
https://segmentfault.com/a/1190000040647422
2021-09-08T00:35:45+08:00
2021-09-08T00:35:45+08:00
大树
https://segmentfault.com/u/mshu
2
<blockquote>ORM全称:object relation mapping,译为:对象关系映射。<br>ORM框架是将对象和数据库表字段建立映射,并提供CRUD操作的API的框架。</blockquote><p>Java原生的与数据库连接的方式是JDBC,每次操作需要以下6个步骤</p><ol><li>加载数据库驱动</li><li>创建连接</li><li>创建一个Statement</li><li>执行SQL</li><li>处理结果集</li><li>关闭连接</li></ol><p>原生的方式步骤繁琐,开发效率低,市面上有很多的优秀的ORM框架:</p><ol><li><a href="https://link.segmentfault.com/?enc=mkbTQsS%2FwUxHjV2nyK6LTA%3D%3D.XIMKE9b1GnFs8rgsvJyqiKr6b0coqIC2B%2FDr%2Fh4nV7c%3D" rel="nofollow">Hibernate</a> 全自动ORM框架,弱化sql, 甚至不需要考虑建表,Hibernate会根据对象生成表甚至中间表。CURD一般不需要写sql。用起来方便,用好很难,有些老的项目还在用。</li><li><a href="https://link.segmentfault.com/?enc=WJXUBghWtSK4jp1AHoFAPw%3D%3D.EtvhxamYzXEpX22rIWU4WFFRVH%2BwczZjZWiTo3rEog4akMVzmKllg0BTJ1ZXlOTk" rel="nofollow">Mybatis</a>半自动ORM框架,本文的主角,被广泛使用,它支持自定义 SQL、存储过程以及高级映射。前身是<a href="https://link.segmentfault.com/?enc=os6zq4DutlqxgeZ8jJ6dew%3D%3D.3i0PEEBmLFjiwVH3AoSAeHaOOt1jkGRQ%2FvbCsBR92k4%3D" rel="nofollow">ibatis</a>, 另外还有一个在此基础上封装的号称为简化开发而生的框架<a href="https://link.segmentfault.com/?enc=mSXp1GJCo8ZhSLKHi19psg%3D%3D.OdRQa6u3nGL%2FgI0rop6Wdfgd3q5TTA0V4vypuoVVxVk%3D" rel="nofollow">MyBatis-Plus</a>。提供通用的CURD,一般操作不需要写sql。</li><li><a href="https://link.segmentfault.com/?enc=riA48BrWQx1iZ1qt%2FX1vLg%3D%3D.Gy23qr7IXZh0j7w6e0zi8wksaqqWOAWSWM9R%2FgkTW1SJp3pvj73OCtbeqmw0pYUFxV0FQhs1cXzCbtNAL8Ugg%2FmrnCpBZCD66kThgi2iafY%3D" rel="nofollow">JPA</a>是大spring旗下ORM框架,特点是根据方法名就能自动实现方法逻辑,你敢信?不信可以看看这篇文章<a href="https://segmentfault.com/a/1190000014269284">《简单才是美! SpringBoot+JPA</a>》</li></ol><p>下面将介绍一些<strong>mybatis</strong>新手进阶知识点,老鸟请走开🤦♂️</p><h3>嵌套查询</h3><blockquote>在<code>resultMap</code>中嵌套一个查询。通过标签<code><association></code>的<code>select</code>属性完成。<code>select</code>的值是另一个<code><select></code>查询的id,<code>column</code>属性为关联字段,用来实现关联查询。</blockquote><ol><li><p>根据user_id查询user</p><pre><code class="xml"> <select id="getUserWithAddress2" resultMap="BaseResultWithAddress2Map">
select * from `user` where user_id = #{userId}
</select></code></pre></li><li><p><association>中嵌套一个id为selectAddressByUserId的查询,查询这个用户的地址。</p><pre><code class="xml"> <resultMap id="BaseResultWithAddress2Map" type="com.mashu.springmybatis.entity.UserWithAddress">
<id column="user_id" property="userId" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="email" property="email" jdbcType="VARCHAR"/>
<association property="address" column="user_id" javaType="com.mashu.springmybatis.entity.Address"
select="selectAddressByUserId">
</association>
</resultMap></code></pre></li><li><p>id为selectAddressByUserId的查询:根据用户id查询地址详情:</p><pre><code class="xml"> <select id="selectAddressByUserId"
resultMap="com.mashu.springmybatis.mapper.AddressMapper.BaseResultMap">
select * from address where user_id = #{userId}
</select></code></pre></li></ol><h3>嵌套结果</h3><blockquote>上面的查询会有N+1的问题,就是执行两遍查询,可以使用联表查询解决这个问题,结果集同样是使用<code><resultMap></code>映射,<code><association></code>标签+<code>resultMap</code>属性。具体写法如下:</blockquote><ol><li><p>association标签的resultMap属性指向address的resultMap</p><pre><code class="xml"><resultMap id="BaseResultWithAddressMap" type="com.mashu.springmybatis.entity.UserWithAddress">
<id column="user_id" property="userId" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="email" property="email" jdbcType="VARCHAR"/>
<association property="address" javaType="com.mashu.springmybatis.entity.Address"
resultMap="com.mashu.springmybatis.mapper.AddressMapper.BaseResultMap">
</association>
</resultMap></code></pre></li><li><p>联表查询sql</p><pre><code class="xml"> <select id="getUserWithAddress" resultMap="BaseResultWithAddressMap">
select * from `user` u join address a on u.user_id and a.user_id where u.user_id = #{userId}
</select></code></pre></li></ol><p>还可以一对多的映射,将<code><association></code>换成<code><collection></code>,实现一个人有多个女朋友的一对多关联查询。<br>myabtis会自动合并重复的user,girlFriends作为集合映射到user的girlFriends属性。</p><pre><code class="xml"> <resultMap id="BaseResultWithGirlFriendsMap" type="com.mashu.springmybatis.entity.UserWithGirlFriends">
<id column="user_id" property="userId" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="email" property="email" jdbcType="VARCHAR"/>
<collection property="girlFriends" ofType="com.mashu.springmybatis.entity.GirlFriend">
<id column="girl_friend_id" property="girlFriendId" jdbcType="INTEGER"/>
<result column="user_id" property="userId" jdbcType="VARCHAR"/>
<result column="girl_friend_name" property="name" jdbcType="VARCHAR"/>
<result column="age" property="age" jdbcType="INTEGER"/>
<result column="weight" property="weight" jdbcType="INTEGER"/>
<result column="height" property="height" jdbcType="INTEGER"/>
</collection>
</resultMap></code></pre><h3>懒加载</h3><p>除了联表查询解决N+1的问题,mybatis的懒加载似乎更好,拿第一个嵌套查询的栗子来说,如果开启了懒加载,<br>在不使用address的时候,只会执行查询user的sql,不会执行查询address的sql。<br>只有在使用中get了address属性才会执行查询address的sql,使用起来也很见简单:</p><ol><li><p>yml配置</p><pre><code class="yml">mybatis:
mapper-locations: classpath:mapper/*Mapper.xml
configuration:
##开启懒加载
lazy-loading-enabled: true
##false:按需加载
aggressive-lazy-loading: false
##触发加载的方法名
lazy-load-trigger-methods:</code></pre></li><li><p><association>加上<code>fetchType="lazy"</code>的属性即可。</p><pre><code class="xml"> <resultMap id="BaseResultWithAddress2Map" type="com.mashu.springmybatis.entity.UserWithAddress">
<id column="user_id" property="userId" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="email" property="email" jdbcType="VARCHAR"/>
<association fetchType="lazy" property="address" column="user_id" javaType="com.mashu.springmybatis.entity.Address"
select="selectAddressByUserId">
</association>
</resultMap></code></pre></li></ol><p>但是,实现懒加载的问题比较多😢:</p><ol><li>如果报错No serializer found for class org.apache.ibatis.executor.loader.javassist.JavassistProxyFactory..<br>序列化问题需要在实体类上添加注解<code>@JsonIgnoreProperties(value = {"handler"})</code></li><li>如果懒加载失败:检查是否是lombok中的@Data注解的toString()导致的</li><li>检查全局配置是否正确</li><li>还有在idea失败,在eclipce成功的。。。</li></ol><h3>一二级缓存</h3><blockquote>一级缓存,一次请求查询两次数据,第二次从缓存中取,mybatis默认开启<br>二级缓存,多次请求查询同一个数据,都能从缓存中取,需要手动开启</blockquote><ol><li><p>开启全局配置:</p><pre><code class="yml">mybatis:
mapper-locations: classpath:mapper/*Mapper.xml
##开启二级缓存
cache-enabled: true</code></pre></li><li><p>添加useCache="true"属性。</p><pre><code class="xml"> <select id="selectByCache" useCache="true" resultMap="BaseResultMap" parameterType="java.lang.Integer">
select user_id, name, email
from user
where user_id = #{userId,jdbcType=INTEGER}
</select></code></pre></li></ol><h3>类型处理器</h3><blockquote>有时候我们在入库和出库的时候对字段做一些处理,<br>比如不支持utf8mb4的数据库存储emoji表情之前需要转义成utf8支持的unicode字符编码,出库后需要转化成emoji表情。<br>又或者用户的密码不能明文保存到数据库,入库需要进行一些加密操作。<br>mybatis的类型处理器,就可以在入库和出库前对数据做一些操作。<br>下面举个栗子将邮箱入库Base64加密,出库Base64解密。</blockquote><ol><li><p>自定义类型处理器类继承<code>BaseTypeHandler</code>抽象类。</p><pre><code class="java">public class MyHandler extends BaseTypeHandler<String> {
//入库加密
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int i, String s, JdbcType jdbcType) throws SQLException {
preparedStatement.setString(i,Base64Utils.encodeToString(s.getBytes()));
}
//出库解密
@Override
public String getNullableResult(ResultSet resultSet, String columnName) throws SQLException {
String column = resultSet.getString(columnName);
return new String(Base64Utils.decode(column.getBytes()));
}
@Override
public String getNullableResult(ResultSet resultSet, int i) throws SQLException {
System.out.println(resultSet);
return null;
}
@Override
public String getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
System.out.println(callableStatement);
return null;
}
}</code></pre></li><li><p>字段添加<code>typeHandler</code>属性,并指向自定义类型处理器类的路径</p><pre><code class="xml"> <resultMap id="BaseResultMap" type="com.mashu.springmybatis.entity.User">
<id column="user_id" property="userId" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="email" property="email" jdbcType="VARCHAR" typeHandler="com.mashu.springmybatis.config.MyHandler"/>
</resultMap></code></pre></li></ol>
Docker安装Jenkins实现自动化部署Maven项目
https://segmentfault.com/a/1190000040090207
2021-05-31T01:47:32+08:00
2021-05-31T01:47:32+08:00
大树
https://segmentfault.com/u/mshu
3
<blockquote>Jenkins version 2.277.4<br>Docker version 20.10.5</blockquote><p>Jenkins中文官网-><a href="https://link.segmentfault.com/?enc=YaOxs9mN3zAoQEJ0m03sMw%3D%3D.OlA2Wt9vNsiUdygaC%2BagbN31U7zneqxX0MLAywjN9vg%3D" rel="nofollow">https://www.jenkins.io/zh/</a></p><h3>安装Jenkins</h3><p>docker 安装一切都是那么简单,注意检查8080是否已经占用!</p><pre><code>docker run --name jenkins -u root --rm -d -p 8080:8080 -p 50000:50000 -v jenkins-data:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock jenkinsci/blueocean</code></pre><p>如果没改端口号的话<br>安装完成后访问地址-> <code>http://{部署jenkins所在服务IP}:8080</code></p><h3>初始化Jenkins</h3><p>详情见官网教程-><a href="https://link.segmentfault.com/?enc=bH%2BnHdvPFMkqLl1nTN%2Btvg%3D%3D.rwfEz96TRrV%2BmETwFBs0Yp4%2FDqWoUrtyfwsQE67Dfb39cqhlIapUxOzavQK6rMwygT7mZ9rXxempTHgD9u1k6w%3D%3D" rel="nofollow">https://www.jenkins.io/zh/doc...</a></p><h3>第一个简单的任务</h3><p>小试牛刀,先创建简单的任务,任务内容:执行服务器的shell脚本或Linux命令。<br><strong>由于jenkins 部署在docker容器内,没办法直接执行宿主机上的shell脚本</strong>,需要ssh登录到宿主机上执行。这就需要<strong>Publish Over SSH</strong>插件。(如果Jenkins不是用docker部署的就不会有这个烦恼)同样的道理,如果jenkins和项目不在一台服务器也可以使用这个插件,远程拷贝打包的文件或者执行脚本等。</p><h4>安装插件</h4><p>首页->系统管理->插件管理->搜索Publish Over SSH并安装.<br><img src="/img/bVcSnp2" alt="image.png" title="image.png"></p><h4>配置 Publish Over SSH</h4><p>首页->系统管理->系统配置-><br><img src="/img/bVcSnp1" alt="image.png" title="image.png"></p><h4>创建任务</h4><p>首页->新建任务->填写任务名称->选择:构建一个自由风格的软件项目<br>直接在切到【构建】选项卡,点击【添加构建步骤】选择<code>Send files or execute commands over SSH</code><br>在SSH service下面选择刚刚在【系统配置】配置的服务器。<br>Exec command一栏直接输入命令即可,不妨可以试试<code>echo $(pwd)</code>命令。<br><img src="/img/bVcSnqe" alt="image.png" title="image.png"></p><p>保存,第一个任务建成功了,回到任务详情页,点击立即构建,找到【控制台输出】可以看到执行详情。</p><hr><h3>创建一个自动化部署maven项目的任务</h3><p>原理:jenkins用git插件将项目拉下来,用Maven Integration插件打包,用Publish Over SSH插件将打包的jar或者文件夹发送到部署项目的服务器,并执行shell脚本启动~</p><p>先决条件:</p><ol><li>git插件:在初始化的时候就默认安装的;</li><li>Maven Integration插件:安装方法同上;</li><li>Maven配置:首页->系统管理->全局工具配置,勾选自动安装,选择maven版本即可;</li><li>Publish Over SSH: 创建上一个任务的安装/配置过了;</li></ol><h4>git拉取代码</h4><p>同样创建任务,来到配置页面,切到【源码管理】选项卡配置仓库地址和密钥:</p><p><img src="/img/bVcSnqW" alt="image.png" title="image.png"></p><p>这个时候可以保存并点击<strong>立即构建</strong>看看代码能否拉下来。</p><h4>Maven 打包</h4><p>切到【构建】选项卡,点击【添加构建步骤】选择“<code>调用顶层 Maven 目标</code>”</p><p>maven版本选择在【全局工具配置】里面配置的maven,如果没有就是你不配,不,是你没配!<br>目标一栏填写打包命令:clean install -Dmaven.test.skip=true,或者根据情况填写。<br><img src="/img/bVcSnq2" alt="image.png" title="image.png"></p><p>这个时候可以保存并点击<strong>立即构建</strong>看看代码能否正常打包。</p><h4>运行启动脚本</h4><p>代码拉下来了,jar也打包好了,但是jar包在容器里面,可以在【构建】模块添加个<code>Send files or execute commands over SSH</code>,使用<strong>Source files</strong>和<strong>Remote directory</strong>传输jar文件,但是我部署jenkins的docker和部署项目的服务器是同一台,使用docker cp 命令就可以将docker容器里面的jar文件拷贝出来,并和启动项目的脚本写在一起。就省去了文件传输,直接执行脚本即可。<br>docker cp详见<a href="https://segmentfault.com/a/1190000020027077">《蛮吉学 Docker》</a></p><p><img src="/img/bVcSnq7" alt="image.png" title="image.png"></p><p>一个自动化部署maven项目的任务就创建完了</p><hr><h4>Send files or execute commands over SSH的文件传输功能</h4><p><strong>Source files(任务的工作空间目录)</strong>:就是代码拉下来的根目录,如果要传输文件夹用<code>**</code>表示<br><strong>Remote directory(登录项目服务器的家目录)</strong>:ssh登录的家目录,比如root登录这个目录就是/root,且文件只能传输到这个目录下或这个目录的下级目录!<br><strong>Exclude files</strong>:不传输的文件。可以过滤不需要的文件比如<code>README.md</code>和<code>.gitignore</code><br>多个用逗号隔开,保证<strong>Pattern separator</strong>配置的是<code>[, ]+</code></p><p><img src="/img/bVcSnrf" alt="image.png" title="image.png"></p><h3>注意事项</h3><ol><li>【系统配置】里配置Publish Over SSH,SSH服务器的登录用户最好用root,否则执行脚本可能会权限不足!</li><li>Send files or execute commands over SSH 的文件传输功能配置的<strong>Remote directory</strong>只能是用户的家目录!</li><li>docker部署的Jenkins不能直接运行宿主机上的shell脚本,且拉取的代码,打包的文件都在docker容器内!要借助Publish Over SSH插件。</li><li>宿主机不需要安装git、maven!</li><li>如果直接执行启动jar的脚本正常,Jenkins执行脚本报错<code>nohup: failed to run command java: No such file or directory</code>,前面加一行<code>source /etc/profile</code>可以解决。</li><li><img src="/img/bVcSnsl" alt="image.png" title="image.png"></li></ol>
1024程序员节,快来领取你的终身免费云服务器
https://segmentfault.com/a/1190000037593220
2020-10-24T23:08:56+08:00
2020-10-24T23:08:56+08:00
大树
https://segmentfault.com/u/mshu
4
<p><img src="/img/bVcHTRU" alt="image.png" title="image.png"></p><p>做了程序员之后你是否想要一台云服务器放上自己开发的小东西?</p><p>无论是有个好的想法、还是纯粹是练技术,将自己的作品放到云服务器上,可以在公网上访问都是很不错的。</p><p>阿里云和腾讯云都有云服务器在租售,根据配置价格不等,对于我这种并不是很刚需的用户来说,能有个免费的服务器就再好不过了。</p><p>我曾经在亚马逊AWS云服务上申请了一年的免费服务器,接了私活开发完放上去,每月再收取点维护费。</p><p>前几天我又发现了一个终身免费的云服务器,已经申请到手,正在开发部署自己的小想法。</p><h3>码云Gitee Pages服务</h3><hr><p>先介绍一款"<strong>静态服务器</strong>"热热身,码云的Pages服务,代码托管国外的github和国内的码云都有Pages功能,能够部署静态页面并在公网上可访问,可以用于制作在线简历、个人博客、项目官网等。</p><h4>使用方法:</h4><ol><li>将项目上传到码云仓库</li><li>启动Gitee Pages服务</li></ol><p><img src="/img/bVcHTTc" alt="image.png" title="image.png"></p><p>如上图,点击 <strong>服务</strong> - <strong>Gitee Pages</strong>,然后选择分支和目录,启动即可。</p><p>仓库必须有 index.html 才可以正常访问。</p><p>官方教程:<a href="https://link.segmentfault.com/?enc=3nR9cKO29vtDeOc5hL5y0g%3D%3D.UPH%2ByIOPAM0UGVa0Ff54bRR%2FzKuksAaM1ZYZklQC%2FeTx0UU9DFgKtXFfCgO49EMLhm8%2BiTNuP11%2BFBEiw0r9Mg%3D%3D" rel="nofollow">Gitee Pages帮助中心</a></p><p>介个是我搞得静态页面-聚合搜索-->,<a href="https://link.segmentfault.com/?enc=j9nhfWrO4AAJpsP69onMvg%3D%3D.sEf3%2BD0Yi4SUO2H1HpKY9sLiOe6UB4g%2B6cubFcbHftg%3D" rel="nofollow">猕猴桃搜索</a></p><p>猕猴桃搜索:如今百度搜索越来越不让人满意,还有各种负面消息,可谓是在作死的边缘疯狂试探,我做了一个聚合搜索,结合了好些搜索引擎,输入要搜索的内容,然后点击想用的搜索引擎即可。</p><h4>你可能还需要</h4><h5>1.嵌入音乐播放器</h5><p>有些博主的个人网站一打开有音乐播放,你也可以,<a href="https://link.segmentfault.com/?enc=SZhfUyXYZTVcREgBHmKB0w%3D%3D.un9UbrMS3AfVJgIWSMLufltzIqejlGNdK9kYZpsEZrE%3D" rel="nofollow">网易云</a>的歌单上都会有生成外链播放器的<strong>链接</strong>,点击后有一串代码,只要放到你的html页面上即可。<br>例如:</p><pre><code class="HTML"><iframe frameborder="no" border="0" marginwidth="0" marginheight="0"
width=330 height=450
src="//music.163.com/outchain/player?type=0&id=5297835171&auto=1&height=430">
</iframe></code></pre><p>可以调节长宽和播放模式,<code>auto</code>等于1自动播放,等于0不自动播放</p><h5>2. Bing壁纸</h5><p><a href="https://link.segmentfault.com/?enc=qD4YkZMteV%2B6ruC%2BS3m9Uw%3D%3D.xfN%2B6eWn%2FS6IQUzhRj7cPkuaaOKCox%2BkwbAAW4ZL0sw%3D" rel="nofollow">必应官网</a>每天会更新一张高质量的背景图。你也可以,<br>官方API:<a href="https://link.segmentfault.com/?enc=hkee0LRnMRjxN%2BTI3ctvLQ%3D%3D.NaP897V3jB9i33wCKAb8V8fwlog3u66AqxiydZT5HO6PGj%2Fv71crX1oYlVosQY43Qvc90j74n01xl6m19%2FK%2BQi4RAQPTGPBIdpxbVOp03TXR%2F5leDtzR2CvcBkf5%2FhUf" rel="nofollow">https://cn.bing.com/HPImageAr...</a></p><p><strong>Response:</strong></p><pre><code class="JSON">{
"images": [
{
"startdate": "20201023",
"fullstartdate": "202010231600",
"enddate": "20201024",
"url": "/th?id=OHR.UNBuilding_ZH-CN7730281645_1920x1080.jpg&rf=LaDigue_1920x1080.jpg&pid=hp",
"urlbase": "/th?id=OHR.UNBuilding_ZH-CN7730281645",
"copyright": "纽约市的天际线与联合国总部大楼 (© Sean Pavone/Alamy)",
"copyrightlink": "https://www.bing.com/search?q=%E8%81%94%E5%90%88%E5%9B%BD%E6%80%BB%E9%83%A8%E5%A4%A7%E6%A5%BC&form=hpcapt&mkt=zh-cn",
"title": "",
"quiz": "/search?q=Bing+homepage+quiz&filters=WQOskey:%22HPQuiz_20201023_UNBuilding%22&FORM=HPQUIZ",
"wp": true,
"hsh": "5136e9f5a7272097e07fab9f3e299de4",
"drk": 1,
"top": 1,
"bot": 1,
"hs": []
}
],
"tooltips": {
"loading": "正在加载...",
"previous": "上一个图像",
"next": "下一个图像",
"walle": "此图片不能下载用作壁纸。",
"walls": "下载今日美图。仅限用作桌面壁纸。"
}
}</code></pre><p>前缀 <code>https://cn.bing.com/</code> 拼接上<code>images</code>节点下的<code>url</code>的值即可得到当天的图片地址。</p><p>如:<br><a href="https://link.segmentfault.com/?enc=OdrXVq0N0fR5otvZ9pEdlA%3D%3D.R94lOwH2z%2FJ2bP75AD5sq%2BouJVXn4B1b2PNDFAieBMk%2FioCvrW6cfweXNTy9UjgyJ3n2dcGNBT6rRLLfcMEG8jOnat1N%2FMJh080wemTp88xCejkPheS1GigJKMDHBPt%2F50%2F7K0Mz1XGl7IlqfptnqA%3D%3D" rel="nofollow">https://cn.bing.com/th?id=OHR...</a></p><h3>终身免费的云服务器</h3><hr><p><img src="/img/bVcHTOs" alt="image.png" title="image.png"></p><p>甲骨文公司提供了终身免费的云服务器,一看到这个消息,我就立马申请了,发现好多人早已经申请过了,这我不能落后。</p><p>官网地址:<a href="https://link.segmentfault.com/?enc=ytP5NBj3zFbbxPHEtycUNA%3D%3D.wh4vI5jy6msX6eKlZ6SZsomsBE32at2jES3KRS79LFSBrsduIV%2FAbqRRB2jVEGBE" rel="nofollow">Oracle 云免费套餐</a><br>注册后即可领取,可以免费申请两个服务器实例,</p><p><img src="/img/bVcHTNT" alt="image.png" title="image.png"></p><h4>服务器申请</h4><p>服务器没有国内的,都说韩国(Korea)和日本(Japan)的服务器速度快一点.</p><p><strong>注意事项</strong>:</p><ol><li>你需要一张VISA信用卡:可以在国外使用的信用卡,一般你办信用卡的时候都会有两张,其中一张就是VISA卡。</li><li>注册的时候家乡地址(Home Region)尽量选择韩国(Korea)或日本(Japan),这样才可以申请这个国家的服务器。</li></ol><p><img src="/img/bVcHUbU" alt="image.png" title="image.png"></p><p>申请时候按照步骤一步一步的填写就好,完了之后会在你信用卡上扣除1新加坡元,然后返还,应该是测试你的信用卡是否可以正常使用。之后也会发生一两次扣除返还,不用管他。</p><h4>服务器连接</h4><p>点击<strong>创建VM实例</strong>,选择操作系统后,需要使用密钥的方式连接服务器,不能使用账号密码。<br>本地可以用户xshell创建密钥对,然后将密钥文件上传,<br>xshell新建连接-输入服务器ip,选择刚刚创建的用户密钥即可。</p><p><img src="/img/bVcHUbm" alt="image.png" title="image.png"></p><h4>添加入站规则</h4><p>服务器默认只开了一个22端口,如果你安装了nginx、redis等服务,想通过外界访问或连接,除了开启<strong>防火墙</strong>外,还需要在你的实例上配置入站规则:<br>实例》虚拟云网络》虚拟云网络详细信息》安全列表<br><img src="/img/bVcHUeR" alt="image.png" title="image.png"></p><hr><p>下面是我的免费小鸡,我正在准备搞个自己的项目上去!<br><img src="/img/bVcHUaZ" alt="image.png" title="image.png"></p><p><img src="/img/bVcHTP0" alt="image.png" title="image.png"></p><hr><p>有了国外服务器你就可以打开世界之窗了-> <a href="https://segmentfault.com/n/1330000038780045">影梭安装和使用</a><br><img src="/img/bVcMSF6" alt="image" title="image"></p>
JAVA三年面试总结,金九银十,你准备好了吗?
https://segmentfault.com/a/1190000024418969
2020-09-11T00:11:07+08:00
2020-09-11T00:11:07+08:00
大树
https://segmentfault.com/u/mshu
18
<p><img src="/img/bVbOCEt" alt="image.png" title="image.png"></p><h2>网络</h2><h3>三次握手的原因?</h3><blockquote><p>三次握手TCP协议建立连接的过程。<br><strong>原因或目的是为了证明客户端和服务端都有发送和接收的能力。</strong></p><p><strong>原理</strong>:<br>第一次:客户端发送SYN包给服务端<br>第二次:服务端接收后在SYN包中的序列号+1 (即SYN+ACK包) 发送给客户端<br>第三次:客户端收到服务端的SYN包后,在SYN包中的序列号+1后 (ACK包) 发送给服务端</p></blockquote><h3>为啥四次挥手?</h3><blockquote>四次挥手是TCP释放连接的过程。<br>原因:客户端和服务端会各自发送1次和回复1次,共4次</blockquote><h3>tcp/udp分别的应用场景?</h3><blockquote>tcp准确度高,适用于文件传输,电子邮件等场景<br>udp效率比较高,适用于直播,网络语音等场景</blockquote><h2>Java</h2><h3>ArrayList和LinkedList的区别、扩容机制以及底层实现</h3><blockquote><p>ArrayList基于数组实现,由于使用下标查询,所以查询比较快,增删数据会移动数据,所以增删略慢<br>扩容:数组是定长的,ArrayList是通过复制到新的数组来实现动态扩容。默认长度10,扩容1.5倍</p><p>LinkedList基于双向链表实现,插入元素只记录前一个元素和后一个元素,所以插入比较快。<br>不需要扩容。</p></blockquote><h3>ArrayList和LinkedList的线程安全解决办法?</h3><blockquote><ol><li>Collections.synchronizedList(new LinkedList<Object>())和Collections.synchronizedList(new ArrayList<Object>())</li><li>Vector : 基于数组+synchronized</li><li>CopyOnWriteArrayList、ConcurrentLinkedQueue</li></ol></blockquote><h3>Set为什么不可重复?</h3><blockquote>set是无序不可重复的,底层使用了map, 比较key值来判断是否重复</blockquote><h3>Set怎么实现有序?</h3><blockquote>HashSet是无序的,<br>但是LinkedHashSet能保证元素添加的顺序,TreeSet能保证元素自然的顺序<br>如果想要自定义排序规则:<br> 1.使用TreeSet存储<br> 2.TreeSet内的元素需要实现Comparable接口,重写compareTo方法自定义排序规则。</blockquote><h3>HashMap、ConcurrentHashMap、HashTable的原理和区别?</h3><blockquote><p><strong>HashMap的介绍</strong>:<br>HashMap在JAVA8之后的结构是:<strong>数组(默认16个)+单向链表+红黑树</strong><br>数组的每个元素对应一条链表,存储的是那条链表的头节点<br>数据存入的时候,对key做hash运算,计算出在数组中的下标,并存入该下标元素对应的链表中<br>当链表的长度超过8后转化为红黑树,当红黑树的元素少于6后转化为链表<br>扩容触发条件:HashMap的长度>容量<em>加载因子(16</em>0.75),<br>扩容大小:2倍</p><p><strong>区别</strong>:<br>HashMap线程不安全,key可以为null,HashTable和ConcurrentHashMap线程安全,key不可以为null<br>HashTable使用数据+链表的结构,并加synchronize锁保证线程安全,<br>ConcurrentHashMap在HashMap的基础上使用了CAS+synchronize来保证线程安全。<br>HashTable锁住整个对象,效率偏低。ConcurrentHashMap只锁住数组的每个元素,锁的粒度更细,效率较高。</p></blockquote><h3>sleep和wait的区别?</h3><blockquote>1.sleep()是 Thread类的方法,wait()是 Object类中定义的方法<br>2.sleep()方法可以在任何地方使用,wait方法只能在 synchronized方法或synchronized块中使用<br>3.sleep()不会释放锁,wait()会。</blockquote><h3>什么是线程安全?</h3><blockquote>在多个线程操作访问某一个方法时,对资源的更改操作不会产生问题<br>实现方法:<br>1.<strong>synchronized</strong>:自动加锁释放锁<br>2.<strong>ReentrantLock</strong>:手动加锁释放锁<br>3.如果是集群结构,需要使用分布式锁</blockquote><h3>线程越多越好吗,为什么?</h3><blockquote>1.根据系统配置有关,<br>2.以一核为例,多线程其实是竞争一个线程的执行权,交替执行</blockquote><h3>有几种线程池,线程池的好处,哪些核心参数?</h3><blockquote><p><strong>Java通过Executors提供四种线程池</strong>:<br>1.newCachedThreadPool 创建一个可缓存线程池,可灵活回收空闲线程,若无可回收,则新建线程<br>2.newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待<br>3.newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行<br>4.newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务</p><p><strong>线程池的好处</strong>:使用线程池可以减少在创建和销毁线程的消耗,并提高线程的可管理性,且提供队列以及拒绝策略等功能。</p><p><strong>核心参数</strong>:<br>1.corePoolSize 线程池中核心线程的数量<br>2.maximumPoolSize 线程池中最大线程数量<br>3.keepAliveTime 非核心线程的最大空闲时间<br>4.workQueue 线程池中的任务队列<br>5.handler 拒绝策略</p></blockquote><h3>什么情况会触发拒绝策略?</h3><blockquote>当线程池的<strong>核心线程数+任务缓存队列已满</strong>并且<strong>线程池中的线程数目达到最大线程数量</strong>时</blockquote><h3>ThreadLocal的使用场景?</h3><blockquote>ThreadLocal是线程之间互相隔离的变量,我们用ThreadLocal维护本线程的simpleDateFormat。</blockquote><h3>自己写一个String类,包名也是java.lang会是怎样?</h3><blockquote><p>手写的String类无效,会被真正的String覆盖。<br>而且在手写的String类中写个方法并调用,会报错:Stirng 没有该方法。</p><p>java类的周期可分为:<strong>加载,连接(验证、准备、解析),初始化、使用、卸载</strong>。<br>其中类加载时候的机制:JVM有四个类加载器:<br>1.<strong>启动类加载器</strong>(Bootstrap ClassLoader):加载核心包<br>2.<strong>扩展类加载器</strong>(Extension ClassLoader):加载扩展包<br>3.<strong>应用程序类加载器</strong>(Application ClassLoader):加载我们写的程序和引用的jar包<br>4.<strong>自定义加载器</strong>(Custom ClassLoader):加载自定义的包</p><p>类的加载的时候会按照上面的顺序,至下而上的检查有没有被加载,然后<strong>至上而下的加载</strong>,由于java自身的String <strong>优先</strong> 被启动类加载器加载,所以手写的java.lang.String 无效。<br>以上也是类加载的双亲委派机制</p></blockquote><h3>讲讲JVM内存模型?</h3><blockquote><strong>堆,元空间,本地方法栈,虚拟机栈,程序计数器</strong> (前面两个线程共享)<br>程序计数器:记录程序执行时的行数<br>虚拟机栈:存储对象的引用,8种基础类型,局部变量表,操作栈,动态链接,方法出口等信息<br>堆:存储对象实例<br>元空间:存储类的信息、方法、属性、常量、静态变量、常量池<br>本地方栈:存储native方法的信息</blockquote><h3>讲讲垃圾回收机制和算法?</h3><blockquote><p><strong>垃圾回收机制:</strong><br>一般情况下,一个对象创建后存在堆内存中年轻代的伊甸区,年轻代分为伊甸区和两个幸存区,对象经过回收从伊甸区移动到幸存区,再经历N次回收后,最终存活的对象移动到老年代。</p><p><strong>垃圾回收算法:</strong><br>年轻代使用的垃圾回收算法是复制算法,原因是年轻代大部分被回收,只需要复制少量存活的对象<br>老年代使用的垃圾回收算法是标记清除算法和标记整理算法。</p><p><strong>垃圾回收触发条件</strong><br>伊甸区满了触发Minor GC ,对年轻代回收<br>老年代满了触发Full GC ,对整个对内存回收</p><p>JVM调优目的:减少Full GC</p></blockquote><p><a href="https://segmentfault.com/a/1190000022827507">《从main方法分析内存溢出》</a></p><h3>什么对象会被认为是垃圾并回收掉?</h3><blockquote>1.引用计数算法:对象被引用的个数为0的会被回收<br>2.可达性算法:与引用链的无关联的对象会被回收</blockquote><h3>排序方法有哪些?</h3><blockquote>1.选择排序<br>2.冒泡排序<br>3.快速排序<br>4.选择排序<br>5.插入排序</blockquote><h3>数据结构有哪些</h3><blockquote>数组(Array):含有下标<br>栈(Stack):先进后出<br>队列(Queue):先进先出<br>链表(Linked List):手拉手<br>树(Tree):倒挂的树,有根节点和叶子节点这样式的。<br>散列表(Hash):key和value<br>堆(Heap):<br>图(Graph):</blockquote><h2>数据库</h2><h3>事务的隔离级别以及每个级别的会引发的问题?</h3><table><thead><tr><th>事务隔离级别</th><th>脏读</th><th>不可重复</th><th>幻读</th></tr></thead><tbody><tr><td>读未提交</td><td>发生</td><td>发生</td><td>发生</td></tr><tr><td>读已提交(oracle默认级别)</td><td>避免</td><td>发生</td><td>发生</td></tr><tr><td>读可重复(mysql默认级别)</td><td>避免</td><td>避免</td><td>发生</td></tr><tr><td>串行化</td><td>避免</td><td>避免</td><td>避免</td></tr></tbody></table><p><strong>脏读</strong>:A事务读取到了B事务未提交的数据,如果B事务回滚了,A读到的数据就是脏数据。<br><strong>不可重复读</strong>:A事务读取到了B事务已提交的数据,在A事务中,前后两次读取的数据不一致的现象。(B事务已提交的修改数据导致的)。<br><strong>幻读</strong>:A事务读取到了B事务已提交的数据,在A事务中,前后两次读取的数据<strong>量</strong>不一致的现象(B事务已提交的新增/删除数据导致的)。<br>幻读和不可重复读有些类似,但是幻读强调的是集合的增减,而不是单条数据的更新。</p><h3>建一个索引,使用Like查询,左右两边都加%。索引会起作用吗?为什么?</h3><blockquote>这个不会起作用,只有在左边没有%的情况下才会起作用。<br>原因后面补充</blockquote><h3>最左匹配原则的成因?</h3><blockquote>最左匹配的原则:MySQL执行sql时候在where后面字段从左到右匹配索引,遇到范围查询就停止,=和in可以乱序。<br>最左匹配的成因:联合索引是多个字段共同组成的B+tree结构,最左边的字段在树的最上边,按照顺序自上而下分布,而查询树结构就是从树的根节点往下查询。</blockquote><h3>什么是覆盖索引?</h3><blockquote>查询语句的索引起作用了,并且查询的字段也是索引本身的字段<br>就是覆盖索引,可避免回表查询。<br>执行计划时:_Extra:__Using index___</blockquote><h4>那什么是回表?</h4><blockquote>和覆盖索引相反,查询的字段除了索引字段还有其他字段。<br>mysql查询完索引树后再回到表里,把其他字段查出来。<br>执行计划时:_Extra:__Using index condition___</blockquote><h3>聚合索引和非聚合索引的区别?</h3><blockquote>主键索引是聚合索引,其他索引是非聚合索引<br>聚合索引叶子节点存储整条记录,非聚合索引叶子节点存储的是主键指针。</blockquote><h3>一条sql执行经历了什么?</h3><blockquote>首先MySQL会去检查这条语句有没有缓存的数据,有就结束了,没有开始检查语法,然后选择用哪些个索引,最后使用选择搜索引擎( InnoDB 还是 MyISAM)去执行。</blockquote><h3>expain怎么用?</h3><blockquote>explain叫执行计划,是mysql检验sql语句效率的工具,用法是直接加在sql语句的前面去执行。<br>我主要是看执行后的两个值 <strong>type</strong> 和 <strong>Extra</strong><br>当type的值是index或all的时候,表示需要优化了<br>当Extra的值是Using filesort 或 Using temporary 表示需要优化了</blockquote><h4>遇到 Using temporary 和 Using filesort怎么优化?</h4><blockquote>Using temporary是使用到了临时表,常发生在order by 和group by 的语句中<br>Using filesort 使用了文件排序,即在内存和磁盘中排序<br>给 order by 或 group by 后面的字段加索引</blockquote><h3>什么是乐观锁和悲观锁?</h3><blockquote>乐观锁和悲观锁并发控制的两种思路<br><strong>乐观锁</strong>:更新的时候校验更新前查到的数据是不是最新的,实现方法:CAS机制和版本号机制<br><strong>悲观锁</strong>:更新前锁住数据,不让其他线程查询和更新,等到更新完成后,再释放锁。实现方法:加锁:数据库加锁<code>select ... for update</code>,代码加锁<code>synchronized</code>。</blockquote><h3>数据库多大的时候需要分表?</h3><blockquote>分别为纵向分表和横向分表<br><strong>纵向分表</strong>:一张表根据字段的活跃度不同为多张表,经常查询的放在一张表这样。<br><strong>横向分表</strong>:数据量大的时候需要数据横向切割,分布在几张结构相同的表中,避免一张表过大,查询太慢,一般mysql在单表1000万的时候就需要了,这个还和服务器的配置、MySQL的性能、表结构的设计,索引的创建,查询的语句有关系。</blockquote><h3>ElasticSearch为什么比mysql 快?</h3><blockquote>ElasticSearch使用的倒排索引技术,MySQL使用的索引结构是B+tree。</blockquote><h3>怎么解决ElasticSearch 深分页问题?</h3><blockquote>ElasticSearch 在大数据量分页的时候,最后面的数据查询很慢(5万条以后),可以使用scroll滚动的方式去查询,根据每次查询得到的scroll_id去进行下次查询,类似于游标,和redis的scan命令查询差不多。<br>弊端,只能上一页下一页查询,不能跳页查询。</blockquote><h2>spring 和 mybatis</h2><h3>spring MVC 和sping boot 的区别?</h3><blockquote><p>spring MVC是spring 框架的一个模块,使用的MVC思想,M代表对象(Model),V代表页面(view),C代表(控制器)controller<br>在一定程度上封装并简化了原生的Servlet的WEB应用的开发。</p><p>spring boot是spring 框架的一个自动配置的完整开发包,简化了spring MVC在搭建web应用时的繁琐的各种配置,比如:视图解析器的配置、注入bean的扫描路径的配置等,它的特点是约定大于配置,很多配置都已经默认配置好了。<br>sping boot内嵌了tomcat,打包默认是jar包。</p></blockquote><h3>spring bean作用域?</h3><blockquote>singleton :IOC容器中只有一个<br>prototype:IOC容器中有多个<br>request:有效期是一次请求<br>session:有效期是一次会话<br>global session:有效期是一次启动</blockquote><h3>讲讲springboot如何实现自动装配的?</h3><blockquote>主要是springboot启动类上面的@SpringBootApplication注解的作用,它是一个复合注解<br>里面的@SpringBootConfiguration标记当前的类是配置类<br>@ComponentScan注解用来指定需要注入到IOC容器的包的路径<br>@EnableAutoConfiguration注解用来指定需要装配的类的配置文件<br>最后通过动态代理来实现自动装配。</blockquote><h3>BeanFactory 和FactoryBean 的区别?</h3><blockquote>日后更新</blockquote><h4>spring怎么解决循环依赖的问题?</h4><blockquote>循环依赖是多个类互相引用,分为构造依赖和属性循环依赖, <br>spring用三级缓存来解决属性循环依赖,详情日后更新。</blockquote><h3>AOP 的实现原理,什么情况下使用JDBC 的代理?</h3><blockquote>AOP是基于动态代理实现的,<br>如果目标类是接口,则用 jDKProxy来实现,否则用cglib<br>JDKProxy:通过ava的内部反射机制实现<br>cgib:以继承的方式动态生成目标类的代理,借助ASM实现</blockquote><p>详情:<a href="https://segmentfault.com/a/1190000016693703">《AOP的两种实现方式 and 五种增强/通知》</a></p><h3>Spring中使用到的设计模式?</h3><blockquote>日后更新</blockquote><h3>懒汉模式和饿汉模式的区别?</h3><blockquote>懒汉模式:在实例化的时候初始化。<br>饿汉模式:在类加载时时候初始化。</blockquote><h3>mybatis什么时候使用${}?</h3><blockquote><code>#{}</code>是预编译时候充当占位符的一种方式,可以防止sql注入。<br><code>${}</code>是直接拼接sql,一般在表示字段名或表名的时候使用。<code>from ${表名} ,order by ${字段名}</code></blockquote><h3>mybatis嵌套查询和嵌套结果有什么区别?</h3><blockquote>都是发生在结果映射的<code><resultMap></code>标签里。<br>都有嵌套的关系,对象嵌套对象用<code><association></code>标签,对象嵌套集合使用<code><collection></code>标签。<br><strong>嵌套查询</strong> 是在嵌套的标签使用<code>select="xxx"</code>关联另一条查询语句,再次查询,有N+1问题。<br><strong>嵌套结果</strong> 是将查询的结果自动映射到<code><resultMap></code>标签的嵌套关系中。</blockquote><h3>怎么使用mybatis的二级缓存?</h3><blockquote>1.在mybais配置文件中开启二级缓存<br>2.在相应的mapper.xml中加上cache标签。</blockquote><h2>中间件等</h2><h3>nginx 的负载均衡方式有哪些?</h3><blockquote>1.轮询<br>2.配置权重<br>3.根据IP做hash算法,同一个IP进同一个服务器<br>4.同一个url进入同一个服务器</blockquote><h3>redis为什么快?</h3><blockquote>运行在内存、数据结构简单、IO多路复用</blockquote><h3>redis有几种数据类型</h3><blockquote>8种,常用的5种:string、list、hash、set、sorted、set<br>3种不常见的bitmap(位图)、hyperloglog(不精准去重计数器)、Geo(地理信息定位)</blockquote><h3>redis的使用场景?</h3><blockquote>用作缓存,存储经常查询的数据,缓解数据库的压力<br>存储短信验证码,定时失效<br>利用redis的过期提醒,实现订单自动过期<a href="https://segmentfault.com/a/1190000022669324">《订单自动过期方案》</a><br>利用setnx命令实现分布式锁</blockquote><h3>缓存穿透、击穿、雪崩的成因和解决方案?</h3><blockquote><p><strong>缓存击穿</strong>:热点key失效,此时大流量进来,请求穿过redis直接访问数据库。<br><em>解决方案</em>:热点key的有效期设置永久。</p><p><strong>缓存穿透</strong>:请求一个不存在的数据,redis没有就去查数据库,反反复复。<br><em>解决方案</em>:<br>1.将不存在的数据在redis中设置默认值并有有效期。弊端:如果有恶意攻击,会存大量的默认值。<br>2.布隆过滤器,弊端:有一定的误差。</p><p><strong>缓存雪崩</strong>:有大量的key同时失效,或者redis挂了<br><em>解决方案</em>:key的有效期在一定范围内随机一点,数据预热、redis部集群。</p></blockquote><h3>redis的更新策略?</h3><blockquote>修改数据:先操作数据库,再删除redis中的key<br>删除数据:先操作数据库,再删除redis中的key</blockquote><h3>redis 的持久化?</h3><blockquote>redis的持久化有两种,AOF和RDB。<br>AOF是全量的,RDB是增量的。</blockquote><h3>redis 哨兵的工作原理?</h3><blockquote>日后更新</blockquote><h3>怎么防止消息的丢失和重复?</h3><blockquote><p>我的项目用的RabbitMQ,消息丢失是使用消息队列会遇到的问题。往往由于网络抖动或服务宕机产生。<br>一般会发生在三个地方,1.生产者到消息队列,2.消息队列,3.消息队列到消费者。<br>生产者到消息队列防止消息丢失可以开启RabbitMQ接收到消息会应答的 <code>confirm</code> 模式,<br>消息队列开启持久化到数据库,可以避免宕机后消息丢失。<br>消费者也是通过一个手动应答的方式告诉RabbitMQ是否真正消费。</p><p>消息重复:对消费消息的方法加锁,并对消息的唯一性做判断。</p></blockquote><h3>分布式锁的实现方式有哪些?</h3><blockquote><p><strong>redis的setnx</strong> :多个线程对一个key去set值,如果不存在key就会设置成功,否则set失败,set成功的就相当于拿到了锁,就可以处理某方法。处理完成删除key,即释放锁。<br>需要对key设置有效期,避免发生得到锁的线程发生意外,不能释放锁。</p><p><strong>zookeeper的临时顺序节点</strong>:多个线程对某个持久化节点设置临时顺序节点,这些临时顺序节点是按照创建时间排序的,第一个创建节点的线程就相当于拿到了锁,处理完逻辑后删除第一个节点,第二个变成了第一个就拿到了锁这样..</p></blockquote><h3>服务注册与发现的实现原理?</h3><blockquote>服务注册于发现是微服务架构中服务之间调用的组件,常见的有Eureka、Nacos、Zookeeper<br>可以简单的想象成一个list,服务注册就相当于将服务的信息存储list,服务发现就相当于查询list,如果某个服务挂了,就从这个list中删除或标记为失效,可以通过组件向各服务发送心跳来确定服务是否正常。</blockquote><h3>spring cloud 的核心组件有哪些?</h3><blockquote>Eureka:服务注册于发现。<br>Feign:根据注解和选择的机器,拼接请求 url 地址,发起请求。<br>Ribbon:实现负载均衡。<br>Hystrix:熔断器,实现了不同服务调用的隔离,防止一个服务出现异常,拖垮整个服务链路,避免了服务雪崩的问题。<br>Zuul:网关管理,由 Zuul 网关转发请求给对应的服务,后端服务的总入口。</blockquote><hr><p>写的稍微有些仓促,后期会更新完善,有描述不正确或者不清楚的地方,还望指出,感谢!</p>
springboot集成Nacos 实现多环境动态配置
https://segmentfault.com/a/1190000023464450
2020-08-01T22:58:10+08:00
2020-08-01T22:58:10+08:00
大树
https://segmentfault.com/u/mshu
3
<p><a href="https://link.segmentfault.com/?enc=aMVh3g%2Fka6wk7x3R4NS4Hw%3D%3D.s0AaxVwEK0inV1ueElBanFJNSUZicV74zmJYRnKUHWM%3D" rel="nofollow">nacos</a> 阿里旗下的开源项目,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。</p><blockquote>随着我国科技的进步,越来越多的开源技术来自我骄傲的中国,以前要了解某项技术除了查看国内大佬博主的文章就是硬着头皮去看官方的英文文档。现在好多了,国内的开源项目官方文档首先就得有中文版的,但是,这nacos的手册写的略显敷衍🤔。</blockquote><p><img src="/img/bVbKDwZ" alt="image.png" title="image.png"></p><blockquote>最近上线了一个项目,上线之前整理生产环境的配置真是让人头大,生怕有遗漏或者填错的地方,如果在现场配置的有问题,改正后还得重启服务才能生效,这不,对方把我们的端口少写了个数字,某功能出现问题,白天他们又不能重启,只能等到晚上解决。如果能够使用nacos作为动态的配置中心,直接改配置不需要重启就可以解决了。</blockquote><p>本章内容介绍的是spring-boot集成nacos ,不是spring-cloud ,导的包不一样使用起来也有点差异。</p><h2>nacos服务端</h2><p>首先下载安装<a href="https://link.segmentfault.com/?enc=opRoMjGNc3mQflFR4ySKkQ%3D%3D.mODOe%2FDrsX8AvoLDOm1JPIhSYMjhFLkV6vS28BfZ2DQLlo8%2F%2B7tzynvHmRyOxJZ9" rel="nofollow">nacos服务端</a><br>启动方法参考<a href="https://link.segmentfault.com/?enc=7NAAlPNKMFwdyawfmunVTA%3D%3D.irfmUCZvf44DnXDCzGn2hwBxLxqwz1ikPgSTcznYPQE3uG4CiWKfz2qlr0mZHT%2BE" rel="nofollow">Nacos 快速入门</a><br>访问地址 <code>http://127.0.0.1:8848/nacos</code> ; 初始用户名和密码都是nacos</p><h4>创建命名空间</h4><p>登陆后左侧导航栏选择命名空间,右上角创建命名空间,分别创建dev 和prod 作为开发和生产环境的配置,不同空间互相隔离。用来区分环境非常合适。<br><img src="/img/bVbKB32" alt="image.png" title="image.png"></p><h4>创建配置文件</h4><p>左侧导航栏>配置管理-配置列表<br>创建配置文件需要填写<strong>DataId</strong> 和<strong>GroupId</strong>,以及<strong>配置内容</strong><br><img src="/img/bVbKGjU" alt="image.png" title="image.png"></p><p>注意:dataId必须以文件类型结尾</p><h2>spring-boot集成nacos配置中心</h2><p>添加nacos配置中心的依赖</p><pre><code><dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>nacos-config-spring-boot-starter</artifactId>
<version>${latest.version}</version>
</dependency></code></pre><p>springboot 版本 <code>2.3.2.RELEASE</code><br>nacos-config 版本 <code>0.2.7</code></p><h4>yml配置nacos服务端地址和命名空间</h4><p>创建配置文件<code>application-dev.yml</code> 和<code>application-prod.yml</code><br>根据在nacos服务端控制台创建的两个命名空间的ID分别填写到开发环境和生产环境的yml文件里</p><ul><li>application-dev.yml</li></ul><pre><code>nacos:
config:
namespace: 133c9c90-6d9c-45cd-8067-06f853607940
server-addr: 127.0.0.1:8848</code></pre><ul><li>application-prod.yml</li></ul><pre><code>nacos:
config:
namespace: 72008218-19b0-4960-ab44-a0cbdd8097a0
server-addr: 127.0.0.1:8848</code></pre><h4>启动类配置<code>dataId</code> 和 <code>groupId</code></h4><p><code>@NacosPropertySource</code>注解填写<code>dataId</code> 和 <code>groupId</code> ,<code>autoRefreshed = true</code> 表示动态刷新的总开关,ture:开启,默认是false.<br>读取多个配置文件就写多个注解,如果两个配置文件中有相同的配置,排在上面的注解读取的配置内容优先级最高。<br>同一个配置文件可以被同空间的应用共享。</p><pre><code class="java">@SpringBootApplication
@NacosPropertySource(dataId = "mashu-demo.yaml", autoRefreshed = true, groupId = "USER_GROUP")
@NacosPropertySource(dataId = "dashu-demo.yaml", autoRefreshed = true, groupId = "USER_GROUP")</code></pre><h4>注入配置内容</h4><p><code>@NacosValue</code>注解获取配置内容,<code>autoRefreshed = true</code> 表示开启动态刷新</p><pre><code class="java">@NacosValue(value = "${name}", autoRefreshed = true)
private String name;</code></pre><h4>不同环境的启动方法</h4><p><strong>命令行读取不同环境的配置:</strong></p><p>启动参数根据<code>-Dspring.profiles.active=dev</code>或<code>-Dspring.profiles.active=prod</code>切换开发和生产环境<br>读取application-dev.yml 还是 application-prod.yml 依据是<code>"-"</code>后面的单词,即: dev/prod</p><p><strong>开发工具读取不同环境的配置:</strong><br>idea 开发工具是打开设置Edit configurations 选项,在Actice profile 后面填写dev或者prod</p><p>至此多环境动态配置完成。</p><h2>数据持久化</h2><p>为了保证数据的安全性,nacos还支持将数据同步到数据库。<br>这个需要在nacos服务端配置,在下载的nacos安装包里面<code>nacos-server-1.2.1\nacos\conf\nacos-mysql.sql</code>有msyql初始化的sql脚本。<br>根据实际情况修改配置文件<code>nacos-server-1.2.1\nacos\conf\application.properties</code></p><pre><code>spring.datasource.platform=mysql
### Count of DB:
db.num=1
### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=123456</code></pre><p>注意:如果之前没有做持久化,做了持久化之后会把之前的配置干掉。😒</p><h2>java SDK</h2><p>nacos还支持java客户端对配置内容的操作和配置变更的监听。</p><p>1.导包:</p><pre><code><dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>1.2.0</version>
</dependency></code></pre><p>2.根据ip和命名空间获取配置文件对象<code>ConfigService</code></p><pre><code>String serverAddr = "127.0.0.1:8848";
String namespace = "72008218-19b0-4960-ab44-a0cbdd8097a0";
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
properties.put("namespace", namespace);
ConfigService configService = NacosFactory.createConfigService(properties);</code></pre><p><strong>configService</strong>对象有几个常用的方法:</p><p>1.getConfig()获得配置内容</p><pre><code>String dataId = "dashu-demo.yaml";
String groupId = "USER_GROUP";
String content = configService.getConfig(dataId, groupId, 5000);</code></pre><p>2.publishConfig()发布配置</p><pre><code>String content = "name: mashu";
configService.publishConfig(dataId, group, content);</code></pre><p>3.removeConfig()删除配置</p><pre><code>configService.removeConfig(dataId, group);</code></pre><p>4.addListener()添加监听</p><pre><code>Listener listener = new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
System.out.println("变更后读取到的配置内容:" + "\r\n" + configInfo);
}
@Override
public Executor getExecutor() {
return null;
}
configService.addListener(dataId, groupId, listener);</code></pre><p>5.removeListener()删除监听</p><pre><code>configService.removeListener(dataId, groupId, listener);</code></pre><p>以上配置内容的获取,添加,删除,监听都是全量的。</p><h2>遇到问题</h2><p>nacos对于已经<code>jasypt</code>加密的数据正常读取但是不能够动态刷新,甚至它父节点以下的兄弟节点没有加密的属性都会丧失动态刷新的功能。重启项目才能读到最新的数据。</p><p><strong>猜测</strong>:nacos 第一次能够读到加密的数据是解密后的可以理解,启动参数有解密的密钥,应该是解密后naocs读取的,但是更新加密的字段nacos应该做不到,它没有密钥。nacos为了防止更新加密的数据失败,它就让加密的数据失去动态更新的能力。</p><p><strong>困惑</strong>:我将原本加密的属性包括它父节点下面节点所有加密的属性都改成了非加密,重启项目后发现依然不能动态刷新!不能理解这种"记仇"现象,即使我重启nacos服务端也不能解决。<br><img src="/img/bVbMnMX" alt="image" title="image"></p><h2>优缺点:</h2><blockquote>单说nacos的配置管理功能</blockquote><h3>优点:</h3><ol><li>支持动态刷新</li><li>支持共享配置文件,相同配置,配置在上面的优先级最高。</li><li>有历史记录,可回滚</li><li>每次修改有文件对比功能,降低出错率</li><li>支持Java代码的配置变更监听,内容的获取,添加,删除,不过都是全量</li></ol><h3>缺点:</h3><ol><li>不支持加密属性的动态刷新</li><li>对配置变更监听,内容的获取,添加,删除都是全量的</li></ol><hr><p>对nacos了解甚浅,有些理解可能有误差。如有错误请指出,感激不尽。</p>
MySQL定时备份方案
https://segmentfault.com/a/1190000023285648
2020-07-18T16:40:01+08:00
2020-07-18T16:40:01+08:00
大树
https://segmentfault.com/u/mshu
8
<blockquote>虽说现在这世道有些爱情是有价的,但是数据是无价的,数据备份是尤为的重要,可以在你未来的某一天不小心删库了,不用着急跑路。</blockquote><p><img src="/img/remote/1460000023285651" alt="image" title="image"></p><p><strong>本片文章介绍的方案是利用Linux自身的crontab定时任务功能,定时执行备份数据库的脚本。</strong></p><h3>技术要点:</h3><ul><li>数据库备份dump命令</li><li>shell脚本</li><li>Linux定时任务crontab</li></ul><h3>数据备份dump</h3><p>数据库都有一个导出数据库内数据和结构的命令,就是备份。<br>将备份的数据还原会将原来的数据中的表删了重建,再插入备份中的数据,这是恢复。<br>这一点需要注意,如果恢复之前的数据比备份的多,恢复后多的数据就没有了。</p><p>列出我常用的两种数据库的备份和恢复命令</p><h6>postgresql:</h6><p><strong>备份</strong> <code>pg_dump -h [ip] -U [用户名] [库名] >[导出的.sql 文件]</code><br><strong>恢复</strong> <code>psql -s [库名] -f [导出.sql 文件]</code></p><h6>mysql:</h6><p><strong>备份</strong> <code>mysqldump -h -u [用户名] -p [库名] > [导出的.sql 文件]</code><br><strong>恢复</strong> <code>mysql -u [用户名] -p [库名] < [导出的.sql 文件]</code></p><h3>shell脚本</h3><p>要完成一个功能完善的备份方案,就需要shell脚本。<br>我们要让这个脚本备份到指定路径,并压缩存放,最多30个,超过30个删除最早的,并记录操作日志。<br>啥也不说了,话都在脚本里,干了!</p><pre><code>#用户名
username=root
#密码
password=nicai
#将要备份的数据库
database_name=l_love_you
#保存备份文件最多个数
count=30
#备份保存路径
backup_path=/app/mysql_backup
#日期
date_time=`date +%Y-%m-%d-%H-%M`
#如果文件夹不存在则创建
if [ ! -d $backup_path ];
then
mkdir -p $backup_path;
fi
#开始备份
mysqldump -u $username -p$password $database_name > $backup_path/$database_name-$date_time.sql
#开始压缩
cd $backup_path
tar -zcvf $database_name-$date_time.tar.gz $database_name-$date_time.sql
#删除源文件
rm -rf $backup_path/$database_name-$date_time.sql
#更新备份日志
echo "create $backup_path/$database_name-$date_time.tar.gz" >> $backup_path/dump.log
#找出需要删除的备份
delfile=`ls -l -crt $backup_path/*.tar.gz | awk '{print $9 }' | head -1`
#判断现在的备份数量是否大于阈值
number=`ls -l -crt $backup_path/*.tar.gz | awk '{print $9 }' | wc -l`
if [ $number -gt $count ]
then
#删除最早生成的备份,只保留count数量的备份
rm $delfile
#更新删除文件日志
echo "delete $delfile" >> $backup_path/dump.log
fi</code></pre><p>给脚本起个顾名思义的漂亮名字 <code>dump_mysql.sh</code><br>给脚本赋予可执行权限 <code>chmod +x dump_mysql.sh</code>, 执行后脚本变绿了就是可实行文件<br>执行方法:./加脚本名称</p><pre><code>chmod命令参数含义--
+ 代表添加某些权限
x 代表可执行权限</code></pre><h3>定时任务crontab</h3><p>crontab是Linux自带的一个定时任务功能,我们可以利用它每天凌晨执行一次<code>dump_mysql.sh</code>脚本。</p><h6>crontab用法:</h6><ul><li>crontab -l 查看定时任务列表</li><li>crontab -e 编辑(新增/删除)定时任务</li></ul><p>运行crontab -e命令,打开一个可编辑的文本,输入<code>00 01 * * * /app/dump_mysql.sh</code><br>保本并退出即添加完成。</p><h6>内容解释:</h6><p><code>00 01 * * * /app/dump_mysql.sh</code> 分两部分看,<br>第一部分<code>00 01 * * * </code>是定时任务的周期,第二部分<code>/app/dump_mysql.sh</code>到时间做的事情。<br>周期表达式是五个占位符,分别代表:<strong>分钟、小时、日、月、星期</strong></p><p>占位符用<code>*</code>表示<code>每</code>,用在第一位就是每分钟,第二位每小时,依此类推<br>占位符用<code>具体数字</code>表示<code>具体时间</code>,10用在第一位就是10分,用在第三位表示10号,依此类推<br>占位符用<code>-</code>表示<code>区间</code>,5-7用在第一位就是5分到7分,用在第五位表示周5到周日,依此类推<br>占位符用<code>/</code>表示<code>间隔</code>,5-10/2用在第一位就是5分到10分间隔2分钟,用在第二位表示5点到10点间隔2小时,依此类推<br>占位符用<code>,</code>表示<code>列表</code>,5,10用在第一位就是5分和10分,用在第四位表示5月和10月,依此类推</p><hr><p>2020年7月20日更新</p><h3>脚本中密码安全性问题</h3><p>多谢思友hack的提醒,脚本里面直接放数据库密码是不安全的。<br>我们脚本执行后msyql也会打印一行警告:<br><code>mysqldump: [Warning] Using a password on the command line interface can be insecure</code><br>译:在命令行界面上使用密码可能不安全</p><p>关于这个问题网上的处理方法也比较一致,mysqldump命令去掉-u用户名-p密码参数,使用<code>--defaults-file</code>参数读取配置的用户名密码。</p><p>也是官网给出的方案:<br><img src="/img/bVbJVZW" alt="image.png" title="image.png"></p><p>在命令行上指定密码应该被认为是不安全的。为了避免在命令行上给出密码,请使用选项文件。参见 <a href="https://link.segmentfault.com/?enc=n4ZffU%2Fm9fUBmrRjdGELAQ%3D%3D.CSKYC7Vh4OL%2Fkd0EzjcHV%2B0oPDmpCxfRs5FXW7oezQGl1z8VuUd0MXd3LwavAnk3WOMdm0JvzrsV9p1JuYp9QA%3D%3D" rel="nofollow">第6.1.2.1节: 用户密码安全指南。</a></p><p><img src="/img/bVbJV02" alt="image.png" title="image.png"></p><h6>编辑my.cnf</h6><p>编辑数据库家目录的my.cnf,我的路径是/etc/my.cnf。在[client]下面添加密码,如果没有[client],就编写一个,不然下面的password 会无效。</p><pre><code>[client]
password="nicai"</code></pre><p>默认使用当前登录Linux服务器的账号作为连接数据库的账号,我刚好都是root ,用户名就可以不配,如果不一致,需要加一条</p><pre><code>user=xxx</code></pre><p>编辑完了最好给my.cnf 设置600的权限,600代表只有拥有者有读写权限。</p><blockquote>shell> chmod 600 my.cnf</blockquote><p>如果密码中含有特殊字符,一定要用双引号引起来,不然运行mysqldump命令会报错<br><code>mysqldump: Got error: 1045: Access denied for user 'root'@'localhost' (using password: YES) when trying to connect</code></p><h6>修改参数</h6><p>运行mysqldump命令时,使用<code>--defaults-file=/etc/my.cnf</code>参数代替<code>-uroot -ppassword</code>的参数即可。<br>脚本中去掉用户名和密码,mysqldump命令按照下面修改:</p><pre><code>mysqldump --defaults-file=/etc/my.cnf $database_name >$backup_path/$database_name-$date_time.sql</code></pre><hr><blockquote>上面的方法虽然是官方提供的,但还是把明文写在文件里,只不过是加了读写权限而已,还是没太有说服力,可以考虑下面的方法</blockquote><h5>使用SHC对脚本加密:</h5><p>Linux下脚本加密工具SHC对shell脚本进行加密,SHC可以将shell脚本转换为一个可执行的二进制文件,这也是一个办法。</p><h6>对备份的文件也需要加密:</h6><p>tar配合openssl完成加密压缩:<br><code>tar -czvf - 源文件 | openssl des3 -salt -k 密码 -out 压加密文件.tar.gz</code><br>解密:<br><code>openssl des3 -d -k 密码 -salt -in 加密文件.tar.gz | tar xvf -</code></p>
CSRF漏洞的原理与防御
https://segmentfault.com/a/1190000023022494
2020-06-26T01:52:52+08:00
2020-06-26T01:52:52+08:00
大树
https://segmentfault.com/u/mshu
2
<p><img src="/img/remote/1460000023022504" alt="image" title="image"></p>
<p><strong>CSRF</strong> 全称:<code>Cross Site Request Forgery</code>,译:跨站请求伪造</p>
<h4>场景</h4>
<p>点击一个链接之后发现:账号被盗,钱被转走,或者莫名发表某些评论等一切自己不知情的操作。</p>
<h4>CSRF是什么</h4>
<p>csrf 是一个可以发送http请求的脚本。可以伪装受害者向网站发送请求,达到修改网站数据的目的。</p>
<h4>原理</h4>
<p>当你在浏览器上登录某网站后,cookie会保存登录的信息,这样在继续访问的时候不用每次都登录了,这个大家都知道。而CSRF就利用这个登陆态去发送恶意请求给后端。</p>
<p>为什么脚本可以获得目标网站的cookie呢?<br>只要是请求目标网站,浏览器会自动带上该网站域名下面的cookie,看下面的脚本,可以证明恶意脚本可以获得CSDN网站的登录信息。<br>前提是你已经在浏览器上登录了CSND网站。</p>
<pre><code><!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>csrf demo</title>
</head>
<body>
您在CSDN上的
粉丝数:<span id="fans_num"></span>
关注数:<span id="follow_num"></span>
<script>
fetch('https://me.csdn.net/api/relation/get', {
credentials: 'include'
}).then(res => res.json())
.then(
res => {
document.getElementById('fans_num').innerText = res.data.fans_num;
document.getElementById('follow_num').innerText = res.data.follow_num;
})
</script>
</body>
</html></code></pre>
<p>保证CSDN的登录状态,用浏览器打开这个html文件,可以看到这个脚本已经获得了我在csdn 上的用户信息。以及寒酸的粉丝数量!<br>F12打开选择应用程序一栏左边Cookie 还有来自csdn网站关于当前用户的一些信息。<br><img src="/img/bVbILjo" alt="image.png" title="image.png"><br><img src="/img/bVbILmG" alt="image.png" title="image.png"><br><img src="/img/bVbILjf" alt="image.png" title="image.png"></p>
<p>这个脚本让每个不同的登录用户打开,都会根据当前用户来展示关注数和粉丝数,<br>这就足以说明可以获得目标网站的当前用户的信息,并能够代表用户发送请求。<br>这只是个无害的get请求,如果是post请求呢?</p>
<h2>CSRF攻击</h2>
<p>知道了原理,攻击就变得好理解了,接着上面的例子,<br>我把请求地址改成评论本篇文章的url,参数为 “<code>这篇文章写得6</code>”,<br>在没有CSRF防御的情况下,我发表一个评论如:<code>脱单秘笈:</code>,后面附上这个脚本的链接,只要有用户点了链接,就会以他的名义给本篇文章发评论“<code>这篇文章写得6</code>”。</p>
<p>CSDN 肯定是做了防御了哈,我就不白费力气了。</p>
<h2>CSRF防御</h2>
<p>三种防御方式:</p>
<h3>1. SameSit</h3>
<p><strong>禁止第三方网站使用本站Cookie</strong>。</p>
<p>这是后端在设置Cookie时候给<code>SameSite</code>的值设置为<code>Strict</code>或者<code>Lax</code>。<br>当设置<code>Strict</code>的时候代表第三方网站所有请求都不能使用本站的Cookie。<br>当设置<code>Lax</code>的时候代表只允许第三方网站的<code>GET</code>表单、<code><a></code>标签和<code><link></code>标签携带Cookie。<br>当设置<code>None</code>的时候代表和没设一样。</p>
<pre><code>@Bean
public CookieSerializer httpSessionIdResolver(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setCookieName("JESSIONID");
cookieSerializer.setUseHttpOnlyCookie(true);
cookieSerializer.setSameSite("Lax");
cookieSerializer.setUseSecureCookie(true);
return cookieSerializer;
}</code></pre>
<p><strong>缺点:</strong><br>目前只有chrome浏览器支持........</p>
<h3>2. referer</h3>
<p><code>referer</code>代表着请求的来源,不可以伪造。<br>后端写个过滤器检查请求的<code>headers</code>中的<code>referer</code>,检验是不是本网站的请求。<br>题外话:<br><code>referer</code>和<code>origin</code>的区别,只有post请求会携带origin请求头,而referer不论何种情况下都带。<br><code>referer</code>正确的拼写 应该是 <code>referrer</code>,HTTP的标准制定者们将错就错,不打算改了<br><strong>缺点:</strong><br>浏览器可以关闭referer..........</p>
<h3>3. token</h3>
<p>最普遍的一种防御方法,后端生成一个token放在session中并发给前端,前端发送请求时携带这个token,后端通过校验这个token和session中的token是否一致判断是否是本网站的请求。</p>
<p>具体实现:<br>用户登录输入账号密码,请求登录接口,后端在用户登录信息正确的情况下将token放到session中,并返回token给前端,前端把token 存放在localstory中,之后再发送请求都会将token放到header中。<br>后端写个过滤器,拦截POST请求,注意忽略掉不需要token的请求,比如登录接口,获取token的接口,免得还没有获取token就检验token。<br>校验原则: session中的token和前端header中的token一致的post ,放行。</p>
<pre><code>/**
* @author mashu
* Date 2020/6/22 9:37
*/
@Slf4j
@Component
@WebFilter(urlPatterns = "/*", filterName = "verificationTokenFilter", description = "用于校验token")
public class VerificationTokenFilter implements Filter {
List<String> ignorePathList = ImmutableList.of("/demo/login","/demo/getToken");
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
//忽略不需要token的请求
String serviceUrl = httpServletRequest.getServletPath();
for (final String ignorePath : ignorePathList) {
if (serviceUrl.contains(ignorePath)) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
}
String method = httpServletRequest.getMethod();
if ("POST".equals(method)) {
String tokenSession = (String)httpServletRequest.getSession().getAttribute("token");
String token = httpServletRequest.getHeader("token");
if (null != token && null != tokenSession && tokenSession.equals(token)) {
filterChain.doFilter(servletRequest, servletResponse);
return;
} else {
log.error("验证token失败!" + tokenSession + "!=" + token);
httpServletResponse.sendError(403);
return;
}
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}</code></pre>
springboot 集成CAS 实现单点登录
https://segmentfault.com/a/1190000022990791
2020-06-22T01:32:14+08:00
2020-06-22T01:32:14+08:00
大树
https://segmentfault.com/u/mshu
7
<p>最近新参与的项目用到了cas单点登录,我还不会,这怎么能容忍!空了学习并搭建了一个spring-boot 集成CAS 的demo。实现了单点登录与登出。</p><p>单点登录英文全称是:Single Sign On,简称<strong>SSO</strong>。<br>含义:在多个相互信任的系统中,只要登录一个系统其他系统均可访问。</p><p><a href="https://link.segmentfault.com/?enc=HCrnmoB4HpOw0EQKpJP0TQ%3D%3D.wMBBbo%2FA8lB%2FrGKkq%2BFz91eT7TrfXFn3AUycdd3yPaO96Ju1cx1a0vclkdxxUyc8" rel="nofollow">CAS</a> 是一种使用广泛的单点登录实现,分为客户端<strong>CAS Client</strong>和服务端 <strong>CAS Service</strong>,客户端就是我们的系统,服务端是认证中心,由CAS提供,我们需要稍作修改,启动起来就可以用。~~~~</p><p><img src="/img/bVbIC6Y" alt="CAS-Logo-V1.png" title="CAS-Logo-V1.png"></p><h2>效果演示</h2><p><img src="/img/remote/1460000022991433" alt="" title=""><br><a href="https://link.segmentfault.com/?enc=DLdJvquN2uZpKsJvR1tx9A%3D%3D.KQEjot51jwnHQacG3MvUUNO1q81gxU0lBcKcNPcX4UGMmsnZ%2FP4Jn6EoCH5LyZvoO6INATXCUbOoBVaDa8WbJw%3D%3D" rel="nofollow">https://img-blog.csdnimg.cn/2...</a></p><h2>CAS原理</h2><p>我尽量一句话讲明白:</p><blockquote>用户通过浏览器访问系统A(CAS Client),系统A 检查本地有没有Session,就是登录信息,如果没有就带着访问地址去认证中心(CAS Service)检查有没有全局Session ,如果没有返回登录页面让用户登录,此时还带着那个访问系统A的地址,登录完成后认证中心保存全局的Session ,去刚刚访问系统A的地址,外加一个令牌,系统A 收到令牌后向认证中心发起验证,通过后保存本地Session. 有本地Session后就可以直接和浏览器交互了。<br>用户访问系统B,系统B检查有没有本地session ,如果没有带着访问地址去认证中心,认证中心发现有全局session ,返回访问系统B 的地址,外加令牌。系统B 收到令牌,向认证中心校验,通过后保存本地session。</blockquote><p>这一切都由cas实现,我们只需要集成它即可。</p><h2>https证书</h2><p>CAS Service 需要用https的方式,那么就需要证书,可以买也可以自己生成一个。<br><strong>其实这一步也可以省略,访问的时候使用http即可,只是cas 会给警告。</strong></p><p>步骤和把大象装进冰箱一样简单,总共三步:</p><ol><li>生成密钥</li><li>生成证书</li><li>导入证书</li></ol><h5>1. 生成密钥</h5><blockquote>keytool -genkey -alias cainiao -keyalg RSA -keystore E:\ssl\cainiao.keystore</blockquote><p>参数说明:</p><ul><li>-genkey 生成密钥</li><li>-keyalg 指定密钥算法,这时指定RSA</li><li>-alias 指定别名</li><li>-keystore 指定密钥库存储位置,这里存在 E:/ssl/目录下</li></ul><p><em>在执行中会问你很多问题,当问到 :您的名字与姓氏是什么?</em><br><em>此时需要填写<strong>域名</strong>,作为之后的访问地址,其他随意。</em><br>执行完后生成一个密钥文件 cainiao.keystore</p><h5>2. 生成证书</h5><blockquote>keytool -export -alias cainiao -storepass 123456 -file E:/ssl/cainiao.cer -keystore E:/ssl/cainiao.keystore</blockquote><p>参数说明:</p><ul><li>-storepass 刚刚生成密钥文件时候的设置的密码</li><li>-file指定导出证书的文件名为cainiao.cer</li><li>-keystore指定之前生成的密钥文件的文件名</li></ul><p>执行完后目录下会生成一个cainiao.cer证书</p><h5>3. 导入证书</h5><blockquote>keytool -import -alias cainiao -keystore C:/"Program Files"/Java/jdk1.8.0_181/jre/lib/security/cacerts -file E:/ssl/cainiao.cer -trustcacerts</blockquote><p>将证书导入到JDK信任库<br>把原来的$JAVA\_HOME/jre/lib/security/cacerts文件要先删掉,否则会报出<code> Keystore was tampered with, or password was incorrect</code>.</p><p>下面是整个过程:</p><pre><code>PS E:\ssl> keytool -genkey -alias cainiao -keyalg RSA -keystore E:\ssl\cainiao.keystore
输入密钥库口令:
再次输入新口令:
您的名字与姓氏是什么?
[Unknown]: www.cainiao.com
您的组织单位名称是什么?
[Unknown]: cainian
您的组织名称是什么?
[Unknown]: cainiao
您所在的城市或区域名称是什么?
[Unknown]: wx
您所在的省/市/自治区名称是什么?
[Unknown]: js
该单位的双字母国家/地区代码是什么?
[Unknown]: CN
CN=www.cainiao.com, OU=cainian, O=cainiao, L=wx, ST=js, C=CN是否正确?
[否]: y
输入 <cainiao> 的密钥口令
(如果和密钥库口令相同, 按回车):
再次输入新口令:
------------------------------------------------------------------------------------
PS E:\ssl> keytool -export -alias cainiao -storepass 123456 -file E:/ssl/cainiao.cer -keystore E:/ssl/cainiao.keystore
存储在文件 <E:/ssl/cainiao.cer> 中的证书
------------------------------------------------------------------------------------
PS E:\ssl> keytool -import -alias cainiao -keystore C:/"Program Files"/Java/jdk1.8.0_181/jre/lib/security/cacerts -file E:/ssl/cainiao.cer -trustcacerts
输入密钥库口令:
所有者: CN=www.cainiao.com, OU=cainian, O=cainiao, L=wx, ST=js, C=CN
发布者: CN=www.cainiao.com, OU=cainian, O=cainiao, L=wx, ST=js, C=CN
序列号: 509d1aea
有效期为 Wed Jun 17 22:02:55 CST 2020 至 Tue Sep 15 22:02:55 CST 2020
证书指纹:
MD5: 5B:B2:7C:D7:B7:31:C5:7C:1C:BC:F7:DA:A8:2D:1C:B2
SHA1: F6:76:55:55:D7:48:E3:9F:3A:B6:EE:68:1F:BE:DC:DE:51:B1:33:E5
SHA256: 24:53:18:CD:E8:95:65:D8:6E:6A:7B:8E:79:CB:91:BD:F4:2E:C3:99:59:D1:76:12:A8:95:45:2A:4B:03:E4:AD
签名算法名称: SHA256withRSA
主体公共密钥算法: 2048 位 RSA 密钥
版本: 3
扩展:
#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 70 B3 D5 76 36 EA 54 BA 75 C1 A1 5C DA 76 82 0E p..v6.T.u..\.v..
0010: 4D F4 C9 05 M...
]
]
是否信任此证书? [否]: y
证书已添加到密钥库中</code></pre><p><strong>最后</strong>,hosts 配置 <code>127.0.0.1 www.cainiao.com</code></p><h2>集成CAS</h2><h3>搭建CAS service</h3><p>需要从github上拉取模板 <a href="https://link.segmentfault.com/?enc=bN2UCn2Mbupm%2FHafid6bsw%3D%3D.3L8HBt3PHuZkQF3h3mo86WMrbK6q2leJ48SWbfjYDycwhHng7U2vGLuilhQL2nzk" rel="nofollow">https://github.com/apereo/cas...</a><br>5.3之后的都是gradle项目,5.3以之前都是maven 项目,我下载5.3版本的。</p><p>1.> 把pom 里面的<repositories>仓库地址去掉,国外的仓库地址比较慢。你懂得。<br>2.> 在根目录下建/src/main/resources目录<br>3.> 将生成的密钥文件复制到/src/main/resources目录下<br>4.> 将overlays/org.apereo.cas.cas-server-webapp-tomcat-5.3.14/WEB-INF/classes/application.properties文件复制到第二步建的目录下。<br>5.> 修改复制过来的/src/main/resources/application.properties文件,根据上面的证书信息如实填写。</p><pre><code>server.ssl.key-store=classpath:cainiao.keystore
server.ssl.key-store-password=123456
server.ssl.key-password=123456</code></pre><p>6.> 连接mysql数据库,在pom 中添加依赖</p><pre><code><dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-jdbc</artifactId>
<version>${cas.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.21</version>
</dependency></code></pre><p>或许你会发现有个xmlsectool-2.0.0.jar包下不下来,这是阿里云的仓库没有,需要到maven中央仓库下载,后安装到本地仓库,可不是直接放到本地仓库,jar包都是必须使用命令安装到本地仓库。</p><blockquote>mvn install:install-file -Dfile="E:\下载\xmlsectool-2.0.0.jar" "-DgroupId=net.shibboleth.tool" "-DartifactId=xmlsectool" "-Dversion=2.0.0" "-Dpackaging=jar"</blockquote><p><a href="https://segmentfault.com/n/1330000022205314">安装jar包到本地仓库笔记</a></p><p>7.> 在复制过来的/src/main/resources/application.properties文件中在添加如下信息</p><pre><code>#查询账号密码sql,必须包含密码字段
cas.authn.jdbc.query[0].sql=select * from sys_user where username=?
#指定上面的sql查询字段名(必须)
cas.authn.jdbc.query[0].fieldPassword=password
#指定过期字段,1为过期,若过期需要修改密码
cas.authn.jdbc.query[0].fieldExpired=expired
#为不可用字段段,1为不可用,
cas.authn.jdbc.query[0].fieldDisabled=disabled
#数据库方言hibernate的知识
cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQLDialect
#数据库驱动
cas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Driver
#数据库连接
cas.authn.jdbc.query[0].url=jdbc:mysql://localhost:3306/cas?useUnicode=true&characterEncoding=UTF-8
#数据库用户名
cas.authn.jdbc.query[0].user=root
#数据库密码
cas.authn.jdbc.query[0].password=123456
#默认加密策略,通过encodingAlgorithm来指定算法,默认NONE不加密
cas.authn.jdbc.query[0].passwordEncoder.type=DEFAULT
cas.authn.jdbc.query[0].passwordEncoder.characterEncoding=UTF-8
cas.authn.jdbc.query[0].passwordEncoder.encodingAlgorithm=MD5
</code></pre><p>附上数据库sql,用户信息表</p><pre><code>-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`expired` int(11) DEFAULT NULL,
`disabled` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES ('1', 'admin', '21232f297a57a5a743894a0e4a801fc3', '0', '1');
INSERT INTO `sys_user` VALUES ('2', 'cainiao', '6b757206058785025cd90c8d865c8e43', '1', '0');
INSERT INTO `sys_user` VALUES ('3', 'mashu', 'd1f21ceb3f710ebbd9f408274aee1193', '0', '0');</code></pre><p>用户名和密码一样,密码在数据库中是MD5加密的。<br> 这样就完成了CAS service 的搭建,在根目录使用 <code>build.cmd run</code> 命令启动<br> 出现 <strong>READY</strong> 的branner就启动好了 访问地址 <a href="https://link.segmentfault.com/?enc=8xTTCtxDrx6BxlMkxRgxOg%3D%3D.JkFlIBsac6nJoUfJ0HNyJakK3A%2FHh1Doosp5kIoFnLmAvgZ4PpCMA5L5NC8pPpHV" rel="nofollow">https://www.cainiao.com:8443/...</a></p><p><img src="/img/bVbIvtZ" alt="image.png" title="image.png"></p><p><em>三个账号的设置的密码和用户名一样</em></p><p>输入mashu正常登录,cainiao需要修改密码,admin被禁用,符合预期。</p><h3>搭建CAS client</h3><p>创建一个spring boot 项目.<br>1.加入 <a href="https://link.segmentfault.com/?enc=VaN8306sZLQMzbaUujh6wQ%3D%3D.Jk46w%2FtTHbvqBvaT07RdARhbOdiaLphSJrnQxnAIRvvnt21beTeMqxuIYYfq5rUzQlr3mx0MTcA7oIdxGttehA%3D%3D" rel="nofollow">cas 客户端</a> 的依赖, 我选择目前最新的 2.3.0-GA 版本</p><pre><code><dependency>
<groupId>net.unicon.cas</groupId>
<artifactId>cas-client-autoconfig-support</artifactId>
<version>2.3.0-GA</version>
</dependency></code></pre><p>2.在启动类上加上注解 <code>@EnableCasClient</code><br>3.在application.properties中添加配置</p><pre><code>#cas服务端的地址
cas.server-url-prefix=https://www.cainiao.com:8443/cas
#cas服务端的登录地址
cas.server-login-url=https://www.cainiao.com:8443/cas/login
#客户端访问地址
cas.client-host-url=http://www.mashu.com:8080
cas.validation-type=CAS3</code></pre><p>4.添加hosts 配置,把客户端的访问地址配置到hosts</p><pre><code>127.0.0.1 www.mashu.com</code></pre><p>这样就客户端就配置好了。</p><h2>单点登录</h2><p>我写一个controller,访问一下。</p><pre><code>@RestController
public class TestController {
@RequestMapping("/hello")
public String hello() {
return "word";
}
}</code></pre><p>访问 <a href="https://link.segmentfault.com/?enc=AxhJwlvVQYtJkIVBz1wIDg%3D%3D.4SqNK3q9bwoVkHWpf%2Bm6zT9yWCs1gRme1U2P4PXls%2Bg%3D" rel="nofollow">http://www.mashu.com:8080/hello</a></p><h3>未认证授权的服务</h3><p>喜提报错:<br><img src="/img/bVbICWa" alt="image.png" title="image.png"></p><p>原因是服务端不允许客户端的http协议的请求。需要对服务端做以下修改,让他妥协。<br>1.>复制overlays/org.apereo.cas.cas-server-webapp-tomcat-5.3.14/WEB-INF/classes/services/HTTPSandIMAPS-10000001.json文件到新建的/src/main/resource/service/下面</p><pre><code>"serviceId" 由原来的"^(https|imaps)://.*"改成 "^(https|imaps|http)://.*"</code></pre><p>2.>在/src/main/resource/application.properties文件中添加:</p><pre><code>#允许http
cas.tgc.secure=false
cas.serviceRegistry.initFromJson=true</code></pre><p>重启,再次访问 <a href="https://link.segmentfault.com/?enc=2esVWGPUJH8Bs1VltmtamQ%3D%3D.h0eKQmcVB1vhgM8Z%2BD29tT9ziG3aNYKi75TLycvwZDdX1lMek3Nft5eh7fLlxpuvdATvOvUwEhLS8yY7GN5nm3xZjjQFYYtexa%2FfoApd1a%2BxTznVHVuIno6TqoOpzwt%2FtVSi3AHfdEWT%2B2wj4xNGgmZB744IE1nlM95q4LiL%2F%2FnkLcskxLJnMxcTsgIBF5xHJTHjklPFe%2BF3FUPPiqe1QaJtn81G24KMQV6r2pTcZRn5PPT3MDPXFVyuiu1iiAJIY4wHOBl025RvbF0Fmh0u7O9fkbP%2BoerOz4SK%2Fsk3H1Y%3D" rel="nofollow">http://www.mashu.com:8080/hel...</a></p><p><img src="/img/bVbICV3" alt="image.png" title="image.png"></p><p>输入账号密码mashu/mashu,登录成功后又回来了!哈哈哈哈嗝,并携带了登录凭证。</p><p><img src="/img/bVbICX5" alt="image.png" title="image.png"></p><h3>多系统登录</h3><p>再启动一个客户端,打开idea 的<strong>edit configurations</strong>设置。勾选 <strong>Allow parallel run</strong></p><p><img src="/img/bVbICZb" alt="image.png" title="image.png"></p><p>修改application.properties,服务的端口等信息,再点击启动,就可以同时启动了(8081/8080)两个客户端</p><pre><code>server.port=8081
#cas服务端的地址
cas.server-url-prefix=https://www.cainiao.com:8443/cas
#cas服务端的登录地址
cas.server-login-url=https://www.cainiao.com:8443/cas/login
#客户端访问地址
cas.client-host-url=http://www.mshu.com:8081
cas.validation-type=CAS3</code></pre><p>访问第二个客户端 <a href="https://link.segmentfault.com/?enc=voxfb5RFj9W7CavBgKdqZQ%3D%3D.qeMIP9evJQCI410j5CAQzq1mjEwXcxH5wln9km3sgeWblF7yDjGwc9%2FyktFokIEpnIhsBCkIRwCWtct76cyLbrI4vCxN3wT1YI3jmMRuKn7by9njrbr2XjtwrxB5%2ByQJihiLCC1wDhvbWVRdDV%2FisIK0i%2FFwW8rCJbuXeVWYfcw7F0%2F86tvdqm26%2FBjX1GsxvS%2B5xChGmJWS%2FfvzrSTZTWLD%2BTTDUQtTa0aPo7vXa4KlK7ZH766sVPf%2FeoI8luNeW60vOTj%2BK37oTHO3dGGXBZWvI9HYMLWn2aEz7%2BjDQd%2BcTkU7kLxCH%2F2JOIooT3v6pwX2UcDNwNmDLkFsmaElfA%3D%3D" rel="nofollow">http://www.mshu.com:8081/hell...</a></p><p><img src="/img/bVbICZs" alt="image.png" title="image.png"></p><h2>点单登出</h2><blockquote>添加两个配置文件;</blockquote><h3>1. CasProperties.java</h3><pre><code>import org.springframework.boot.context.properties.ConfigurationProperties;
import javax.validation.constraints.NotNull;
@ConfigurationProperties(prefix = "cas",ignoreUnknownFields = true)
public class CasProperties {
/**
* CAS server URL E.g. https://example.com/cas or https://cas.example. Required.
* CAS 服务端 url 不能为空
*/
@NotNull
private String serverUrlPrefix;
/**
* CAS server login URL E.g. https://example.com/cas/login or https://cas.example/login. Required.
* CAS 服务端登录地址 上面的连接 加上/login 该参数不能为空
*/
@NotNull
private String serverLoginUrl;
/**
* CAS-protected client application host URL E.g. https://myclient.example.com Required.
* 当前客户端的地址
*/
@NotNull
private String clientHostUrl;
/**
* 忽略规则,访问那些地址 不需要登录
*/
private String ignorePattern;
/**
* 自定义UrlPatternMatcherStrategy验证
*/
private String ignoreUrlPatternType;
public String getServerUrlPrefix() {
return serverUrlPrefix;
}
public void setServerUrlPrefix(String serverUrlPrefix) {
this.serverUrlPrefix = serverUrlPrefix;
}
public String getServerLoginUrl() {
return serverLoginUrl;
}
public void setServerLoginUrl(String serverLoginUrl) {
this.serverLoginUrl = serverLoginUrl;
}
public String getClientHostUrl() {
return clientHostUrl;
}
public void setClientHostUrl(String clientHostUrl) {
this.clientHostUrl = clientHostUrl;
}
public String getIgnorePattern() {
return ignorePattern;
}
public void setIgnorePattern(String ignorePattern) {
this.ignorePattern = ignorePattern;
}
public String getIgnoreUrlPatternType() {
return ignoreUrlPatternType;
}
public void setIgnoreUrlPatternType(String ignoreUrlPatternType) {
this.ignoreUrlPatternType = ignoreUrlPatternType;
}
}</code></pre><h3>2. Configs.java</h3><pre><code>import org.jasig.cas.client.authentication.AuthenticationFilter;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.jasig.cas.client.util.HttpServletRequestWrapperFilter;
import org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.EventListener;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableConfigurationProperties(CasProperties.class)
public class Configs {
@Autowired
private CasProperties configProps;
/**
* 配置登出过滤器
* @return
*/
@Bean
public FilterRegistrationBean filterSingleRegistration() {
final FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new SingleSignOutFilter());
// 设定匹配的路径
registration.addUrlPatterns("/*");
Map<String,String> initParameters = new HashMap<String, String>();
initParameters.put("casServerUrlPrefix", configProps.getServerUrlPrefix());
registration.setInitParameters(initParameters);
// 设定加载的顺序
registration.setOrder(1);
return registration;
}
/**
* 配置过滤验证器 这里用的是Cas30ProxyReceivingTicketValidationFilter
* @return
*/
@Bean
public FilterRegistrationBean filterValidationRegistration() {
final FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new Cas30ProxyReceivingTicketValidationFilter());
// 设定匹配的路径
registration.addUrlPatterns("/*");
Map<String,String> initParameters = new HashMap<String, String>();
initParameters.put("casServerUrlPrefix", configProps.getServerUrlPrefix());
initParameters.put("serverName", configProps.getClientHostUrl());
initParameters.put("useSession", "true");
registration.setInitParameters(initParameters);
// 设定加载的顺序
registration.setOrder(2);
return registration;
}
/**
* 配置授权过滤器
* @return
*/
@Bean
public FilterRegistrationBean filterAuthenticationRegistration() {
final FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new AuthenticationFilter());
// 设定匹配的路径
registration.addUrlPatterns("/*");
Map<String,String> initParameters = new HashMap<String, String>();
initParameters.put("casServerLoginUrl", configProps.getServerLoginUrl());
initParameters.put("serverName", configProps.getClientHostUrl());
if(configProps.getIgnorePattern() != null && !"".equals(configProps.getIgnorePattern())){
initParameters.put("ignorePattern", configProps.getIgnorePattern());
}
//自定义UrlPatternMatcherStrategy 验证规则
if(configProps.getIgnoreUrlPatternType() != null && !"".equals(configProps.getIgnoreUrlPatternType())){
initParameters.put("ignoreUrlPatternType", configProps.getIgnoreUrlPatternType());
}
registration.setInitParameters(initParameters);
// 设定加载的顺序
registration.setOrder(3);
return registration;
}
/**
* request wraper过滤器
* @return
*/
@Bean
public FilterRegistrationBean filterWrapperRegistration() {
final FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new HttpServletRequestWrapperFilter());
// 设定匹配的路径
registration.addUrlPatterns("/*");
// 设定加载的顺序
registration.setOrder(4);
return registration;
}
/**
* 添加监听器
* @return
*/
@Bean
public ServletListenerRegistrationBean<EventListener> singleSignOutListenerRegistration(){
ServletListenerRegistrationBean<EventListener> registrationBean = new ServletListenerRegistrationBean<EventListener>();
registrationBean.setListener(new SingleSignOutHttpSessionListener());
registrationBean.setOrder(1);
return registrationBean;
}
}
</code></pre><p>登出地址: <a href="https://link.segmentfault.com/?enc=H4umEhzzdVGUpTjHAm0jng%3D%3D.L1Qb4xDGtx6vEC6Q9z%2BHJ%2BxSDC9Q6AwxfiljCE7FV1NJK4WbhhpovdRMu1JWQXuzZ91h4JTfs6fh8u6Q6ZsjgR9qlicz2dFiWZY3nZmVaOgBsdNoJsRdHJ3%2FGoDLXp4IXkWurJO4H4PRdxxcH4Q3LQ%3D%3D" rel="nofollow">https://www.cainiao.com:8443/...</a><br>再次访问客户端发现自动跳到了登录页面,即客户端也自动退出成功。</p><p><img src="/img/bVbIC0z" alt="image.png" title="image.png"></p><h2>获取当前登录用户的姓名</h2><pre><code> @RequestMapping("/hello")
public String hello(HttpSession session) {
Assertion assertion = (Assertion)session.getAttribute(CONST_CAS_ASSERTION);
AttributePrincipal principal = assertion.getPrincipal();
String loginName = principal.getName();
return "hello "+loginName;
}</code></pre><h2>一些问题</h2><p>最开始我想把客户端也加一个证书,用https访问。免得在服务端做修改去支持http, <br>当我添加证书后,单点登录正常,但是登出功能总是失败,表现为服务端退出,客户端没有退出。<br>我一直以为客户端配置的登出有问题,搞了半天都没成功,后来我把客户端的证书去掉,就成功了。想了想<strong>大概是因为我们自己生成的证书不能被服务端认可</strong>,因为登出的时候需要服务端向客户端发起广播,而我们之前修改的<code>HTTPSandIMAPS-10000001.json</code>文件只是作用于客户端向服务端的请求。和登出相反。</p><p>在我使用springboot配置证书的时候,2.1.0.RELEASE以上版本的spring-boot-starter-parent都不行。会报错<code>org.apache.tomcat.jni.SSL.renegotiatePending(J)I</code>。</p><h5>demo地址</h5><p>有一些小伙伴私信问我在搭建过程中的各种问题,我又特地搭了一遍,并上传到github上面去了,<a href="https://link.segmentfault.com/?enc=l3qg0sji6jZFldNVxgM4Hw%3D%3D.j4ven9OFrcOEbkHWOwgXOFNZICqMam8fXPjJ%2BuiwI772GuoUR9FROYWAoMgC6Npd" rel="nofollow">传送门</a>。<br>在第一次搭的时候,最好还是认真的按照教程搭建,在成功之后再去做其他方向的修改,一来提高成功率,少走弯路,另外,成功过一次在做其他方向的修改如果失败了,更容易排查问题。</p>
从main方法分析内存溢出
https://segmentfault.com/a/1190000022827507
2020-06-03T16:50:12+08:00
2020-06-03T16:50:12+08:00
大树
https://segmentfault.com/u/mshu
1
<p>内存溢出OutOfMemoryError不常遇到,起码没有姨妈空指针异常(NullPointerException)来的那么频繁。<br>现在就用最简单的main方法复现堆内存溢出并做分析。</p><p><img src="/img/bVbKDvW" alt="image.png" title="image.png"></p><h2>概念先行</h2><p><strong>JVM内存模型(JMM):</strong><br> 堆,方法区,本地方法栈,虚拟机栈,程序计数器 (<em>前面两个线程共享</em>)<br><strong>栈和堆:</strong><br>栈是运行空间,堆是存储空间,类似于我小米手机的运行内存(RAM)8G和存储空间(ROM)128G。<br>java中基本类型和堆对象的引用存在栈中。<br><strong>堆:</strong><br>堆在JVM中占据了很大的空间,用来存放实例对象,等会儿我们就拿它下手!<br><em>堆:我当时害怕急了。</em><br>堆内存分为年轻代和老年代,java8之后没有了永久代。(往细了说年轻代还有伊甸园(eden)和两个幸存区(from、to))<br><strong>内存溢出</strong><br>内存溢出有五种:</p><ol><li>java.lang.OutOfMemoryError: java heap space</li><li>java.lang.OutOfMemoryError: GC over head limit exceeded</li><li>java.lang.OutOfMemoryError: PermGen space</li><li>java.lang.OutOfMemoryError: Direct buffer memory</li><li>java.lang.StackOverflowError<br>java heap space:<br>当java对象在年轻代存活一段时间经历过N次回收没有被回收掉后(N还可以自己设置),就会进入老年代,在老年代中又经历回收后积累的没有被回收的对象超负荷后就会抛出内存溢出的异常。</li></ol><p><strong>垃圾回收机制算法</strong>:</p><ol><li>标记清除算法</li><li>标记整理算法</li><li>复制算法</li></ol><h2>堆内存溢出</h2><p>我把堆内存设置小一点先。<br>在idea的配置 (VM options)加上启动参数 -Xms10m -Xmx20m。<br>运行以下代码,不断的生成People对象并放入集合中防止被回收。</p><pre><code>public static void main(String[] args) {
List<People> peoples = new ArrayList<>();
int i = 0;
while (true) {
People abc = new People();
i++;
peoples.add(abc);
System.out.println(abc.toString() + i);
}
}</code></pre><p>结果在生成540217个对象的时候抛出了内存溢出的异常。</p><pre><code>......
People{name='null', sex='null'}540216
People{name='null', sex='null'}540217
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.add(ArrayList.java:462)
at com.example.demo.DemoMain.main(DemoMain.java:17)</code></pre><h2>开启GC日志</h2><p>添加启动参数,不同的参数打印不同格式的日志。</p><table><thead><tr><th>参数</th><th>说明</th></tr></thead><tbody><tr><td>-XX:-PrintGC</td><td>开启GC日志</td></tr><tr><td>-XX:-PrintGCDetails</td><td>打印详细信息</td></tr><tr><td>-XX:+PrintGCDateStamps</td><td>打印时间</td></tr><tr><td>-Xloggc:./gclogs.log</td><td>GC日志的生成路径和日志名称</td></tr></tbody></table><p>加上后看到的GC日志如下,太多了,我截取了后半段,可以看到短期内GC之后都是Full GC,<br>应该是老年代满了,触发Full GC但是还是有大量的对象不能被回收,最后抛出OOM的异常。<br>年轻代满了会触发GC,也叫Minor GC,老年代满了会触发Full GC,CPU空闲时也会回收垃圾,<br><strong>JVM调优的目的就是减少GC的频率和Full GC的次数。</strong></p><pre><code>2020-06-02T22:30:54.038+0800: 4.966: [GC (Allocation Failure) 12088K->7608K(15872K), 0.0046395 secs]
2020-06-02T22:30:54.151+0800: 5.079: [GC (Allocation Failure) 12216K->7800K(15872K), 0.0055183 secs]
2020-06-02T22:30:54.229+0800: 5.158: [GC (Allocation Failure) 12408K->9383K(15872K), 0.0062808 secs]
2020-06-02T22:30:54.347+0800: 5.275: [GC (Allocation Failure) 13991K->9551K(15872K), 0.0045744 secs]
2020-06-02T22:30:54.467+0800: 5.394: [GC (Allocation Failure) 14159K->9751K(15872K), 0.0054287 secs]
2020-06-02T22:30:54.472+0800: 5.400: [Full GC (Ergonomics) 9751K->8508K(19456K), 0.1291069 secs]
2020-06-02T22:30:54.714+0800: 5.642: [GC (Allocation Failure) 13116K->8796K(19456K), 0.0035147 secs]
2020-06-02T22:30:54.826+0800: 5.754: [GC (Allocation Failure) 13404K->8988K(19456K), 0.0054200 secs]
2020-06-02T22:30:54.941+0800: 5.869: [GC (Allocation Failure) 13596K->9084K(19456K), 0.0070004 secs]
2020-06-02T22:30:55.076+0800: 6.005: [GC (Allocation Failure) 13692K->9340K(19456K), 0.0082953 secs]
2020-06-02T22:30:55.206+0800: 6.134: [GC (Allocation Failure) 13948K->9468K(18432K), 0.0099645 secs]
2020-06-02T22:30:55.311+0800: 6.240: [GC (Allocation Failure) 13052K->9604K(18944K), 0.0110139 secs]
2020-06-02T22:30:55.404+0800: 6.332: [GC (Allocation Failure) 13188K->9780K(18944K), 0.0114867 secs]
2020-06-02T22:30:55.508+0800: 6.436: [GC (Allocation Failure) 13364K->9988K(18944K), 0.0095337 secs]
2020-06-02T22:30:55.600+0800: 6.528: [GC (Allocation Failure) 13572K->10052K(18944K), 0.0077667 secs]
2020-06-02T22:30:55.697+0800: 6.625: [GC (Allocation Failure) 13636K->10308K(18944K), 0.0076699 secs]
2020-06-02T22:30:55.789+0800: 6.717: [GC (Allocation Failure) 13892K->10460K(18944K), 0.0044046 secs]
2020-06-02T22:30:55.876+0800: 6.804: [GC (Allocation Failure) 14044K->10580K(17920K), 0.0046781 secs]
2020-06-02T22:30:55.965+0800: 6.892: [GC (Allocation Failure) 14164K->10764K(18432K), 0.0047827 secs]
2020-06-02T22:30:56.057+0800: 6.985: [GC (Allocation Failure) 14348K->10892K(18432K), 0.0050403 secs]
2020-06-02T22:30:56.171+0800: 7.100: [GC (Allocation Failure) 14476K->11036K(18432K), 0.0052502 secs]
2020-06-02T22:30:56.283+0800: 7.211: [GC (Allocation Failure) 15132K->11188K(18944K), 0.0052689 secs]
2020-06-02T22:30:56.340+0800: 7.267: [GC (Allocation Failure) 15284K->13394K(19456K), 0.0086552 secs]
2020-06-02T22:30:56.348+0800: 7.276: [Full GC (Ergonomics) 13394K->11645K(19456K), 0.2421109 secs]
2020-06-02T22:30:56.703+0800: 7.631: [GC (Allocation Failure) 16253K->11965K(19456K), 0.0045707 secs]
2020-06-02T22:30:56.821+0800: 7.749: [GC (Allocation Failure) 16573K->12125K(19456K), 0.0061328 secs]
2020-06-02T22:30:56.943+0800: 7.871: [GC (Allocation Failure) 16733K->12253K(19456K), 0.0067886 secs]
2020-06-02T22:30:57.066+0800: 7.994: [GC (Allocation Failure) 16861K->12509K(19456K), 0.0097933 secs]
2020-06-02T22:30:57.192+0800: 8.121: [GC (Allocation Failure) 17117K->12677K(19456K), 0.0115250 secs]
2020-06-02T22:30:57.312+0800: 8.241: [GC (Allocation Failure) 17285K->12789K(18432K), 0.0135347 secs]
2020-06-02T22:30:57.416+0800: 8.344: [GC (Allocation Failure) 16373K->12957K(18944K), 0.0105043 secs]
2020-06-02T22:30:57.516+0800: 8.445: [GC (Allocation Failure) 16541K->13093K(18944K), 0.0103981 secs]
2020-06-02T22:30:57.620+0800: 8.548: [GC (Allocation Failure) 16677K->13237K(18944K), 0.0098493 secs]
2020-06-02T22:30:57.729+0800: 8.657: [GC (Allocation Failure) 16821K->13421K(18944K), 0.0087252 secs]
2020-06-02T22:30:57.738+0800: 8.666: [Full GC (Ergonomics) 13421K->13218K(18944K), 0.1904231 secs]
2020-06-02T22:30:58.021+0800: 8.949: [Full GC (Ergonomics) 16802K->13300K(18944K), 0.2117216 secs]
2020-06-02T22:30:58.335+0800: 9.264: [Full GC (Ergonomics) 16884K->13435K(18944K), 0.1494933 secs]
2020-06-02T22:30:58.606+0800: 9.533: [Full GC (Ergonomics) 17019K->13569K(18944K), 0.1366324 secs]
2020-06-02T22:30:58.838+0800: 9.766: [Full GC (Ergonomics) 17153K->13703K(18944K), 0.1436044 secs]
2020-06-02T22:30:59.069+0800: 9.998: [Full GC (Ergonomics) 17287K->13837K(18944K), 0.1610447 secs]
2020-06-02T22:30:59.318+0800: 10.246: [Full GC (Ergonomics) 17402K->13971K(18944K), 0.1682002 secs]
2020-06-02T22:30:59.592+0800: 10.520: [Full GC (Ergonomics) 17402K->14099K(18944K), 0.1644734 secs]
2020-06-02T22:30:59.858+0800: 10.787: [Full GC (Ergonomics) 17402K->14223K(18944K), 0.2900750 secs]
2020-06-02T22:31:00.248+0800: 11.176: [Full GC (Ergonomics) 17402K->14342K(18944K), 0.1845255 secs]
2020-06-02T22:31:00.532+0800: 11.461: [Full GC (Ergonomics) 17402K->14457K(18944K), 0.1687967 secs]
2020-06-02T22:31:00.774+0800: 11.702: [Full GC (Ergonomics) 17402K->14567K(18944K), 0.2179055 secs]
2020-06-02T22:31:01.070+0800: 11.998: [Full GC (Ergonomics) 17402K->14674K(18944K), 0.2099567 secs]
2020-06-02T22:31:01.376+0800: 12.304: [Full GC (Ergonomics) 17402K->14776K(18944K), 0.1890367 secs]
2020-06-02T22:31:01.638+0800: 12.567: [Full GC (Ergonomics) 17402K->14874K(18944K), 0.1799833 secs]
2020-06-02T22:31:01.877+0800: 12.805: [Full GC (Ergonomics) 17402K->14969K(18944K), 0.1828141 secs]
2020-06-02T22:31:02.121+0800: 13.050: [Full GC (Ergonomics) 17402K->15060K(18944K), 0.1930089 secs]
2020-06-02T22:31:02.366+0800: 13.295: [Full GC (Ergonomics) 17402K->15148K(18944K), 0.1954160 secs]
2020-06-02T22:31:02.629+0800: 13.557: [Full GC (Ergonomics) 17402K->15232K(18944K), 0.1789831 secs]
2020-06-02T22:31:02.862+0800: 13.791: [Full GC (Ergonomics) 17402K->15313K(18944K), 0.1870638 secs]
2020-06-02T22:31:03.136+0800: 14.065: [Full GC (Ergonomics) 17402K->15392K(18944K), 0.2053623 secs]
2020-06-02T22:31:03.409+0800: 14.337: [Full GC (Ergonomics) 17402K->15467K(18944K), 0.1735414 secs]
2020-06-02T22:31:03.631+0800: 14.559: [Full GC (Ergonomics) 17402K->15539K(18944K), 0.1986520 secs]
2020-06-02T22:31:03.880+0800: 14.809: [Full GC (Ergonomics) 17402K->15609K(18944K), 0.1910945 secs]
2020-06-02T22:31:04.113+0800: 15.041: [Full GC (Ergonomics) 17402K->15676K(18944K), 0.1868629 secs]
2020-06-02T22:31:04.350+0800: 15.278: [Full GC (Ergonomics) 17402K->15741K(18944K), 0.2251443 secs]
2020-06-02T22:31:04.612+0800: 15.541: [Full GC (Ergonomics) 16188K->15757K(18944K), 0.2292204 secs]
2020-06-02T22:31:04.842+0800: 15.770: [Full GC (Allocation Failure) 15757K->15757K(18944K), 0.1922108 secs]</code></pre><p>我把GC 日志粘贴到这个分析GC日志的<a href="https://link.segmentfault.com/?enc=BD%2BTtLEqEjhkoCuRPMuClA%3D%3D.8k1Di%2FmgzbR5CQBPjp%2FdLn%2FYr3%2BpOqwa5MKPsm4fglA%3D" rel="nofollow">网站</a>上,这个可视化工具可以帮我们更直观的欣赏GC的情况。各种饼状图,柱状图,折线图给你安排的明明白白。</p><p><img src="/img/bVbHUtG" alt="image.png" title="image.png"></p><p>选几个看一下,我的内存设置的最大20M,可以看到峰值的时候是16.9M</p><p><img src="/img/bVbHUuf" alt="image.png" title="image.png"></p><p>堆空间渐渐被占满</p><p><img src="/img/bVbHUur" alt="image.png" title="image.png"></p><p>GC和Full GC的回收的大小,时间。<br><img src="/img/bVbHUu1" alt="image.png" title="image.png"></p><h2>分析内存快照</h2><p>想要更详细的分析还得生成内存快照,同样添加启动参数</p><table><thead><tr><th>参数</th><th>说明</th></tr></thead><tbody><tr><td>-XX:+HeapDumpOnOutOfMemoryError</td><td>开启内存快照</td></tr><tr><td>-XX:HeapDumpPath=./</td><td>存储路径</td></tr></tbody></table><p>使用jprofiler打开生成的快照文件(xxx.hprof),结果显而易见,People对象的锅,这样你就可以很方便的找出代码中哪里不洽当使用该对象的地方,精准定位,甩锅给同事了。<br><img src="/img/bVbHUv0" alt="image.png" title="image.png"><br>当然仅限于代码需要优化的情况,如果没有需要优化的还出现这种异常,就需要增大堆内存的空间-Xms-Xmx两个参数。<br>如果还不行就需要结合上面两个工具分析,做出更细化的调整。</p><h2>JVM调优参数</h2><p>-Xms初始分配大小,默认为物理内存的1/64<br>-Xmx初始最大分配内存,默认为物理内存的14</p><table><thead><tr><th>参数</th><th>说明</th></tr></thead><tbody><tr><td>-Xms4g</td><td>堆最小值,和最大值设置一样为宜</td></tr><tr><td>-Xmx4g</td><td>堆最大值</td></tr><tr><td>-Xmn2g</td><td>年轻代大小</td></tr><tr><td>-Xss128k</td><td>每个线程的堆栈大小</td></tr><tr><td>-XX:NewRatio=4</td><td>年轻代和老年代的比例1:4</td></tr><tr><td>-XX:SurvivorRatio=4</td><td>年轻代中Survivor区与Eden区与的大小比值1:4</td></tr><tr><td>-XX:MaxTenuringThreshold=15</td><td>对象在年轻代能经历回收的次数</td></tr><tr><td>-XX:+UseseriaIGC</td><td>使用串行GC收集器</td></tr><tr><td>-XX: +Use ParalleIGC</td><td>使用并行GC收集器</td></tr><tr><td>-XX: +UseParallelOldGC</td><td>使用 parallel old收集器</td></tr><tr><td>-XX: +Use ConcMarkSweepGC</td><td>使用CMS收集器</td></tr></tbody></table><hr><h2>实战 jmap的使用</h2><p>表现:线上环境出现接口响应超慢,或者响应失败,服务器CPU异常的高,飙到300就不正常(top名称查看)</p><p><img src="/img/bVcSxx3" alt="image.png" title="image.png"></p><p><code>ps -ef|grep {PID}</code> 根据PID找到异常的服务</p><p><img src="/img/bVcSxyR" alt="image.png" title="image.png"></p><p><code>jmap -heap {PID}</code> 打印 JVM 堆概要信息,包括堆配置、新生代、老生代信息<br><img src="/img/bVcSxy7" alt="" title=""><br><code>jmap -dump:live,format=b,file={文件名}.jps {PID} </code>导出JVM 堆信息<br><img src="/img/bVcSxzH" alt="image.png" title="image.png"><br><code>sz {文件名}</code> 将文件从服务器上下载下来。<br>再用堆分析工具,比如 visualVM、JProfile、MAT 等进行分析</p>
订单自动过期实现方案
https://segmentfault.com/a/1190000022669324
2020-05-17T23:58:47+08:00
2020-05-17T23:58:47+08:00
大树
https://segmentfault.com/u/mshu
17
<h3>需求分析:</h3><blockquote>24小时内未支付的订单过期失效。</blockquote><h3>解决方案</h3><ol><li><strong>被动设置</strong>:在查询订单的时候检查是否过期并设置过期状态。</li><li><strong>定时调度</strong>:定时器定时查询并过期需要过期的订单。</li><li><strong>延时队列</strong>:将未支付的订单放入一个延时队列中,依次取出过期订单。</li><li><strong>过期提醒</strong>:reids支持将一个过期的key(订单号)通知给客户端,监听过期的key进行相应的处理。</li><li><strong>延时队列</strong>:利用RabbitMq的延时队列功能,订单创建后发送检查订单状态的消息,24小时后监听到该消息并执行订单是否支付完成的逻辑。</li></ol><h4>1. 被动设置</h4><p>这个太简单了,就是在查询的时候判断是否失效,如果失效了就给他设置失效状态。但是弊端也很明显,每次查询都要对未失效的订单做判断,如果用户不查询,订单就不失效,那么如果有类似统计失效状态个数的功能,将会受到影响,所以只能适用于简单独立的场景。简直low爆了。</p><h4>2. 定时调度</h4><p>这种是常见的方法,利用一个定时器,在设置的周期内轮询检查并处理需要过期的订单。<br>具体实现有基于<code>Timer</code>的,有基于<code>Quartz</code>,还有springboot自带的<code>Scheduler</code>,实现起来比较简单。<br>就写一下第三个的实现方法吧:</p><ol><li>启动类加上注解<code>@EnableScheduling</code></li><li>新建一个定时调度类,方法上加上<code>@Scheduled</code>注解,如下图那么简单。<br><img src="/img/bVbHgHw" alt="image.png" title="image.png"></li></ol><p>弊端</p><ol><li>不能够精准的去处理过期订单,轮询周期设置的越小,精准度越高,但是项目的压力越大,我们上一个项目就有这种状况,太多定时器在跑,项目运行起来比较笨重。</li></ol><h4>3. 延时队列</h4><p>基于JDK的实现方法,将未支付的订单放到一个有序的队列中,程序会自动依次取出过期的订单。<br>如果当前没有过期的订单,就会阻塞,直至有过期的订单。由于每次只处理过期的订单,并且处理的时间也很精准,不存在定时调度方案的那两个弊端。<br>实现:<br>1.首先创建一个订单类<code>OrderDelayDto</code>需要实现<code>Delayed</code>接口。然后重写<code>getDelay()</code>方法和<code>compareTo()</code>方法,只加了订单编号和过期时间两个属性。<br>这两个方法很重要,<br><code>getDelay()</code>方法实现过期的策略,比如,订单的过期时间等于当前时间就是过期,返回负数就代表需要处理。否则不处理。<br><code>compareTo()</code>方法实现订单在队列中的排序规则,这样即使后面加入的订单,也能加入到排序中,我这里写的规则是按照过期时间排序,最先过期的排到最前面,这一点很重要,因为排在最前面的如果没有被处理,就会进入阻塞状态,后面的不会被处理。</p><pre><code>import lombok.Data;
import java.util.Date;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* @author mashu
* Date 2020/5/17 16:25
*/
@Data
public class OrderDelayDto implements Delayed {
/**
* 订单编号
*/
private String orderCode;
/**
* 过期时间
*/
private Date expirationTime;
/**
* 判断过期的策略:过期时间大于等于当前时间就算过期
*
* @param unit
* @return
*/
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.expirationTime.getTime() - System.currentTimeMillis(), TimeUnit.NANOSECONDS);
}
/**
* 订单加入队列的排序规则
*
* @param o
* @return
*/
@Override
public int compareTo(Delayed o) {
OrderDelayDto orderDelayDto = (OrderDelayDto) o;
long time = orderDelayDto.getExpirationTime().getTime();
long time1 = this.getExpirationTime().getTime();
return time == time1 ? 0 : time < time1 ? 1 : -1;
}
}
</code></pre><p>其实这样已经算是写好了。我没有耍你。<br>写个main 方法测试一下,创建两个订单o1和o2,放入到延时队列中,然后while()方法不断的去取。<br>在此方法内通过队列的<code>take()</code>方法获得已过期的订单,然后做出相应的处理。</p><pre><code> public static void main(String[] args) {
DelayQueue<OrderDelayDto> queue = new DelayQueue<>();
OrderDelayDto o1 = new OrderDelayDto();
//第一个订单,过期时间设置为一分钟后
o1.setOrderCode("1001");
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, 1);
o1.setExpirationTime(calendar.getTime());
OrderDelayDto o2 = new OrderDelayDto();
//第二个订单,过期时间设置为现在
o2.setOrderCode("1002");
o2.setExpirationTime(new Date());
//往队列中放入数据
queue.offer(o1);
queue.offer(o2);
// 延时队列
while (true) {
try {
OrderDelayDto take = queue.take();
System.out.println("订单编号:" + take.getOrderCode() + " 过期时间:" + take.getExpirationTime());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}</code></pre><p>运行结果:<br><img src="/img/bVbHhkq" alt="image.png" title="image.png"></p><p>我故意把第二个订单的过期时间设置为第一个订单之前,从结果可以看出,他们已经自动排序把最先过期的排到了最前面。<br>第一个订单的失效时间是当前时间的后一分钟,结果也显示一分钟后处理了第一条订单。</p><p>2.然而通常情况下,我们会使用多线程去取延时队列中的数据,这样即使线程启动之后也能动态的向队列中添加订单。<br>创建一个线程类<code>OrderCheckScheduler</code>实现<code>Runnable</code>接口,<br> 添加一个延时队列属性,重写<code>run()</code>方法,在此方法内通过队列的<code>take()</code>方法获得已过期的订单,然后做出相应的处理。</p><pre><code>import java.util.concurrent.DelayQueue;
/**
* @author mashu
* Date 2020/5/17 14:27
*/
public class OrderCheckScheduler implements Runnable {
// 延时队列
private DelayQueue<OrderDelayDto> queue;
public OrderCheckScheduler(DelayQueue<OrderDelayDto> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
try {
OrderDelayDto take = queue.take();
System.out.println("订单编号:" + take.getOrderCode() + " 过期时间:" + take.getExpirationTime());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}</code></pre><p>好了,写个方法测试一下:</p><pre><code> public static void main(String[] args) {
// 创建延时队列
DelayQueue<OrderDelayDto> queue = new DelayQueue<>();
OrderDelayDto o1 = new OrderDelayDto();
//第一个订单,过期时间设置为一分钟后
o1.setOrderCode("1001");
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, 1);
o1.setExpirationTime(calendar.getTime());
OrderDelayDto o2 = new OrderDelayDto();
//第二个订单,过期时间设置为现在
o2.setOrderCode("1002");
o2.setExpirationTime(new Date());
//运行线程
ExecutorService exec = Executors.newFixedThreadPool(1);
exec.execute(new OrderCheckScheduler(queue));
//往队列中放入数据
queue.offer(o1);
queue.offer(o2);
exec.shutdown();
}</code></pre><p>结果和上面的一样,图就不截了,相信我。</p><h4>过期提醒</h4><p>基于redis的过期提醒功能,听名字就知道这个方案最是纯真、最直接的,就是单纯处理过期的订单。<br>修改个redis的配置吧先,因为redis默认不开启过期提醒。<br><code>notify-keyspace-events</code>改为<code>notify-keyspace-events "Ex"</code><br>写一个类用来接收来自redis的暖心提醒<code>OrderExpirationListener</code>,继承一下<code>KeyExpirationEventMessageListener</code>抽象类。重写<code>onMessage()</code>方法,在此方法中处理接收到的过期key.</p><pre><code>import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* @author mashu
* Date 2020/5/17 23:01
*/
@Component
public class OrderExpirationListener extends KeyExpirationEventMessageListener {
public OrderExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {
final String expiredKey = message.toString();
System.out.println("我过期了" + expiredKey+"当前时间:"+new Date());
}
}</code></pre><p>ok,向redis中存入一个订单,过期时间为1分钟。</p><pre><code>redis.set("orderCode/10010", "1", 1L, TimeUnit.MINUTES);
System.out.println("redis存入订单号 key: orderCode/10010,value:1,过期时间一分钟,当前时间"+new Date());</code></pre><p>运行结果:<br><img src="/img/bVbHhqc" alt="image.png" title="image.png"></p><h4>延时队列</h4><p>RabbitMQ默认是不支持延时消息的,需要安装rabbitmq_delayed_message_exchange延时插件,<br>下载地址:<a href="https://link.segmentfault.com/?enc=P8fxiZgDu6oj6coPrSjbeA%3D%3D.nWo8EO6zf917h1JxfQxfqKvoGzWZ5DI7%2FY6ccS0JmLyaHaDfd7XeC%2BubXbr72EsM" rel="nofollow">https://www.rabbitmq.com/comm...</a><br>上传插件rabbitmq_delayed_message_exchange-3.8.0.ez到/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.14/plugins目录。</p><p>开启插件</p><pre><code>rabbitmq-plugins enable rabbitmq_delayed_message_exchange</code></pre><p>查看已安装插件列表</p><pre><code>rabbitmq-plugins list</code></pre><p>重启RabbitMQ</p><pre><code>/bin/systemctl restart rabbitmq-server.service</code></pre><p>生产者延时发送代码:<br>delayTime:延时时间</p><pre><code class="java">rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUTING_KEY, msg, a ->{
a.getMessageProperties().setDelay(delayTime);
return a;
});</code></pre><p>没有绝对的好方案,只有在不同场景下的更合适的方案。随着需求的变化,技术的革新,方案也会不断的被优化和迭代,唯一不变的是工资。</p>
蛮吉学 Docker
https://segmentfault.com/a/1190000020027077
2019-08-09T23:21:10+08:00
2019-08-09T23:21:10+08:00
大树
https://segmentfault.com/u/mshu
2
<h2>What is Docker?</h2><p>docker 是一个可以放东西的容器,那东西是什么?可以是redis、nginx、mysql。总之你能在系统上安装的都可以在dokcer里面安装。</p><blockquote>蛮吉:为什么这么做?</blockquote><p><strong>集中管理,使用方便,安装更方便</strong>,不用到各各目录上去找配置文件啊,启动文件之类的。</p><p>“<em>在我们测试环境明明是好的啊?</em>”,这句话是不是很熟悉?(坏笑)</p><p>比如我们代码需要的组件,redis在测试环境有很多配置需要修改,但是在线上环境如果少了一个甚至版本不一致了,那么在上线的时候,都会出现不可预料的问题。</p><p>那么有了docker 就可以把<strong>组件、环境、配置</strong>封装到一个镜像,交给运维就可以避免许多次脸红脖子粗的事情了。<br>封装好的镜像就可以在不同的系统中运行,这封装和到处运行的思想和Java比较一致。</p><p>名词解释:<br><strong>镜像</strong>:类似于安装包。<br><strong>容器</strong>:一个应用对应一个容器,比如,我安装一个redis,一个nginx ,那么就是两个容器,分别装有redis和nginx。<br>用java思想理解:镜像是对象,容器是实例</p><blockquote>安装docker<br>yum -y install docker</blockquote><blockquote>启动docker<br>systemctl start docker</blockquote><blockquote>查看docker状态<br>systemctl status docker</blockquote><p><img src="/img/bVbwb5E" alt="图片描述" title="图片描述"></p><h2>安装nginx</h2><p>我们先安装一个nginx试试,<br>第一步搜索一下仓库有没有nginx的镜像</p><pre><code>docker search nginx
</code></pre><p><img src="/img/bVbwbix" alt="clipboard.png" title="clipboard.png"></p><p>我们发现列出了很多,那么我们需要的是第一个,把它拉下来(默认最新版)需要特订版本后面加上冒号和版本号。</p><pre><code>docker pull nginx
</code></pre><p>这时候可以使用 <code>docker images</code> 命令查看已经下载下来的镜像们。</p><p><img src="/img/bVbwbjt" alt="clipboard.png" title="clipboard.png"></p><p>有了镜像我们就安装吧</p><pre><code>docker run -p 80:80 -d --name nginx nginx
</code></pre><p>安装成功!对!你没有看错。</p><p><img src="/img/bVbwblv" alt="clipboard.png" title="clipboard.png"></p><p>我来解释一下上面那条命令的参数,</p><ul><li>-p 后面跟端口,冒号前面是宿主机的端口,后面是容器内nginx 的端口</li><li>-d 后台运行</li><li>--name 启动后容器的别名</li><li>最后一个nginx 是镜像的名称。</li></ul><p>docker ps 可以查看正在运行的容器,dockers ps -a 查看所有容器。</p><p><img src="/img/bVbwboQ" alt="clipboard.png" title="clipboard.png"></p><blockquote>蛮吉:如果我想修改nginx 的配置怎么办?</blockquote><h3>容器数据卷</h3><ol><li>可以使用 <code>docker exec -it [CONTAINER ID] bash</code> 进入容器里面找到并修改,<code>exit</code>命令退出容器。</li><li><p>也可以在启动的时候使用<code>-v</code> 参数挂载目录,给容器和宿主机指定目录做个映射。只需要在宿主机指定目录操作,不需要进入容器。<code>-v</code> 可以使用多个。<br> 我先在主机新建好了目录和配置文件。</p><ul><li>主机nginx配置文件:<em>/app/nginx/conf/nginx.conf</em></li><li>容器nginx配置文件:<em>/etc/nginx/nginx.conf</em></li><li>主机redis日志目录:/app/nginx/logs</li><li>容器redis日志目录:/var/log/nginx</li></ul></li></ol><pre><code>docker run -d -p 8082:80 --name nginx3 -v /app/nginx/conf/nginx.conf:/etc/nginx/nginx.conf -v /app/nginx/logs:/var/log/nginx nginx
</code></pre><p>如果安装redis 也是那么简单,两条命令就可以完成安装</p><pre><code>docker pull redis
docker run -p 6379:6379 -d --name redis-6379 redis
</code></pre><p><em>当你 <code>docker exec -it [CONTAINER ID] bash</code> 进入容器后发现,原来里面是一个精简版的linux.</em></p><h2>制作镜像</h2><blockquote>蛮吉:那我们能不能自己制作镜像?</blockquote><p>答案是肯定的,</p><p>我们就用运行jar包为例。<br>比如我写了一个spring-boot 的项目,把它打成jar包 :<code>datashare-0.0.1-SNAPSHOT.jar</code>,<br>怎么打jar包?先这样,然后那样,最后再这样一下就好了。</p><blockquote>蛮吉:怎么运行?</blockquote><p><code>java -jar datashare-0.0.1-SNAPSHOT.jar &</code> ?<br><code>nohup java -jar datashare-0.0.1-SNAPSHOT.jar >info.log &</code> ?</p><p>no no no <br>我要用docker 部署。<br>用docker 部署 就要先有镜像,制作镜像只需要一个Dockerfile文件就可以啦<br>以下是Dockerfile的内容:</p><pre><code># openjdk 基础镜像 是我pull到本地的,运行Jar 需要jdk 环境
FROM openjdk
# ?作者签名
MAINTAINER MSHU
# ?简化 jar 的名字路径
COPY datashare-0.0.1-SNAPSHOT.jar datashare.jar
# ?执行 java -jar 命令
CMD java -jar datashare.jar
# ?设置对外端口为 8089
EXPOSE 8089</code></pre><p>运行Jar 需要jdk 环境,所以我提前<code>docker pull openjdk</code>到本地了。<br>将我们新建的 Dockerfile 和<code>datashare-0.0.1-SNAPSHOT.jar</code>放一起,该目录不要有其他文件。</p><p>运行 <code>docker build -t datashare .</code> 开始制作。</p><ul><li><code>datashare</code> 代表制作的镜像名称,<code>.</code>代表使用当前目录的 Dockerfile 。</li></ul><p>镜像做好了放入容器吧。<br><code>docker run -p 8089:8089 -d --name datashare datashare</code> </p><p>成功了!<br><img src="/img/bVbwbUd" alt="clipboard.png" title="clipboard.png"></p><blockquote>蛮吉:如果我想把这个镜像拷贝出来,在我同事电脑运行行不行?</blockquote><p>满足你!<br><code>docker save -o datashare.img datashare</code><br>在当前目录导出名为 datashare.img 的镜像。<br>然后复制到你同事电脑上并运行<code>docker load -i datashare.img</code>就导入了。</p><h2>镜像加速</h2><p>官方仓库在国外,下载缓慢,使用以下方法可以更快!和 maven 一个道理。</p><p><strong>centos7以上</strong>:<br>修改 <code>/etc/docker/daemon.json</code>文件,<strong>如果没有就新建 !</strong><br>内容:</p><pre><code>{
"registry-mirrors": ["http://hub-mirror.c.163.com"]
}</code></pre><p><strong>重启docker不能忘 !</strong></p><pre><code>systemctl restart docker
</code></pre><h2>珍藏命令</h2><table><tbody><tr><td>docker pull [nginx]</td><td>下载容器</td></tr><tr><td>docker ps</td><td>查看正在运行的容器</td></tr><tr><td>docker ps -a</td><td>查看所有容器</td></tr><tr><td>docker exec -t -i [id] bash</td><td>根据id进入对应的软件目录</td></tr><tr><td>docker start [id]</td><td>启动某个容器</td></tr><tr><td>docker stop [id]</td><td>停止某个容器</td></tr><tr><td>docker restart [id]</td><td>重启容器</td></tr><tr><td>docker images</td><td>查看所有已安装的镜像</td></tr><tr><td>docker inspect [id]</td><td>查看启动容器的挂载信息</td></tr><tr><td>docker rm [id]</td><td>删除容器</td></tr><tr><td>docker rmi [镜像id]</td><td>删除镜像</td></tr><tr><td>docker logs -f -t --tail 200</td><td>容器ID 查看容器日志</td></tr><tr><td>systemctl enable docker</td><td>设置开机启动</td></tr><tr><td>service docker start</td><td>启动docker服务</td></tr><tr><td>usermod -G docker [用户名]</td><td>给指定用户添加权限</td></tr><tr><td>docker cp [id]:目录 /主机目录</td><td>复制容器某路径下的文件(夹)到主机某路径下的文件(夹)</td></tr><tr><td>docker commit -m="提交的描述信息" -a="作者" [原容器id:[标签名]]</td><td>将容器封装成镜像</td></tr><tr><td>docker logs -f [镜像id]</td><td>查看容器日志</td></tr></tbody></table><p>docker run 后面也有很多参数,我知道的也不多,各位自行搜索吧,我去看《魁拔》啦,真心话真好看!</p>
五分钟快速了解ActiveMQ,案例简单且详细!
https://segmentfault.com/a/1190000019033292
2019-04-29T20:26:58+08:00
2019-04-29T20:26:58+08:00
大树
https://segmentfault.com/u/mshu
2
<p>最近得闲,探索了一下ActiveMQ。</p>
<blockquote>ActiveMQ消息队列,信息收发的容器,作用有异步消息,流量削锋,应用耦合。<br>同行还有 Kafka、RabbitMQ、RocketMQ、ZeroMQ、MetaMQ 。</blockquote>
<h2>安装</h2>
<p>下载地址:<a href="https://link.segmentfault.com/?enc=3cziagGvvR9c09GBwP%2FzOQ%3D%3D.9h%2FGdFKj7uTxNDDujaSVC1GMgisVmuYCFv44hgqvy1tgpw01IOPBvb0TEVI2%2BRbY3ZrB5T402%2BJz34VrTkQepg%3D%3D" rel="nofollow">http://activemq.apache.org/co...</a></p>
<p>window版本的解压后双击/bin/<code>activemq.bat</code> 即可启动。<br>或者以服务方式启动:右键管理员运行<code>InstallService.bat</code>,然后在Windows系统的服务中启动。</p>
<p>它有自己的可视化页面:<a href="https://link.segmentfault.com/?enc=gzVISIHpKahdHJbr%2BpWCLQ%3D%3D.1Ddw8YuXMsC2X3n%2FJgQassXhyOukVWpQuqVBp2gTCVg%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=cvg9Qxv6n%2Fmw1A08yQnsxQ%3D%3D.uU35bHxiEV6HHo%2BG%2FJvkbYyo87yoWVw9SJ4be6Uv8Zg%3D" rel="nofollow">http://localhost</a>:8161/admin/</p>
<p><img src="/img/bVbr0PU?w=1841&h=677" alt="clipboard.png" title="clipboard.png"></p>
<p>默认访问密码是:admin/admin<br>如果需要修改在:/conf/jetty-realm.properties 中修改</p>
<h2>JmsTemplate</h2>
<p>在<code>springboot</code>上整合的,使用spring 的<code>JmsTemplate</code>来操作ActiveMQ<br>一、首先在pom文件中导入所需的jar包坐标:</p>
<pre><code> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-pool</artifactId>
</dependency></code></pre>
<p>二、新增一个ActiveMQ的配置文件<code>spring-jms.xml</code></p>
<pre><code><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
<!-- 配置JMS连接工厂 -->
<bean id="innerConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL" value="${spring.activemq.broker-url}" />
</bean>
<!--配置连接池-->
<bean id="pooledConnectionFactory" class="org.apache.activemq.pool.PooledConnectionFactory"
destroy-method="stop">
<property name="connectionFactory" ref="innerConnectionFactory" />
<property name="maxConnections" value="100"></property>
</bean>
<!-- 配置JMS模板,Spring提供的JMS工具类 -->
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="pooledConnectionFactory" />
<property name="defaultDestination" ref="JmsSenderDestination" />
<property name="receiveTimeout" value="10000" />
</bean>
</beans></code></pre>
<p>三、在启动类上配置以生效</p>
<pre><code>@ImportResource(locations={"classpath:/config/spring-jms.xml"})
</code></pre>
<p>四、在application.properties中配置ActiveMQ 的连接地址</p>
<pre><code>spring.activemq.broker-url=tcp://localhost:61616
</code></pre>
<blockquote>准备就绪;开始写生产者和消费者,我这里把生产者和消费者写在一个项目里面。在这之前需要明白两个概念<br>队列(Queue)和主题(Topic)</blockquote>
<h2>传递模型</h2>
<p>队列(Queue)和主题(Topic)是JMS支持的两种消息传递模型:</p>
<ol>
<li>点对点(point-to-point,简称PTP)Queue消息传递模型:<br>一个消息只能被一个消费者消费</li>
<li>发布/订阅(publish/subscribe,简称pub/sub)Topic消息传递模型:<br>一个消息会被多个消费者消费</li>
</ol>
<h2>Queue</h2>
<p><img src="/img/bVbwEA2?w=910&h=654" alt="图片描述" title="图片描述"></p>
<ol>
<li>
<p>先在<code>spring-jms.xml</code>里添加配置一个队列名称<code>Queue_love</code></p>
<pre><code><bean id="JmsSenderDestination" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg>
<value>Queue_love</value>
</constructor-arg>
</bean>
</code></pre>
</li>
<li>
<p>创建一个生产者来发送消息;<code>@Qualifier("JmsSenderDestination")</code>指定了发送到上面配置的<code>Queue_love</code>队列</p>
<pre><code>@Component
public class JmsSender {
@Autowired
private JmsTemplate jmsTemplate;
@Qualifier("JmsSenderDestination")
@Autowired
protected Destination destination;
public void sendMessage(final String msg) {
logger.info("QUEUE destination :" + destination.toString() + ", 发送消息:" + msg);
jmsTemplate.send(destination, new MessageCreator() {
@Override
public Message createMessage(final Session session) throws JMSException {
return session.createTextMessage(msg);
}
});
}}</code></pre>
</li>
<li>
<p>创建一个消费者来消费消息:</p>
<pre><code>@Component
public class JmsTemplateListener implements MessageListener {
@Override
public void onMessage(Message message) {
final TextMessage tm = (TextMessage) message;
try {
logger.info("QUEUE接收信息==="+tm.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}}</code></pre>
</li>
<li>
<p>消费者需要在<code>spring-jms.xml</code>配置一下并指明该消费者需要消费哪个队列的消息</p>
<pre><code><!-- 配置消息队列监听者 -->
<bean id="JmsListener" class="com.mashu.activeMq.jmsTemplate.JmsTemplateListener" />
<!-- 使用spring进行配置 监听 -->
<bean id="JmsTemplateListenerContainer"
class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="pooledConnectionFactory"></property>
<property name="destination" ref="JmsSenderDestination"></property>
<property name="messageListener" ref="JmsListener"></property>
<property name="sessionTransacted" value="false"></property>
<property name="concurrentConsumers" value="6"></property>
<property name="concurrency" value="2-4"></property>
<property name="maxConcurrentConsumers" value="10"></property>
</bean></code></pre>
</li>
</ol>
<p>这样一个简单的消息队列收发程序已经写好了,调用生产者方法,看看结果</p>
<p><img src="/img/bVbr1nL?w=628&h=50" alt="clipboard.png" title="clipboard.png"></p>
<p>发出的消息立马就被消费了!<br>我们可以先把消费者注释掉,只用生产者发送消息就可以在可视化页面上看到还没有被消费的消息内容。</p>
<p><img src="/img/bVbr1o1?w=1347&h=809" alt="clipboard.png" title="clipboard.png"></p>
<h2>Topic</h2>
<p><img src="/img/bVbwEBg?w=910&h=654" alt="图片描述" title="图片描述"><br>Topic的方式和Queue类似,只需要在定义队列的时候<code>calss</code>=<code>org.apache.activemq.command.ActiveMQTopic</code>即可</p>
<pre><code><bean id="JmsSenderTDestination" class="org.apache.activemq.command.ActiveMQTopic">
<constructor-arg>
<value>Topic_love</value>
</constructor-arg>
</bean></code></pre>
<h2>Message</h2>
<p>除了使用<code>createTextMessage()</code>方法发送纯字符串消息,还有</p>
<ol>
<li>序列化对象的形式<br><code>session.createObjectMessage()</code>;</li>
<li>流的形式,可以用来传递文件<br><code>session.createStreamMessage()</code>;</li>
<li>字节的形式<br><code>session.createBytesMessage()</code>;</li>
<li>map的形式<br><code>session.createMapMessage()</code>;</li>
</ol>
<h2>安全配置</h2>
<p>ActiveMQ在使用的时候和<code>MySQL</code>一样,也可以配置用户名密码,默认不没有,我们可以打开:</p>
<ol>
<li>
<p>在conf/<code>activemq.xml</code>添加以下信息(务必在<code><systemUsage></code>标签上面)</p>
<pre><code> <plugins>
<simpleAuthenticationPlugin>
<users>
<authenticationUser username="${activemq.username}"
password="${activemq.password}" groups="users,admins"/>
</users>
</simpleAuthenticationPlugin>
</plugins>
</code></pre>
</li>
<li>
<p>对应的用户名密码在/conf/<code>credentials.properties</code>中配置</p>
<pre><code>activemq.username=admin
activemq.password=123456
guest.password=password</code></pre>
</li>
<li>
<p>那么我们在项目中的<code>application.properties</code>需要加上也要用户名密码:</p>
<pre><code>spring.activemq.broker-url=tcp://localhost:61616
spring.activemq.user=admin
spring.activemq.password=123456</code></pre>
</li>
</ol>
<h2>消息持久化</h2>
<p>ActiveMQ的持久化机制包含JDBC,KahaDB(默认)、LevelDB<br>默认保存的消息在\data\kahadb目录下;下面方法修改为用<code>MySQL(JDBC)</code>保存,<br>生产者发送消息存储到<code>MySQL</code>数据库,消费者消费后消息从数据库消失。</p>
<ol>
<li>
<p>修改/conf/activemq.xml<br>将:</p>
<pre><code> <persistenceAdapter>
<kahaDB directory="${activemq.data}/kahadb"/>
</persistenceAdapter>
</code></pre>
<p>修改为:</p>
<pre><code><persistenceAdapter>
<jdbcPersistenceAdapter dataSource="#mysql-ds" createTablesOnStartup="true"/>
</persistenceAdapter></code></pre>
<p><code>createTablesOnStartup</code> 默认值是true,每次ActiveMQ启动的时候都重新创建数据表,一般是首次启动设置为true,之后设置为false。</p>
</li>
<li>
<p>添加:在/conf/activemq.xml 中加入MySQL连接信息</p>
<pre><code><bean id="mysql-ds" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/db_activemq?relaxAutoCommit=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<property name="poolPreparedStatements" value="true"/>
</bean>
</code></pre>
</li>
<li>添加 mysql-connector-java.jar 到/bin目录</li>
<li>新建数据库db_activemq</li>
</ol>
<p>重启<code>ActiveMQ</code>后数据库产生三个表<code>activemq_acks</code>、<code>activemq_lock</code>、<code>activemq_msgs</code></p>
AOP的两种实现方式 and 五种增强/通知
https://segmentfault.com/a/1190000016693703
2018-10-15T22:41:58+08:00
2018-10-15T22:41:58+08:00
大树
https://segmentfault.com/u/mshu
2
<blockquote>大家都知道spring的特点IOC和AOP,IOC是最常用的注入,就是被注入的类上加@Component注解,在需要用到时候,通过 @Autowired注入,不用每次都<code>new</code>出来。当然为了分清层级,@Component通常使用@Repository、@Service、@Controller代替。</blockquote><p>本文只要记录AOP的用法,以springboot框架为例。</p><p><img src="/img/bVbKYKZ" alt="image.png" title="image.png"></p><p>AOP通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术,<br>在不修改主流程的情况下,可以进行权限校验,日志记录,性能监控等。<br>底层实现是JDC的动态代理和cglib动态代理。</p><p><strong>动态代理</strong><br>默认策略如果目标类是接口,则用 jDKProxy来实现,否则用cglib<br>JDKProxy:通过ava的内部反射机制实现<br>cgib:以继承的方式动态生成目标类的代理,借助ASM实现</p><p><strong>两种实现方法</strong>:<br>路径切入和注解切入,区别在于切点,前者适合批量切入,后者比较灵活,加注解的类才会被切。</p><p>1、通过路径切入<br>2、通过注解切入</p><h2>路径切入</h2><p>1、新建切面类上面加俩注解 @Aspect @Component 缺一不可<br>2、@Pointcut写上要切入的包,也可以精确到类<br>3、@Before切入点之前要处理的业务<br>4、@After切入点之后要处理的业务</p><pre><code>@Aspect
@Component
public class VisitAop {
@Pointcut("execution(public * com.forum.controller.*.*(..))")
public void log() {
}
@Before("log()")
public void doBefore(JoinPoint joinPoint) {
........
}
@After("log()")
public void doAfter() {
........
}
}
</code></pre><h2>注解切入</h2><p>1、自定义注解<br>2、切入类@Aspect @Component 缺一不可<br>3、@Pointcut写上要切入注解(意思是带此注解者,必切!)<br>4、@Before、@After同上。</p><p><strong>1、自定义注解</strong><br>1.1 @Target和@Retention定义自定义注解,无需其他,标识作用的注解。</p><pre><code>@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface VisitCount {
}
</code></pre><p><strong>3、切入点</strong><br>3.1和路径切入的区别在此</p><pre><code>@Pointcut("@annotation(com.Annotation.VisitCount)")
</code></pre><h2>JoinPoint</h2><p>此外可以了解一些doBefore(),的参数JoinPoint,以便操作业务;</p><p>1、joinPoint.getSignature().getDeclaringType().getSimpleName(),切入的类名<br>2、joinPoint.getArgs(),切入方法的参数数组<br>3、joinPoint.getSignature().getName(),切入方法名</p><h2>五种增强/通知</h2><p>上面的@Before和@After只是aop五种增强也叫通知的其中两种。执行顺序各有不同。</p><blockquote>前置通知(@Before)<br>后置通知(@Ater)<br>返回通知(@AfterReturning)<br>异常通知(@AfterThrowing)<br>环绕通知(@Around)</blockquote><p>异常通知和返回通知只会有一个出现,目标方法异常后会进入异常通知方法,不会进入返回通知方法。<br>正常执行会进入返回通知方法,不会进入异常通知方法。</p><pre><code> @Before("log()")
public void doBefore() {
System.out.println("前置通知-Before");
}
@After("log()")
public void doAfter() {
System.out.println("后置通知-After");
}
@AfterReturning("log()")
public void doAfterReturning() {
System.out.println("返回通知-AfterReturning");
}
@AfterThrowing("log()")
public void doAfterThrowing() {
System.out.println("异常通知-AfterThrowing");
}</code></pre><p>目标方法正常执行的情况下打印:</p><pre><code>前置通知-Before
后置通知-After
返回通知-AfterReturning</code></pre><p>目标方法异常执行的情况下打印:</p><pre><code class="log">前置通知-Before
后置通知-After
异常通知-AfterThrowing</code></pre><p>环绕通知比较特殊,ProceedingJoinPoint.proceed()方法表示继续执行目标方法。<br>环绕通知的返回值就是目标方法的返回值。如果没有返回值,目标方法也没有了。</p><pre><code> @Around("log()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) {
System.out.println("环绕通知开始-Around");
Object o = null;
try {
o = proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("环绕通知结束-Around");
return o;
}</code></pre><p>加入环绕通知后的执行顺序</p><pre><code>环绕通知开始-Around
前置通知-Before
环绕通知结束-Around
后置通知-After
返回通知-AfterReturning</code></pre><p>proceed()方法的有参方法甚至可以修改目标方法的参数,传入一个<code>Object数组</code></p><pre><code> Object[] var1 = new Object[]{"参数a"};
o = proceedingJoinPoint.proceed(var1);</code></pre><p>由此可见,环绕通知可以用于对业务逻辑判断后可以修改目标方法入参的场景。</p>
?️未雨绸缪.再也不怕手贱删数据了 MySql数据回滚事纪
https://segmentfault.com/a/1190000016063063
2018-08-19T01:46:46+08:00
2018-08-19T01:46:46+08:00
大树
https://segmentfault.com/u/mshu
4
<blockquote>案发当天,中午2点的天气很是炎热,知了拼命的鸣叫,仿佛要和天气叫板,公司的老空调还算没有拖后腿,整个工作区温度适宜,我在整理一批刚结束的爬虫数据,一切都是那么正常,毫无征兆。</blockquote>
<p>复制sql语句,修改表名、列名,run~</p>
<p><strong>次奥 !</strong>2.03s 删除了10万数据,数据虽然不多但是爬的辛苦啊,顿时感觉天气燥热,浑身是汗,呼吸困难,什么也不想干,就想骂人;</p>
<p>一直以为这种事情不会发生在我身上,对于数据回滚的知识也一直是片空白,现在终于明白了,哪个CTO年轻的时候没有删过几个库,没干过这种蠢事说明你还 So Young;</p>
<p>扯止。</p>
<hr>
<h2>使用事务安全操作</h2>
<blockquote>始于 BEGIN; 终于 ROLLBACK / COMMIT;</blockquote>
<ol>
<li>啥也别说运行一次BEGIN;</li>
<li>执行增/删/改sql语句,然后使用select查看是否操作正确;</li>
<li>如果正确运行一次COMMIT; 否则运行一次ROLLBACK;</li>
</ol>
<h3>举个栗子??️</h3>
<p>操作之前的数据:</p>
<pre><code>SELECT * from sys_user where id = 2 ;
结果:2 123456 mshu 123456 11855079819 2018-04-30 11:41:03 无不良记录 100 100</code></pre>
<p>ROLLBACK; 使用</p>
<pre><code>BEGIN;
DELETE FROM sys_user where id = 2;
SELECT * from sys_user where id = 2 ;
结果:
ROLLBACK;
SELECT * from sys_user where id = 2 ;
结果:2 123456 mshu 123456 11855079819 2018-04-30 11:41:03 无不良记录 100 100</code></pre>
<p>COMMIT; 使用</p>
<pre><code>BEGIN;
DELETE FROM sys_user where id = 2;
SELECT * from sys_user where id = 2 ;
结果:
COMMIT;
SELECT * from sys_user where id = 2 ;
结果:
</code></pre>
<blockquote>人都是懒惰的,每次操作sql,都要运行BEGIN;ROLLBACK / COMMIT实在太麻烦,当遇到概率事件时,人们往往会把有利于自己的一边的概率放大。 ——Mshu</blockquote>
<p>Mysql是有日志记录功能的,但是默认关闭,就像著名哲学家Mshu说的那样,Mysql也认为误操作是个概率事件,如果记录操作日志的话,时间久、频率高就会需要很大的空间,而且用不到的话就是一堆垃圾,所以它放小了不利自己的概率,认为没有必要开启。</p>
<p><em>我猜我在这里鬼扯让读者感到很无聊,但是我真的觉得在这里扯很舒服,所以我认为这篇文章被人读到的概率也很小。</em></p>
<p>为了保证文章顺利发表,言归正传</p>
<h2>开启logbin</h2>
<p>使用MySQL的日志记录功能,首先要确定是否开启了logbin,mysq命令:</p>
<pre><code>show variables like '%log_bin%' </code></pre>
<p>结果:</p>
<table>
<thead><tr>
<th>Variable</th>
<th align="left">value</th>
</tr></thead>
<tbody>
<tr>
<td>log_bin</td>
<td align="left">ON</td>
</tr>
<tr>
<td>sql_log_bin</td>
<td align="left">ON</td>
</tr>
<tr>
<td>log_bin_basename</td>
<td align="left">C:ProgramDataMySQLMySQL Server 5.7Datamysql-log</td>
</tr>
<tr>
<td>log_bin_index</td>
<td align="left">C:ProgramDataMySQLMySQL Server 5.7Datamysql-log.index</td>
</tr>
</tbody>
</table>
<p>很欣慰的看到我已经开启了logbin?;</p>
<p><strong>怎么开启?</strong></p>
<p>如果没有开启需要修改my.ini文件,至于这个文件在哪,Linux当然使用whereis,<br>Window的话,还是MySQL命令:</p>
<pre><code>show variables like '%data%' </code></pre>
<p>结果:</p>
<table>
<thead><tr>
<th>Variable</th>
<th align="left">value</th>
</tr></thead>
<tbody><tr>
<td>datadir</td>
<td align="left">C:ProgramDataMySQLMySQL Server 5.7Data\</td>
</tr></tbody>
</table>
<p>就在这个目录的父目录下。</p>
<p><strong>怎么修改?</strong></p>
<p>加菊花,呸,加句话:log-bin=mysql-log</p>
<pre><code># Binary Logging.
# log-bin
log-bin=mysql-log</code></pre>
<p>等号后面随便取,这将会是你的日志文件名,如我所取mysql-log</p>
<p><strong>重启服务器</strong></p>
<p>重启之后操作一番增删改后,可以看到日志文件,目录就在<code>show variables like '%data%'</code>显示的 datadir指向的文件夹里</p>
<pre><code>文件名为你刚刚设置的名字加00000加数字的二进制文件
如:mysql-log.000001</code></pre>
<p>这就是MySQL的日志文件。</p>
<p><strong>针对日志文件show一波命令</strong></p>
<table><tbody>
<tr>
<td>flush logs</td>
<td align="left">至此生成新的日志文件</td>
</tr>
<tr>
<td>reset master</td>
<td align="left">清空日志文件</td>
</tr>
</tbody></table>
<p>听说在此之前需要保证两个设置是对的</p>
<ol>
<li>存储引擎:InnoDB</li>
<li>日志格式:ROW</li>
</ol>
<p>由于我的MySQL默认都是对的,我就没有测试,就简单介绍一下吧:</p>
<p>查看存储引擎</p>
<pre><code>show engines;</code></pre>
<p>查看日志格式</p>
<pre><code>show variables like '%binlog_format%';
</code></pre>
<h2>mysqlog</h2>
<p><strong>怎么阅读日志文件?</strong></p>
<blockquote>下面开始画重点,这也是我写这篇文章的重要目的<br>既然日志文件是二进制,怎么使用呢,感兴趣的小伙伴可以了解一下<strong><code>mysqlbinlog</code>命令</strong>,这里不做重点;<br>重点是我写的一个小工具,使用简单,操作流畅,令人心旷神怡。</blockquote>
<p><strong>功能介绍:</strong>根据MySQL二进制日志文件,生成人类可以理解的操作记录文件,以excel人性化呈现;<br>而你只需要提供二进制文件以及数据库ip、用户名、和密码即可<br>项目使用python3开发,所以你电脑要安装python;</p>
<p><strong>项目目录:</strong></p>
<p><img src="/img/bVbfyRB?w=865&h=636" alt="clipboard.png" title="clipboard.png"></p>
<p><strong>使用步骤:</strong></p>
<ol>
<li>/log0000/文件夹内放你要解析的二进制日志文件</li>
<li>在main.py文件中填入config配置(数据库ip、用户名、和密码)</li>
<li>运行主方法,在/result/文件下可得到解析结果。</li>
</ol>
<p><strong>运行结果:</strong></p>
<p>包含时间、增删改的类型且用不同颜色区分明显,数据库,表,需要回滚的sql语句,以及当时执行的语句;一目了然。</p>
<p><img src="/img/bVbfyRM?w=1041&h=218" alt="clipboard.png" title="clipboard.png"></p>
<p>本工具主要是用来解析mysql的操作日志,除了生成当时执行的sql语句外,还有为了数据回滚的sql语句,都是标准的sql,可以直接复制运行。给使用者来分析和回滚,我把它命名为<strong><a href="https://link.segmentfault.com/?enc=9zb5b45f3WL4F1pTndvYNw%3D%3D.LKJAorMhXRnRVTjTmy7dYttJBMm2jY1AMmcjVznKg8yidI7XgV190lcGSAkV0zz2" rel="nofollow">mysqlog</a></strong>,<---链接为GitHub地址。</p>
<p>数据回滚很重要,误删数据别哭闹。<br>MysqLog福带到,让你眉开又眼笑。<br>好言相劝若不理,删库跑路就是你。</p>
<h3>免责声明</h3>
<p>本项目作者放荡不羁,不能保证项目准确无误,数据无价,对于生成的sql,请适当检查后使用。</p>
python爬虫之初恋 selenium
https://segmentfault.com/a/1190000014738930
2018-05-05T17:10:05+08:00
2018-05-05T17:10:05+08:00
大树
https://segmentfault.com/u/mshu
5
<p>selenium 是一个web应用测试工具,能够真正的模拟人去操作浏览器。<br>用她来爬数据比较直观,灵活,和传统的爬虫不同的是,<br>她真的是打开浏览器,输入表单,点击按钮,模拟登陆,获得数据,样样行。完全不用考虑异步请求,所见即所得。</p>
<p>selenium语言方面支持java/python,浏览器方面支持各大主流浏览器谷歌,火狐,ie等。我选用的是python3.6+chrome组合</p>
<hr>
<h2>chrome</h2>
<p>写python爬虫程序之前,需要准备两样东西:</p>
<pre><code>1.[chrome][1]/浏览器 https://www.google.cn/chrome/
2.[chromedriver][2] /浏览器驱动 http://chromedriver.storage.googleapis.com/index.html
</code></pre>
<p>浏览器和浏览器驱动的搭配版本要求比较严格,不同的浏览器版本需要不同的驱动版本;我的版本信息:</p>
<pre><code> chrome info: chrome=66.0.3359.139
Driver info: chromedriver=2.37.544315
</code></pre>
<p><strong>其他版本对照,浏览器驱动地址可直接根据浏览器版本下载对应驱动</strong></p>
<table>
<tr>
<td>chromedriver版本</td>
<td>Chrome版本</td>
</tr>
<tr>
<td>v2.37</td>
<td>v64-66</td>
</tr>
<tr>
<td>v2.36</td>
<td>v63-65</td>
</tr>
<tr>
<td>v2.34</td>
<td>v61-63</td>
</tr>
</table>
<p><strong>chrome浏览器</strong><br>这里需要注意的是如果想更换对应的谷歌浏览器,要高版本的请务必直接升级处理,低版本的卸载时要彻底!彻底!彻底!卸载,包括(Google升级程序,注册表,残留文件等),再安装。否则爬虫程序启动不了浏览器。</p>
<p><strong>chromedriver浏览器驱动</strong><br>chromedriver 放置的位置也很重要,<strong>把chromedriver放在等会要写的.py文件旁边</strong>是最方便的方法。当然也可以不放这里,但是需要配置chromedriver的路径,我这里就不介绍这种方法了。</p>
<p><strong>火狐驱动下载地址</strong>:<a href="https://link.segmentfault.com/?enc=07OkfSVEr8yXiUQbjeoIxw%3D%3D.WaLFD4B6ua6%2BKi7XQteMuCryx26ncUq42rdBpxomAgNaGXhy8xvd5j1mj%2B3BSTlH" rel="nofollow">https://github.com/mozilla/ge...</a></p>
<h2>python</h2>
<p>终于开始敲代码了</p>
<h2>打开网站</h2>
<pre><code>from selenium import webdriver
browser = webdriver.Chrome()
browser.get("https://segmentfault.com/")
</code></pre>
<p>三行代码即可自动完成启动谷歌浏览器,输出url,回车的骚操作。<br>此时的窗口地址栏下方会出现【Chrome 正在受到自动测试软件的控制】字样。</p>
<p><img src="/img/bV90cw" alt="clipboard.png" title="clipboard.png"></p>
<h2>提交表单</h2>
<p>下面我们来尝试控制浏览器输入并搜索关键字找到我们这篇文章;<br>先打开segmentfault网站,F12查看搜索框元素</p>
<pre><code><input id="searchBox" name="q" type="text" placeholder="搜索问题或关键字" class="form-control" value="">
</code></pre>
<p>发现是一个id为searchBox的input标签,ok</p>
<pre><code>from selenium import webdriver
browser = webdriver.Chrome() #打开浏览器
browser.get("https://segmentfault.com/") #输入url
searchBox = browser.find_element_by_id("searchBox") #通过id获得表单元素
searchBox.send_keys("python爬虫之初恋 selenium") #向表单输入文字
searchBox.submit() #提交
</code></pre>
<p><img src="/img/bV90wF" alt="clipboard.png" title="clipboard.png"></p>
<p>find_element_by_id()方法:根据id获得该元素。<br>同样还有其他方法比如</p>
<table>
<tr>
<td>find_element_by_xpath()</td>
<td>通过路径选择元素</td>
</tr>
<tr>
<td>find_element_by_tag_name()</td>
<td>通过标签名获得元素</td>
</tr>
<tr>
<td>find_element_by_css_selector()</td>
<td>通过样式选择元素</td>
</tr>
<tr>
<td>find_element_by_class_name()</td>
<td>通过class获得元素</td>
</tr>
<tr>
<td>find_elements_by_class_name()</td>
<td>通过class获得元素们,element加s的返回的都是集合</td>
</tr>
</table>
<p>举个栗子:</p>
<pre><code>1.find_elements_by_css_selector("tr[bgcolor='#F2F2F2']>td")
获得 style为 bgcolor='#F2F2F2' 的tr的子元素td
2.find_element_by_xpath("/html/body/div[4]/div/div/div[2]/div[3]/div[1]/div[2]/div/h4/a")
获得此路径下的a元素。
find_element_by_xpath方法使用谷歌浏览器F12选择元素右键copy->copyXpath急速获得准确位置,非常好用,谁用谁知道
3.find_element_by_xpath("..")获得上级元素
4.find_element_by_xpath("following-sibling::")获同级下级元素
5.find_element_by_xpath("preceding-sibling::")获同级上级元素
</code></pre>
<h2>抓取数据</h2>
<p>获得元素后<strong>.text</strong>方法即可获得该元素的内容 <br>我们获得文章的简介试试:</p>
<pre><code>from selenium import webdriver
browser = webdriver.Chrome() #打开浏览器
browser.get("https://segmentfault.com/") #输入url
searchBox = browser.find_element_by_id("searchBox") #通过id获得表单元素
searchBox.send_keys("python爬虫之初恋 selenium") #向表单输入文字
searchBox.submit() #提交
text = browser.find_element_by_xpath("//*[@id='searchPage']/div[2]/div/div[1]/section/p[1]").text
print(text)
</code></pre>
<p><img src="/img/bV90xw" alt="clipboard.png" title="clipboard.png"></p>
<p>除了捕获元素还有其他操控浏览器的方法:</p>
<table>
<tr>
<td>refresh()</td>
<td>刷新</td>
</tr>
<tr></tr>
<tr>
<td>click()</td>
<td>点击</td>
</tr>
<tr>
<td>close()</td>
<td>关闭当前标签页
(如果只有一个标签页就关闭浏览器)
</td>
</tr>
<tr>
<td>quit()</td>
<td>关闭浏览器</td>
</tr>
<tr>
<td>title</td>
<td>获得当前页面的title</td>
</tr>
<tr>
<td>
window_handles
</td>
<td>
获得所有窗口标签页id集合
</td>
</tr>
<tr>
<td>
current_window_handle
</td>
<td>
获得当前窗口标签页id
</td>
</tr>
<tr>
<td>
switch_to.window(【标签页id】)
</td>
<td>
根据选项卡id切换标签页
</td>
</tr>
<tr>
<td>switch_to_frame("iframe的Id或name")</td>
<td>切换到iframe</td>
</tr>
<tr>
<td>
switch_to.default_content()
</td>
<td>
切回最外层DOM
</td>
</tr>
<tr>
<td>
execute_script('window.open("www.segmentfault.com")')
</td>
<td>
执行js脚本(打开新标签)
</td>
</tr>
<tr>
<td>maximize_window()</td>
<td>最大化</td>
</tr>
<tr>
<td>get_screenshot_as_file()</td>
<td>截图(图片保存路径+名称+后缀)</td>
</tr>
<tr>
<td>set_page_load_timeout(30)</td>
<td>设置加载时间</td>
</tr>
<tr>
<td>ActionChains(driver).move_to_element(ele).perform()</td>
<td>鼠标悬浮在ele元素上</td>
</tr>
<tr>
<td>value_of_css_property()</td>
<td>获得元素的样式(无论行内式还是内嵌式)</td>
</tr>
<tr>
<td>
execute_script("return arguments[0].curentSrc;",【video元素】)
</td>
<td>获得视频链接</td>
</tr>
<tr>
<td>
execute_script("return arguments[0].duration;",【video元素】)
</td>
<td>获得视频时长</td>
</tr>
<tr>
<td>
execute_script("return arguments[0].play();",【video元素】)
</td>
<td>播放视频</td>
</tr>
<tr>
<td>
execute_script("return arguments[0].pause();",【video元素】)
</td>
<td>暂停视频</td>
</tr>
</table>
<blockquote>启动前添加参数</blockquote>
<pre><code>chromeOptions = webdriver.ChromeOptions()
chromeOptions.add_argument("--proxy-server=http://101.236.23.202:8866") //代理
chromeOptions.add_argument("headless") //不启动浏览器模式
browser = webdriver.Chrome(chrome_options=chromeOptions)
</code></pre>
<blockquote>不加载图片启动</blockquote>
<pre><code>def openDriver_no_img():
options = webdriver.ChromeOptions()
prefs = {
'profile.default_content_setting_values': {
'images': 2
}
}
options.add_experimental_option('prefs', prefs)
browser = webdriver.Chrome(chrome_options=options)
return browser
</code></pre>
<h2>反爬虫应对手段</h2>
<p>验证码识别:<a href="https://segmentfault.com/a/1190000015489113">https://segmentfault.com/a/11...</a><br> IP代理:<a href="https://segmentfault.com/n/1330000015678406">https://segmentfault.com/n/13...</a></p>
简单才是美! SpringBoot+JPA
https://segmentfault.com/a/1190000014269284
2018-04-09T22:30:24+08:00
2018-04-09T22:30:24+08:00
大树
https://segmentfault.com/u/mshu
6
<p>SpringBoot 急速构建项目,真的是用了才知道,搭配JPA作为持久层,一简到底!<br>下面记录项目的搭建,后续会添加NOSQL redis,搜索引擎elasticSearch,等等,什么不过时就加什么。</p><p>开发工具idea、项目构建gradle、模板引擎thymeleaf</p><h2>项目构建</h2><p>1.【new】 -> 【product】 -> 选择Spring Initializr -> 【next】</p><p><img src="/img/bV70zB" alt="clipboard.png" title="clipboard.png"></p><p>2.填写Group,Artifact,Type ->【next】</p><p><img src="/img/bV70Cq" alt="clipboard.png" title="clipboard.png"></p><p>3.导包</p><ol start="3"><li>1.左边选择Web右边勾选Web<br>2.左边选择SQL右边勾选JPA<br>3.左边选择SQL右边勾选mysql<br>4.左边选择Template Engines右边勾选Thymeleaf<br>5.【next】->【finish】<br><img src="/img/bV70CR" alt="clipboard.png" title="clipboard.png"><br>好了<br>现在的项目结构</li></ol><p><img src="/img/bV70GU" alt="clipboard.png" title="clipboard.png"></p><p>BootjpaApplication 是项目的启动类<br>resources/templates/ 文件夹是放页面的<br>build.gradle 存放jar包坐标</p><h3>application.properties</h3><pre><code>spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.properties.hibernate.hbm2ddl.auto=update
</code></pre><h3>@RestController</h3><p>配置完成,写个controller试试看</p><pre><code>@RestController
public class HelloBootController {
@RequestMapping("helloBoot")
public String helloBoot(){
return "Hello Boot-JPA";
}
}</code></pre><p>在BootjpaApplication文件上启动</p><p><img src="/img/bV70Pb" alt="clipboard.png" title="clipboard.png"></p><p>@RestController注解,代替@Controller+@ResponseBody<br>那么返回页面就直接用@Controller就好了</p><h2>现在JPA登场</h2><hr><h3>entity</h3><p>注解和hibernate一样。</p><pre><code>@Entity
public class User {
private long id;
private String name;
private String passWord;
private String email;
@Id
@GeneratedValue
public long getId() {
return id;
}
。。。。。
}
</code></pre><p>现在,就是见证奇迹的时刻!</p><h3>dao</h3><p>dao层继承JpaRepository即可</p><pre><code>public interface UserRepository extends JpaRepository<User,Long> {
}</code></pre><p>什么!这就完了??对,低调</p><h3>controller</h3><p>controller层,service层跳过。</p><pre><code>@Controller
public class HelloBootController {
@Autowired
UserRepository userRepository;
@RequestMapping("/toHello")
public String toHello(ModelMap modelMap){
userRepository.save(new User("Mshu","123456","zhuiqiu95@foxmail.com"));
List<User> users = userRepository.findAll();
modelMap.put("users",users);
return "helloBoot"; //页面地址
}
}
</code></pre><h2>thymeleaf</h2><hr><p>至于页面,默认是在resources/templates/下的html,试图解析器已经配置默认配置好的。</p><pre><code>前缀:resources/templates/
后缀:html</code></pre><p>那我们就在resources/templates/下新建一个名为<code>helloBoot.html</code>的页面<br>注意<html xmlns:th="http://www.thymeleaf.org " lang="en">引入thymeleaf<br>用到了thymeleaf语法遍历。</p><pre><code><!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<table >
<tr th:each="user,userState : ${users}">
<td style="border: 1px solid seagreen" th:text="${user.name}"></td>
<td style="border: 1px solid seagreen" th:text="${user.passWord}"></td>
<td style="border: 1px solid seagreen" th:text="${user.email}"></td>
</tr>
</table>
</body>
</html></code></pre><p>启动,输入地址,回车!<br><img src="/img/bV719N" alt="clipboard.png" title="clipboard.png"></p><h2>application.properties</h2><p>我们需要将application.properties里面配置的数据注入到类中可以这么做,<br>以下是一段application.properties中的数据;</p><pre><code>#elasticsearch
cluster.name=elasticsearch
elasticsearch.ip=127.0.0.1
elasticsearch.port=9300</code></pre><p>在类中使用@Value注入,记住使用${}包裹:</p><pre><code>@Component
public class ELClient {
@Value("${cluster.name}")
private String clusterName;
@Value("${elasticsearch.ip}")
private String elacticSearchIp;
@Value("${elasticsearch.port}")
private Integer elacticSearchPort;
}
</code></pre><h2>精彩回顾</h2><p>刚刚dao层明明只写了一个接口没有写任何方法,怎么就能调用save(),findAll()呢,<br>对JPA默认了许多基础增删改查方法,直接调用即可。<br>怎么写除了默认给出的方法以外怎么写呢,</p><pre><code>public interface UserRepository extends JpaRepository<User,Long> {
User findByName(String name);
}</code></pre><p>调用的话直接</p><pre><code>User user = userRepository.findByName("Mshu");
</code></pre><p>那么怎么做的映射的,它怎么知道我的参数name对应表里的name,原来名字一样就可以映射,好像很有道理<br>没错就那么简单,这种写法太hibernate了。</p><h2>注意事项</h2><ol><li>如果发现浏览器访问controller时404 <br><a href="https://segmentfault.com/n/1330000018997213">https://segmentfault.com/n/13...</a></li><li>Srpingboot 打war包<br><a href="https://segmentfault.com/n/1330000020235503">https://segmentfault.com/n/13...</a></li></ol>
Spring整合Quartz调度器
https://segmentfault.com/a/1190000012926491
2018-01-22T00:17:39+08:00
2018-01-22T00:17:39+08:00
大树
https://segmentfault.com/u/mshu
2
<p>Quartz是一个任务调度框架,由Java语言开发,可以用来做一些定时发送,监听事件等工作。<br>例如:让一个程序每天晚上12点执行一次。或者每隔5秒执行一次。</p>
<p>jar: org.quartz-scheduler包下</p>
<p>Quartz完成调度需要3步</p>
<pre><code>JobDetail:告诉调度器要做什么。
Trigger:告诉调度器什么时候做。
Scheduler:准备妥了就从这里start
</code></pre>
<p>下面就是一个简单的spring 整合quartz的实例。</p>
<h2>简单实现</h2>
<p>先看JobDetail部分:写一个类实现Job接口,重写execute()方法,在该方法中写要执行的逻辑,(告诉调度器要做什么)</p>
<pre><code>public class UpdateProductJob implements Job{
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
try {
System.out.print("我来检查啦")
}catch (Exception e){
System.out.println(e);
}
}
}</code></pre>
<p>然后是配置xml文件。</p>
<pre><code><!--quartz-->
<bean id="jobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobClass" value="com.config.quartz.UpdateProductJob"></property>
</bean>
<!--执行时间表达方式一-->
<!--<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">-->
<!--<property name="jobDetail" ref="jobDetail"></property>-->
<!--<property name="startDelay" value="10000"></property>&lt;!&ndash;延迟10s&ndash;&gt;-->
<!--<property name="repeatInterval" value="5000"></property>-->
<!--</bean>-->
<!--执行时间表达方式二-->
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="jobDetail" />
<property name="cronExpression" value="0 59 23 ? * *" />
</bean>
<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="cronTrigger"/>
</list>
</property>
</bean></code></pre>
<p>从上面可以看到有两个Trigger分别是simpleTrigger和cronTrigger,这是两种表达执行时间的方式。</p>
<pre><code>simpleTrigger是比较简单的方法,
<property name="repeatInterval" value="5000"></property>表示间隔5秒执行一次
而cronTrigger可以表达一些比较复杂的时间格式
<property name="cronExpression" value="0 59 23 ? * *" />表示每天23点59分执行一次
具体的语法请搜索cron表达式,有些网站提供了自动生成cron表达式的功能
比如http://cron.qqe2.com/ or http://www.cronmaker.com/
</code></pre>
<h2>解决spring注入问题</h2>
<p>在spring框架中会经常用到IOC,那么在上面的execute()方法中不避免的也会用到注入,但是对于新手来说会遇到注入的接口会是null。<br>解决这个问题有两个方法:</p>
<p>1.使用ApplicationContex对象加载applicationContext.xml文件注入UserDaoI接口,但是这个方法不好的一点是每当用一个接口就要写一个,比较麻烦。</p>
<pre><code>ApplicationContext content =
new ClassPathXmlApplicationContext("classpath:META-INF/applicationContext.xml");
userDaoI = content.getBean(UserDaoI.class);
</code></pre>
<p>2.第二个方法比较好,先写一个类继承AdaptableJobFactory 抽象方法,不需要任何改动</p>
<pre><code>@Service("jobFactory")
public class JobFactory extends AdaptableJobFactory {
@Autowired
private AutowireCapableBeanFactory capableBeanFactory;
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
// 调用父类的方法
Object jobInstance = super.createJobInstance(bundle);
// 进行注入
capableBeanFactory.autowireBean(jobInstance);
return jobInstance;
}
}</code></pre>
<p>2.2然后将这个类添加到配置文件里。</p>
<pre><code> <bean id="jobFactory" class="com.config.quartz.JobFactory"></bean>
<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="jobFactory" ref="jobFactory"></property>
<property name="triggers">
<list>
<ref bean="cronTrigger"/>
</list>
</property>
</bean></code></pre>
<p>最后完整的配置信息是:</p>
<pre><code><!--quartz-->
<bean id="jobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobClass" value="com.config.quartz.UpdateProductJob"></property>
</bean>
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="jobDetail" />
<property name="cronExpression" value="0 59 23 ? * *" />
</bean>
<bean id="jobFactory" class="com.config.quartz.JobFactory"></bean>
<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="jobFactory" ref="jobFactory"></property>
<property name="triggers">
<list>
<ref bean="cronTrigger"/>
</list>
</property>
</bean>
</code></pre>
<p>JobDetail部分就可以这样写了</p>
<pre><code>@Service
public class UpdateProductJob implements Job{
@Autowired
ArticleService articleService;
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
try {
articleService.updateArticleType();
}catch (Exception e){
System.out.println(e);
}
}
}
</code></pre>
<h2>Quartz轮训方式</h2>
<p>Quartz轮训任务,需要配置一个参数,这个参数来控制Job任务是否并行,这个参数是concurrent。默认是true,如果concurrent设为true,到了指定的时间就如去执行,不管上一次有没有执行完。</p>
<pre><code><bean id="transmitTask"
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject">
<ref bean="transTaskBusiness" />
</property>
<property name="targetMethod">
<value>execute</value>
</property>
<property name="concurrent">
<value>false</value>
</property>
</bean>
</code></pre>
<h2>任务监听器</h2>
<p><code>TriggerListeners</code> and <code>JobListeners</code> 两个接口分别是触发器相关的监听接口和作业相关监听接口。<br>用来监听<code>监听点</code>之前,之中,之后需要处理的事情。<br>此外,也可以直接继承<code>JobListenerSupport</code> or <code>TriggerListenerSupport</code></p>
<p>并且在配置文件添加以下内容来生效监听器:</p>
<pre><code><!--全局任务监听器 -->
<bean id="scheduleJobListener"
class="com.mashu.services.task.manager.ScheduleJobListener">
<property name="name" value="scheduleJobListener"></property>
</bean></code></pre>
<p>除了可以配置全局的,也可以根据业务按个,按组配置监听器。</p>
<p>Quartz 2.3.0</p>
过滤器, 拦截器,监听器
https://segmentfault.com/a/1190000011448952
2017-10-04T23:41:32+08:00
2017-10-04T23:41:32+08:00
大树
https://segmentfault.com/u/mshu
4
<p>下面介绍过滤器和拦截器以及监听器的使用方法:<br>执行顺序 :监听器 > 过滤器 > 拦截器</p>
<h2>一.过滤器</h2>
<p>主要的用途是过滤字符编码、或者去除掉一些非法字符<br>过滤器需要写两部分,一是java类,二是web.xml配置</p>
<h3>1.java代码,写个类实现Filter接口(implements Filter)</h3>
<pre><code>public class MangerFilter implements Filter {
public static UserDaoI userDaoI;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
ApplicationContext content =
new ClassPathXmlApplicationContext("classpath:META-INF/applicationContext.xml");
userDaoI = content.getBean(UserDaoI.class);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest Request = (HttpServletRequest)servletRequest;
HttpServletResponse Response = (HttpServletResponse) servletResponse;
HttpSession session = Request.getSession();
String path = Request.getRequestURI();
String userName = (String)session.getAttribute("userName");
User user = userDaoI.getUserByName(userName);
session.setAttribute("managerPage",path);
if(path.indexOf("index")==-1 && path.indexOf("resources")==-1)
.........
}else{
filterChain.doFilter(servletRequest,servletResponse);
}
}
@Override
public void destroy() {
}
}
</code></pre>
<p>这里需要注意的是在spring中,实现Filter接口的类中不能使用@Autowired注入,需要使用init方法内手动加载配置文件的方法去调用。</p>
<h3>2.在web.xml如下</h3>
<pre><code> <filter>
<filter-name>login</filter-name>
<filter-class>org.mshu.util.LoginFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>login</filter-name>
<url-pattern>/login/*</url-pattern>
</filter-mapping>
</code></pre>
<p>解释:</p>
<pre><code> <filter-name>随便取
<filter-class>过滤器的路径
<url-pattern>>过滤的路径
</code></pre>
<h3>小菜:</h3>
<p>1.判断是否是Ajax请求</p>
<pre><code>String isAjax = request.getHeader("x-requested-with");</code></pre>
<p>2.另外一般出来编码问题的时候会直接在web.xml中加上这段:这个不需要再写java代码,因为它指向的代码是org.springframework.web.filter.CharacterEncodingFilter已经存在的。直接复制可用,无需改动。</p>
<pre><code> <!-- Spring字符集过滤器 -->
<filter>
<filter-name>SpringEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>SpringEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</code></pre>
<h2>二.拦截器</h2>
<p>类似面向切片的技术,拦截器要做的工作更多是安全方面,比如用户验证,判断是否登陆<br>日志记录,或者限制时间点访问。<br>拦截器也是要写两部分,一部分是spring-mvc.xml,另一部分是java类</p>
<h3>1.java代码部分,需要一个继承了HandlerInterceptorAdapter抽象类的方法</h3>
<pre><code>public class LoginIntercepter extends HandlerInterceptorAdapter{
/**
* 在业务处理器处理请求之前被调用
* 如果返回false
* 从当前的拦截器往回执行所有拦截器的afterCompletion(),再退出拦截器链
* 如果返回true
* 执行下一个拦截器,直到所有的拦截器都执行完毕
* 再执行被拦截的Controller
* 然后进入拦截器链,
* 从最后一个拦截器往回执行所有的postHandle()
* 接着再从最后一个拦截器往回执行所有的afterCompletion()
*/
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
String username = (String)request.getSession().getAttribute("adminName");
if(username == null){
// request.getRequestDispatcher("/WEB-INF/content/login/index.jsp").forward(request, response);
System.out.println("拦截来了");
return false;
}else{
return true;
}
}
/**
* 在业务处理器处理请求执行完成后,生成视图之前执行的动作
* 可在modelAndView中加入数据,比如当前时间
*/
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
}
/**
* 在DispatcherServlet完全处理完请求后被调用,可用于清理资源等
*
* 当有拦截器抛出异常时,会从当前拦截器往回执行所有的拦截器的afterCompletion()
*/
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
}
</code></pre>
<h3>2.spring-mvc.xml部分</h3>
<pre><code> <?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--配置拦截器, 多个拦截器,顺序执行 -->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**" />
<mvc:exclude-mapping path="/login/**" />
<mvc:exclude-mapping path="/resources/**" />
<bean class="interceptor.LoginIntercepter"></bean>
</mvc:interceptor>
<!-- 当设置多个拦截器时,先按顺序调用preHandle方法,
然后逆序调用每个拦截器的postHandle和afterCompletion方法 -->
</mvc:interceptors>
</code></pre>
<p><mvc:mapping>表示拦截的路径 /**为全路径<br><mvc:exclude-mapping>表示不拦截的路径。</p>
<h3>小菜:</h3>
<p>如果你发现<mvc:exclude-mapping>似乎不起作用,还是被拦截,并且页面出现了如下的错误</p>
<pre><code>Resource interpreted as Stylesheet but transferred with MIME type text/plain
Uncaught ReferenceError: $ is not defined</code></pre>
<p>那么请你把静态文件加入免拦截<mvc:exclude-mapping>的队伍中,现在看起来很合理,但是查bug的时候,,哎,鬼知道我经历的什么。</p>
<h2>三.监听器</h2>
<p>用于监听一些重要事件的发生,监听器对象可以在事情发生前、发生后可以做一些必要的处理,<br>系统启动时加载初始化信息<br>监听器的功能是在项目启动和销毁时候搞事情:<br>和上面一样两部分:</p>
<h3>1.监听器类</h3>
<pre><code>实现ServletContextListener接口即可
public class initListener implements ServletContextListener{
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
}
}</code></pre>
<h3>2.编写web.xml文件</h3>
<pre><code> <!-- 初始化监听器-->
<listener>
<listener-class>org.yaoyan.util.initListener</listener-class>
</listener>
</code></pre>
<p><listener-class>是将要写的监听器类的路径</p>
mysql 基本操作
https://segmentfault.com/a/1190000009841999
2017-06-19T13:52:37+08:00
2017-06-19T13:52:37+08:00
大树
https://segmentfault.com/u/mshu
1
<h2><strong>数据层(DML)</strong></h2><h3>增</h3><ol><li>insert into 表名 values(值1,值2,......);</li><li>insert into 表名(字段名1,字段名2,.....) values(值1,值2,......);</li><li>从一张表插入另一张表:insert into 目标表 SELECT * FROM 来源表;</li><li>从一张表插入另一张表指定字段:INTO目标表 (字段1, 字段2) SELECT 字段1, 字段2 FROM 来源表;</li></ol><h3>删</h3><ol><li><p>DELETE (删除数据表里记录的语句,可以恢复)</p><pre><code> 1.delete from 表名 where 条件; (删除指定的某条数据) 【常用】
2.delect from 表名 ; (删除全表数据)
例:1.DELETE FROM STU WHERE ID = 10;
2.DELETE STU ;
</code></pre></li><li>truncate (删除数据,不可恢复)<p>truncate table 表名;</p></li></ol><h3>查</h3><p>select * from 表名 where 条件<br> select 列名,列名 from 表名 where 条件。</p><p>where 筛选功能,用在表名后;多个要求用 and 或 or 连接;<br>(or满足一个即可显示,and两个都要满足)</p><p>between and :</p><pre><code>between 1 and 2 = 1<x and x>2
</code></pre><p>in:</p><pre><code>表名 = 数据1 or 表名 = 数据2
= 表名 in (数据1,数据2);
</code></pre><p>like:</p><pre><code>用在where后 用%表示模糊字
</code></pre><p>null</p><pre><code>判断一个字段是否为null 用is null 不是=null;
</code></pre><p>三种关联方式:<br> select * from 表一,表二,表三 where 一关连二 and 二关连三 ;<br> select * from 表一 join 表二 on 一关连二 join 表三 on 二关连三;<br> select * from 表一,表二,表三 ;容易出现迪卡尔效应(数据重复)。</p><h3>改</h3><pre><code>update的语法:(修改个别数据)
update 表名 set 列名 = '新数据' where 另一个列名=数据 ;
</code></pre><h2><strong>结构层(DML)</strong></h2><h3>增</h3><ol><li>添加一列<br> alter table 表名 add column 列名 数据类型(长度) comment "描述";</li><li>添加一列并赋默认值<br> alter table 表名 add column 列名 数据类型(长度) DEFAULT "默认值" comment "描述";</li><li>在某列之后添加一列<br>alter table 表名 add column 列名 数据类型(长度) comment "描述" after 某列;</li><li>添加一列并创建索引<br> alter table 表名 add column 列名 数据类型(长度) comment "描述" , ADD INDEX 索引名称 列名;</li></ol><h3>删</h3><ol><li>删除一列<br> alter table 表名 drop column 列名;</li><li>删除表结构,不可恢复<br> drop table 表名</li></ol><h3>查</h3><p>查看表的结构 :</p><pre><code>desc 表名;
</code></pre><h3>改</h3><p>修改表结构:</p><pre><code>alter table 表名 modify column 列名 类型(长度) comment "描述";
</code></pre><hr><p>笔记:</p><ol><li><a href="https://segmentfault.com/n/1330000013790480">sql优化</a></li><li><a href="https://segmentfault.com/n/1330000015190630">触发器</a></li><li><a href="https://segmentfault.com/n/1330000023777349">存储过程</a></li></ol>
mysql一对多查询合并多的一方的数据。
https://segmentfault.com/a/1190000009705894
2017-06-08T15:26:38+08:00
2017-06-08T15:26:38+08:00
大树
https://segmentfault.com/u/mshu
6
<p>有时候会有这样一个需求,<br> 查询的一条记录需要包含另一个表的多条记录,并且让多条记录成为一个字段组成最终的一条记录。比较难描述,看例子吧。</p>
<p><strong>创建一个产品表:</strong></p>
<pre><code> create table product(
proId int(10),
proName varchar(50)
)</code></pre>
<p><strong>创建一个成分表:</strong></p>
<pre><code> create table componen(
comId int (10),
proId int(10),
comName varchar(50)
)</code></pre>
<p>案例需求:如果一个产品有多个成分,也就是一个产品表对应多个成分表,我想查出的结果,一条记录包含产品 proId, ProName, ComName,的字段。</p>
<p><strong>思路:</strong></p>
<ol>
<li>先写出不含成分表的查询语句,</li>
<li>然后将一个产品对应的多个成分合并成一个字段,</li>
<li>将合成的字段插入到一个语句中。</li>
</ol>
<p><strong>实践:</strong></p>
<pre><code> 1. select p.proId , p.proName from product p;
2. SELECT group_concat( c.comName ) FROM componen WHERE componen.proId= 1
3. SELECT
p.proId AS "产品id",
p.proName AS "产品名称",
(SELECT group_concat( c.comName ) FROM componen WHERE componen.proId
= p.proId)AS "成分"
FROM
product p;
</code></pre>
<p>*注意:第2步骤的语句和第三部引用第二部的语句有差别,那部分很重要的!<br>*如果要对合并的一方去重:嵌套DISTINCT即可</p>
<pre><code>(SELECT group_concat( DISTINCT(c.comName) ) FROM componen WHERE componen.proId= p.proId)
</code></pre>
post和get的区别,面试经常被问到!(二)
https://segmentfault.com/a/1190000009512784
2017-05-22T17:45:57+08:00
2017-05-22T17:45:57+08:00
大树
https://segmentfault.com/u/mshu
8
<blockquote>了解历史</blockquote>
<pre><code>get和post是HTTP与服务器交互的方式,
说到方式,其实总共有四种: post、delete、put、get。
他们的作用分别是对服务器资源的增、删、改、查。
所以,get是获取数据,post是修改数据。
但是,现在大家都不这么干了!只用一个方式就可以做增删查减的操作。</code></pre>
<hr>
<blockquote>区别分析</blockquote>
<ol>
<li>
<p><strong>get</strong>把请求的数据放在url上,即HTTP协议头上,其格式为:</p>
<pre><code> 以?分割URL和传输数据,参数之间以&相连。
数据如果是英文字母/数字,原样发送,
如果是空格,转换为+,
如果是中文/其他字符,则直接把字符串用BASE64加密,及“%”加上“字符串的16进制ASCII码”。</code></pre>
</li>
<li>
<strong>post</strong>把数据放在HTTP的包体内(requrest body)。</li>
<li>
<strong>get</strong>提交的数据最大是2k(原则上url长度无限制,那么get提交的数据也没有限制咯?限制实际上取决于浏览器,(大多数)浏览器通常都会限制url长度在2K个字节,即使(大多数)服务器最多处理64K大小的url。也没有卵用。)。</li>
<li>
<strong>post</strong>理论上没有限制。实际上IIS4中最大量为80KB,IIS5中为100KB。</li>
<li>
<strong>GET</strong>产生一个TCP数据包,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);</li>
<li>
<strong>POST</strong>产生两个TCP数据包,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。</li>
<li>
<strong>GET</strong>在浏览器回退时是无害的,<strong>POST</strong>会再次提交请求。</li>
<li>
<strong>GET</strong>产生的URL地址可以被Bookmark,而<strong>POST</strong>不可以。</li>
<li>
<strong>GET</strong>请求会被浏览器主动cache,而<strong>POST</strong>不会,除非手动设置。</li>
<li>
<strong>GET</strong>请求只能进行url编码,而<strong>POST</strong>支持多种编码方式。</li>
<li>
<strong>GET</strong>请求参数会被完整保留在浏览器历史记录里,而<strong>POST</strong>中的参数不会被保留。</li>
<li>
<strong>GET</strong>只接受ASCII字符的参数的数据类型,而<strong>POST</strong>没有限制</li>
</ol>
<p>那么,post那么好为什么还用get ?<strong>get</strong>效率高!</p>
<blockquote>补充:除了上面4种还有另外4种:</blockquote>
<ol>
<li>
<strong>HEAD</strong> :类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头</li>
<li>
<strong>TRACE</strong>: 回显服务器收到的请求,主要用于测试或诊断</li>
<li>
<strong>OPTIONS</strong>: 允许客户端查看服务器的性能</li>
<li>
<strong>CONNECT</strong>:HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。</li>
</ol>
<blockquote>纠正一下:</blockquote>
<ol>
<li>不是所有的POST的都发送俩个TCP包,火狐浏览器就一个</li>
<li>get将参数接在<code>URL</code>后面,post放在<code>body</code>只是语法规范。get也可以将参数放在<code>body</code>里面,post接在<code>URL</code>后面</li>
</ol>