开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Java开发技术大杂烩(三)之电商项目优化、rabbitmq

发表于 2018-06-18

前言

最近公司让我维护Spring+Servlet+Hibernate+Spring Security+Jsp的老项目,正好可以锻炼我的业务逻辑和掌控项目的能力。虽然项目很老,但是其中还是有很多值我学习的地方。

电商项目优化

1.我们大致优化的点是秒杀接口:redis预减库存,减少数据库访问;内存标记较少redis的访问;rabbitmq队列缓冲,异步下单,增强用户体验。那么具体步骤如下。

1.处理秒杀业务的Controller在Spring容器周期内加载就绪。也就是实现InitializingBean,在afterPropertiesSet()方法中把商品库存加载到redis中,并且设置在内存中设置商品是否秒杀结束的flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码    /**
* 内存标记初始化
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
List<GoodsVo> goodsVoList = goodsService.listGoodsVo();

if (CollectionUtils.isEmpty(goodsVoList)) {
return;
}

goodsVoList.forEach(goodsVo -> {
redisService.set(GoodsKey.getMiaoshaGoodsStock, "" + goodsVo.getId(), goodsVo.getStockCount());
localOverMap.put(goodsVo.getId(), false);
});
}

2.后台收到秒杀请求,首先查看内存flag标记,然后减少redis中的商品库存。如果商品秒杀结束,在内存中设置秒杀结束的flag。如果商品秒杀还在进行中,那么进入下一步。

3.把秒杀商品的消息进行入队缓冲,直接返回。这里并不是返回成功,而是返回到排队中。此时,前台不能直接提示秒杀成功,而是启动定时器,过一段时间再去查看是否成功。

4.消息出队,修改db中的库存,创建秒杀订单。

2.分布式Session的解决方案是生成唯一token,token标识用户,把token写到Cookie中,然后把token+用户信息写进Redis,token在redis的失效时间要和Cookie失效时间保持一致。每当用户登录一次,要延迟Session的有效期和Cookie有效期。

3.从缓存的角度来说,我们可以进行页面缓存+URL缓存+对象缓存来达到优化的目的。我们可以手动渲染Thymeleaf模板,把商品详情页和商品列表页缓存到redis中,这里用商品列表页举例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码    @RequestMapping(value = "/to_list", produces = "text/html;charset=UTF-8")
@ResponseBody
public String list(MiaoshaUser miaoshaUser) throws IOException {
modelMap.addAttribute("user", miaoshaUser);
//取缓存
String htmlCached = redisService.get(GoodsKey.getGoodsList, "", String.class);
if (!StringUtils.isEmpty(htmlCached)) {
return htmlCached;
}
List<GoodsVo> goodsVoList = goodsService.listGoodsVo();
modelMap.addAttribute("goodsList", goodsVoList);
SpringWebContext springWebContext = new SpringWebContext(request, response, request.getServletContext(),
request.getLocale(), modelMap, applicationContext);
String html = thymeleafViewResolver.getTemplateEngine().process("goods_list", springWebContext);

if (!StringUtils.isEmpty(html)) {
redisService.set(GoodsKey.getGoodsList, "", html);
}
return html;
}

4.从静态资源角度考虑,我们进行页面静态化、前后端分离、静态资源优化、CDN节点优化。这里用静态资源优化举例。

1.JS/CSS压缩、减少流量。
2.多个JS/CSS组合,减少连接数
3.CDN就近访问,减少请求时间。
4.将一些界面缓存到用户的浏览器中。

5.安全优化。密码两次加盐,第一次加盐是固定的,写在Java代码的。第二次加盐是随机的,存储在数据库中。在商品秒杀页,添加数学公式验证码,分散用户的请求。对接口加入限流防刷机制。这里以接口限流防刷机制举例。

1.定义AccessLimit注解,作用于方法。

1
2
3
4
5
6
7
8
9
10
11
复制代码@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {

int seconds();

int maxCount();

boolean needLogin() default true;
}

2.定义AccessInterceptor拦截器,获得方法中AccessLimit注解中的参数。请求的reqeusturi作为redis中的key,seconds作为key的失效时间。每次请求加1,如果在指定时间内访问该url的次数超过设置的maxCount,那么返回“访问太频繁”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
复制代码    @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
MiaoshaUser user = getUser(request, response);
UserContext.setUser(user);
HandlerMethod hm = (HandlerMethod) handler;
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);

if (Objects.isNull(accessLimit)) {
return true;
}
int seconds = accessLimit.seconds();
int maxCount = accessLimit.maxCount();
boolean needLogin = accessLimit.needLogin();
String key = request.getRequestURI();

if (needLogin) {
if (Objects.isNull(user)) {
render(response, CodeMsg.SESSION_ERROR);
return false;
}
}

AccessKey ak = AccessKey.withExpire(seconds);
Integer count = redisService.get(ak, key, Integer.class);

if (Objects.isNull(count)) {
redisService.set(ak, key, 1);
} else if (count < maxCount) {
redisService.incr(ak, key);
} else {
render(response, CodeMsg.ACCESS_LIMIT_REACHED);
return false;
}
}
return true;
}

6.部署优化。LVS+Keepalived双机热备模式+Nginx+Tomcat。


Intelli J IDEA使用技巧

1.全局搜索 Ctrl + Shift + F
2.全局替换 Ctrl +Shift + R


Vim编辑器使用技巧

1.在vim编辑器中进行查找。

1.命令模式输入“/字符串”,例如”/xiaoma”
2.如果继续查找下一个,按n即可。


Redis设置密码

1.因为在application-dev.properties中配置了spring.redis.password=,如果没有在redis.conf没有设置requirepass ${password},控制台会抛出连接拒绝异常。

HTTP

Cache Control的用法

no cache : 强制每次请求直接发送给源服务器,而不用经过本地缓存版本的校验。
max-age > 0 : 直接从浏览器缓存中提取。


RabbitMQ

1.AMQP(Advance Message Queuing Protocol)是一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议。

2.Exchange在RabbitMQ中充当交换机的角色,也相当于路由。当然也可以形象的理解成RabbitMQ的过滤器。RabbitMQ有4种模式。

rabbitmq图解-本图来自于互联网

1.Direct:按照Routing Key分到指定的Queue中。
2.Topic:和Direct差不多,但是可以多关键字匹配。
3.Fanout:无Routing Key概念,相当于广播模式,将消息分发给所有绑定FanoutExchange中的Queue。
4.Header:和上面3个不一样,通过添加属性key-value进行匹配。

3.编写RabbitMQ代码

配置RabbitMQ的4种模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
复制代码/**
* @author cmazxiaoma
* @version V1.0
* @Description: TODO
* @date 2018/6/4 11:36
*/
@Configuration
public class MQConfig {

public static final String MIAOSHA_QUEUE = "miaosha.queue";
public static final String QUEUE = "queue";
public static final String TOPIC_QUEUE1 = "topic.queue1";
public static final String TOPIC_QUEUE2 = "topic.queue2";
public static final String HEADER_QUEUE = "header.queue";
public static final String TOPIC_EXCHANGE = "topicExchange";
public static final String FANOUT_EXCHANGE = "fanoutExchange";
public static final String HEADERS_EXCHANGE = "headersExchange";

/**
* Direct模式
* @return
*/
@Bean
public Queue queue() {
return new Queue(QUEUE, true);
}

@Bean
public Queue miaoshaoQue() {
return new Queue(MQConfig.MIAOSHA_QUEUE, true);
}

/**
* Topic模式
* @return
*/
@Bean
public TopicExchange topicExchange() {
return new TopicExchange(TOPIC_EXCHANGE);
}

@Bean
public Queue topicQueue1() {
return new Queue(TOPIC_QUEUE1, true);
}

@Bean
public Queue topicQueue2() {
return new Queue(TOPIC_QUEUE2, true);
}

@Bean
public Binding topicBinding1() {
return BindingBuilder
.bind(topicQueue1())
.to(topicExchange())
.with("topic.key1");
}

@Bean
public Binding topicBinding2() {
return BindingBuilder
.bind(topicQueue2())
.to(topicExchange())
.with("topic.#");
}

/**
* Fanout模式
* @return
*/
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange(FANOUT_EXCHANGE);
}

@Bean
public Binding fanoutBinding1() {
return BindingBuilder.bind(topicQueue1())
.to(fanoutExchange());
}

@Bean
public Binding fanoutBinding2() {
return BindingBuilder.bind(topicQueue2())
.to(fanoutExchange());
}

/**
* Header模式
* @return
*/
@Bean
public HeadersExchange headersExchange() {
return new HeadersExchange(HEADERS_EXCHANGE);
}

@Bean
public Queue headerQueue1() {
return new Queue(HEADER_QUEUE, true);
}

@Bean
public Binding headerBinding() {
Map<String, Object> map = new HashMap<>();
map.put("header1", "value1");
map.put("header2", "value2");
return BindingBuilder.bind(headerQueue1()).to(headersExchange())
.whereAll(map).match();
}
}

配置消息生产者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
复制代码/**
* @author cmazxiaoma
* @version V1.0
* @Description: TODO
* @date 2018/6/4 13:05
*/
@Service
@Slf4j
public class MQSender {

@Autowired
private AmqpTemplate amqpTemplate;

public void sendMiaoshaMessageDirect(MiaoshaMessage miaoshaMessage) {
String msg = RedisService.beanToString(miaoshaMessage);
log.info("send direct message = {}", msg);
amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);
}

public void sendDirect(Object message) {
String msg = RedisService.beanToString(message);
log.info("send direct message = {}", msg);
amqpTemplate.convertAndSend(MQConfig.QUEUE, msg);
}

public void sendTopic(Object message) {
String msg = RedisService.beanToString(message);
log.info("send topic message = {}", msg);
amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE, "topic.key1", msg + "-1");
amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE, "topic.key2", msg + "-2");
}

public void sendFanout(Object message) {
String msg = RedisService.beanToString(message);
log.info("send fanout message = {}", msg);
amqpTemplate.convertAndSend(MQConfig.FANOUT_EXCHANGE, "", msg);
}

public void sendHeader(Object message) {
String msg = RedisService.beanToString(message);
log.info("send header message = {}", msg);
MessageProperties messageProperties = new MessageProperties();
messageProperties.setHeader("header1", "value1");
messageProperties.setHeader("header2", "value2");
Message newMessage = new Message(msg.getBytes(), messageProperties);
amqpTemplate.convertAndSend(MQConfig.HEADERS_EXCHANGE, "", newMessage);
}

}

配置消息消费者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
复制代码/**
* @author cmazxiaoma
* @version V1.0
* @Description: TODO
* @date 2018/6/4 13:47
*/
@Service
@Slf4j
public class MQReceiver {

@Autowired
private RedisService redisService;

@Autowired
private GoodsService goodsService;

@Autowired
private OrderService orderService;

@Autowired
private MiaoshaService miaoshaService;

@RabbitListener(queues = MQConfig.MIAOSHA_QUEUE)
public void receiveMiaoshaMessageDirect(String message) {
log.info("receive direct miaosha message = {}", message);
MiaoshaMessage miaoshaMessage = RedisService.stringToBean(message, MiaoshaMessage.class);
MiaoshaUser miaoshaUser = miaoshaMessage.getMiaoshaUser();
Long goodsId = miaoshaMessage.getGoodsId();
GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId);
int stock = goodsVo.getStockCount();

if (stock <= 0) {
return;
}
//判断是否已经秒杀过
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(miaoshaUser.getId(), goodsId);

if (!Objects.isNull(order)) {
return;
}
//减库存 下订单 写入秒杀订单
miaoshaService.miaosha(miaoshaUser, goodsVo);
}

@RabbitListener(queues = MQConfig.QUEUE)
public void receiveDirect(String message) {
log.info("receive direct message = {}", message);
}

@RabbitListener(queues = MQConfig.TOPIC_QUEUE1)
public void receiveTopic1(String message) {
log.info("receive topic queue1 message = {}", message);
}

@RabbitListener(queues = MQConfig.TOPIC_QUEUE2)
public void receiveTopic2(String message) {
log.info("receive topic queue2 message = {}", message);
}

@RabbitListener(queues = MQConfig.HEADER_QUEUE)
public void receiveHeader(byte[] message) {
log.info("receive header message = {}", new String(message));
}
}

测试RabbitMQ的Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
复制代码/**
* @author cmazxiaoma
* @version V1.0
* @Description: TODO
* @date 2018/5/29 16:36
*/
@Controller
@RequestMapping("/rabbitmq")
public class RabbitmqController extends BaseController {

@Autowired
private MQSender mqSender;

@GetMapping("/header")
@ResponseBody
public Result<String> header() {
mqSender.sendHeader("hello, header");
return Result.success("hello, header");
}

@GetMapping("/fanout")
@ResponseBody
public Result<String> fanout() {
mqSender.sendFanout("hello, fanout");
return Result.success("hello, fanout");
}

@GetMapping("/topic")
@ResponseBody
public Result<String> topic() {
mqSender.sendTopic("hello, topic");
return Result.success("hello, topic");
}

@GetMapping("/direct")
@ResponseBody
public Result<String> direct() {
mqSender.sendDirect("hello, direct");
return Result.success("hello, direct");
}
}

Nginx

Nginx的命令过一阵子不写,老是忘记。还是记在简书上面吧。
启动:/usr/local/nginx/sbin/nginx -C /usr/local/nginx/conf/nginx.conf
关闭:/usr/local/nginx/sbin/nginx -s stop

我们在nginx.conf配置max_fail和fail_timeout参数,当失败次数超过max_fail,nginx会把接下来的请求交给其他Real Server去处理。fail_timeout是失败等待时间,当请求被认定失败后,等待fail_timeout时间再去请求,判断是否成功。


Git容易混淆的知识点

工作区:包括实际更改的文件,当前修改还未add进入暂存区的文件变化信息。
暂存区:临时存储文件的变化信息

git reset filename:清空add命令向暂存区提交的关于filename文件的修改。
git checkout –filename:撤销对工作区的修改。


JS基础知识

众所周知,Java有三大特性:封装,继承,多态。我们可以用JS的protoType往java对象中注入这三大特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
复制代码<script type="text/javascript">
var myObject = {
foo: "bar",
func: function() {
var self = this;
console.log("outer func:this.foo=" + this.foo);
console.log("outer func:self.foo=" + self.foo);

(function() {
console.log("inner func:this.foo=" + this.foo);
console.log("inner func:self.foo=" + self.foo);
}());

}
};
myObject.func();

Java = function() {};
Java.prototype = {
oriented: function() {
console.log("面向对象");
},
fengzhuang: function() {
console.log("封装");
},
extend: function() {
console.log("继承");
}
};
java = new Java();
java.oriented();
java.fengzhuang();
java.extend();
</script>

Spring MVC冷门注解

1.produces=”text/html”表示方法将产生“text/html”格式的数据,并且响应条的ContentType。我们在写入消息返回响应前,调用addDefaultHeaders()设置响应条中ContentType和ContentLength属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
复制代码	protected void addDefaultHeaders(HttpHeaders headers, T t, MediaType contentType) throws IOException{
if (headers.getContentType() == null) {
MediaType contentTypeToUse = contentType;
if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
contentTypeToUse = getDefaultContentType(t);
}
else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
MediaType mediaType = getDefaultContentType(t);
contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);
}
if (contentTypeToUse != null) {
if (contentTypeToUse.getCharset() == null) {
Charset defaultCharset = getDefaultCharset();
if (defaultCharset != null) {
contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);
}
}
headers.setContentType(contentTypeToUse);
}
}
if (headers.getContentLength() < 0 && !headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) {
Long contentLength = getContentLength(t, headers.getContentType());
if (contentLength != null) {
headers.setContentLength(contentLength);
}
}
}

2.@ResponseBody该注解用于将Controller中方法的返回对象,根据HttpRequest中请求头中Accept的内容,再通过合适的HttpMessageConverter转换指定格式后,写入到response对象(HttpOutputMessage)的body数据区中。若指定方法中consume为“application/json”,那么方法仅处理请求头中ContentType属性值为”application/json”的请求。

image.png

3.判断某个方法是否有指定的注解、某个方法所在的类上是否有指定的注解、某个方法的参数上是否有指定的注解。

1
2
3
复制代码parameter.hasParameterAnnotation(RequestBody.class)
AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class)
returnType.hasMethodAnnotation(ResponseBody.class)

4.@ModelAttribute的妙用

1.运用在方法的参数上,会将客户端传递过来的参数按名称注入到指定对象中,并且会将这个对象自动加入到modelMap中,便于view层调用

2.运用在方法中,会在每一个@RequestMapping标注的方法前执行,如果有返回值,则自动将该返回值加入modelMap中。我一般用于封装BaseController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码public abstract class BaseController {

protected HttpServletRequest request;
protected HttpServletResponse response;
protected HttpSession session;
protected ModelMap modelMap;

@ModelAttribute
protected void initSpringMvc(HttpServletRequest request, HttpServletResponse response,
HttpSession session, ModelMap modelMap) {
this.request = request;
this.response = response;
this.session = session;
this.modelMap = modelMap;
}
}

5.定时任务,我们在WebApplication类注解@EnableScheduling,开启定时任务。cron表达式的参数从左到右分别是秒 、分、 时、 天、 月、 星期、 年。详细的cron表达式用法请看这个网站http://cron.qqe2.com/

1
2
3
4
5
6
7
8
9
复制代码@Component
public class TestTask {

@Scheduled(cron = "4-40 * * * * ?")
public void reportCurrentTime() {
System.out.println("现在时间:" + DateFormatUtils.format(new Date(),
"yyyy-MM-dd HH:mm:ss"));
}
}

6.开启异步任务,我们在WebApplication类注解@EnableAsync。

我们可以写一个AsyncTask任务类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
复制代码@Component
public class AsyncTask {

@Async
public Future<Boolean> doTask1() throws Exception {
long start = System.currentTimeMillis();
Thread.sleep(1000);
long end = System.currentTimeMillis();
System.out.println("任务1耗时:" + (end - start));
return new AsyncResult<>((true));
}

@Async
public Future<Boolean> doTask2() throws Exception {
long start = System.currentTimeMillis();
Thread.sleep(2000);
long end = System.currentTimeMillis();
System.out.println("任务2耗时:" + (end - start));
return new AsyncResult<>((true));
}

@Async
public Future<Boolean> doTask3() throws Exception {
long start = System.currentTimeMillis();
Thread.sleep(3000);
long end = System.currentTimeMillis();
System.out.println("任务3耗时:" + (end - start));
return new AsyncResult<>((true));
}
}

然后在写TaskController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
复制代码@RestController
@RequestMapping("/tasks")
public class TaskController extends BaseController {

@Autowired
private AsyncTask asyncTask;

@RequestMapping("test")
public Result test() throws Exception {
long start = System.currentTimeMillis();

Future<Boolean> a = asyncTask.doTask1();
Future<Boolean> b = asyncTask.doTask2();
Future<Boolean> c = asyncTask.doTask3();

while (!a.isDone() || !b.isDone() || !c.isDone()) {
if (a.isDone() && b.isDone() && c.isDone()) {
break;
}
}
long end = System.currentTimeMillis();
String times = "任务全部完成,总耗时:" + (end - start) + "毫秒";
return Result.success(times);
}
}

我们可以看到这3个任务总耗时是3000ms,证明任务是异步执行的。如果去掉@Async,这3个任务执行是同步的,总耗时应该是6000多ms。

1
复制代码{"code":0,"data":"任务全部完成,总耗时:3005毫秒","msg":""}

7.SpringBoot部署到外部Tomcat,配置pom文件。使tomcat作用域设置为provided,provided表明只在编译器和测试时候使用,因为我们部署到外部Tomcat,运行期间有外部Tomcat的支持。

1
2
3
4
5
6
7
8
9
复制代码		<!--spring boot tomcat
默认可以不用配置,但当需要把当前web应用布置到外部servlet容器时就需要配置,
并将scope配置为provided
当需要默认的jar启动,则去掉provided, provided表明只在编译期和测试的时候使用-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>

记得把打包的方式从jar改成war

1
2
3
4
复制代码	<groupId>com.cmazxiaoma</groupId>
<artifactId>seckillSystem</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>

重写SpringApplication这个启动类,我这里重新创建了一个类,名为WebApplication

1
2
3
4
5
6
7
8
9
10
11
12
复制代码@SpringBootApplication
//开启定时任务
@EnableScheduling
//开启异步调用方法
@EnableAsync
public class WebApplication extends SpringBootServletInitializer {

@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(WebApplication.class);
}
}

然后Build Artifacts即可。

image.png


OSI

OSI是开放式系统互联,英文是Open System Interconnection

应用层
表示层
会话层
传输层
网络层
数据链路层
物理层

TCP/IP模型

应用层 =》 HTTP(超文本传输协议)、TFTP(简单文件传输协议)、SMTP(简单邮件传输协议)、DNS(域名系统)、SNMP(简单网络管理协议)、NFS(网络文件系统)、Telnet(终端登录)
传输层 =》 TCP、IP
网络层 =》 IP、ICMP(国际控制报文协议)、ARP(地址解析协议)、RARP(反地址解析协议)
数据链路层 =》 PPP(点对点协议)


HttpMessageConverter所引发的异常

当我去请求/login/to_login会返回login视图,login界面会去加载背景图片。此时我们没有去配置资源映射,导致背景图片会请求后端的Controller。如果没有找到合适的Controller去处理这个请求,会进入全局异常捕获器进入异常处理。在RequestResponseBodyMethodProcessor中的writeWithMessageConverters()方法中,我们会调用getProducibleMediaTypes()方法获取该请求的所有返回消息格式类型。

1
2
3
4
5
6
7
复制代码		HttpServletRequest request = inputMessage.getServletRequest();
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);

if (outputValue != null && producibleMediaTypes.isEmpty()) {
throw new IllegalArgumentException("No converter found for return value of type: " + valueType);
}

由于我们没有在全局异常捕获器HandlerMapping中显式设置produces属性,我们只能通过遍历所有的HttpMessageConverter,通过canWrite()方法找到支持解析Java对象的HttpMessageConverter,并且把其所支持的mediaType加入mediaTypes集合里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码	protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, Type declaredType) {
Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(mediaTypes)) {
return new ArrayList<MediaType>(mediaTypes);
}
else if (!this.allSupportedMediaTypes.isEmpty()) {
List<MediaType> result = new ArrayList<MediaType>();
for (HttpMessageConverter<?> converter : this.messageConverters) {
if (converter instanceof GenericHttpMessageConverter && declaredType != null) {
if (((GenericHttpMessageConverter<?>) converter).canWrite(declaredType, valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
else if (converter.canWrite(valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
return result;
}
else {
return Collections.singletonList(MediaType.ALL);
}
}

我们得出producibleMediaTypes都是关于”application/json”的格式,我们for循环2次,将requestedMediaTypes和producibleMediaTypes一一比较,得出兼容的compatibleMediaTypes。如果请求消息格式和返回消息格式没有一个匹配的话,则抛出HttpMediaTypeNotAcceptableException异常。

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码		Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
for (MediaType requestedType : requestedMediaTypes) {
for (MediaType producibleType : producibleMediaTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
if (compatibleMediaTypes.isEmpty()) {
if (outputValue != null) {
throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
}
return;
}
解决办法

在application-dev.properties文件中配置静态资源自动映射

1
复制代码spring.resources.add-mappings=true

或者是手动配置资源映射

1
2
3
4
5
复制代码    @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
super.addResourceHandlers(registry);
}

Java基础知识

PreparedStatement对象有addBatch()、executeBatch()方法,用于批量插入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码        Connection conn = DBUtil.getConn();
String sql = "insert into miaosha_user(login_count, nickname, register_date, salt, password, id)values(?,?,?,?,?,?)";
PreparedStatement pstmt = conn.prepareStatement(sql);

for (int i = 0; i < users.size(); i++) {
MiaoshaUser user = users.get(i);
pstmt.setInt(1, user.getLoginCount());
pstmt.setString(2, user.getNickname());
pstmt.setTimestamp(3, new Timestamp(user.getRegisterDate().getTime()));
pstmt.setString(4, user.getSalt());
pstmt.setString(5, user.getPassword());
pstmt.setLong(6, user.getId());
pstmt.addBatch();
}
pstmt.executeBatch();
pstmt.close();
conn.close();

isAssignableFrom()的用法,判断Class1和Class2是否相同,判断Class1是否是Class2的接口或者是其父类。

1
复制代码Class1.isAssignableFrom(Class2)

instance of 容易和isAssignableFrom()混淆,这用cmazxiaoma instance of Object举例子,判断一个对象实例是否是一个类、接口的实例,或者是其父类、子接口的实例

###JSR303用法
JSR303是一个数据验证的规范,这里用手机号验证举例子

定义@IsMobile注解,这个注解要被IsMobileValidator类去实现验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class})
public @interface IsMobile {

boolean required() default true;

String message() default "手机号码格式错误";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

}

定义手机号验证类,验证没通过会抛出BindException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {

private boolean required = false;

@Override
public void initialize(IsMobile isMobile) {
required = isMobile.required();
}

@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
if (required) {
return ValidatorUtil.isMobile(value);
} else {
if (StringUtils.isEmpty(value)) {
return true;
} else {
return ValidatorUtil.isMobile(value);
}
}
}
}

验证没通过会抛出BindException,我们在全局异常捕获器中捕获这个异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
复制代码
@ControllerAdvice
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {

@ExceptionHandler(value = Exception.class)
public Result<String> exceptionHandler(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception e) {
log.error(e.getMessage());

if (e instanceof GlobalException) {
GlobalException ex = (GlobalException) e;
return Result.error(ex.getCm());
} else if (e instanceof BindException) {
BindException ex = (BindException) e;
List<ObjectError> errors = ex.getAllErrors();
ObjectError error = errors.get(0);
String msg = error.getDefaultMessage();
return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));
} else {
return Result.error(CodeMsg.SERVER_ERROR.fillArgs(e.getMessage()));
}
}
}

尾言

每次逛博客的时候,看到不懂的地方,一定要拿小本本记住。然后整理到简书上面,日积月累,量变引发质变。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用go搭建个人博客(一):初始化项目

发表于 2018-06-17

  最近学习了Go的一些基本知识之后,决定实践一下。因为个人博客是比较常见和比较简单的联系实例,所以就打算再精简一下需求,做一个比较简单的个人博客,来了解Go语言在Web服务上的基本使用。

考虑需要实现的功能

  一个简化版的个人博客,满足基本需求即可。

  • 存在一个可以用来管理md文件的工具,该工具能将本地的md文件,也就是写好的博客,能上传到服务器上,或者进行根据文章标题进行文章文件的删除等。该工具就模仿hexo博客系统做一个命令行工具吧,简单也直接。
  • Web前端页面:1. 存在一个主页用来显示所有的文章,这里就以一个列表的形式存在。 2. 列表每项,有文章的基本信息,例如:标题、上传时间、文章字数等。3. 文章内容页。 4. 支持进行分类和标签检索相应文章。
  • Web服务端: 1. 提供查询所有文章、根据分类ID查询对于文章列表、根据标签ID查询对于文章列表等API。 2. 提供将md文件转成HTML字符串的功能,最后一接口形式暴露。 3. 接收文件,并分析md文件,将相应的信息存入数据库。

大概整个系统由上面三个部分组成。

采用前前后端分离的模式

  • 后端 :
    • 对于Go语言的Web框架有很多,哪怕是使用内置的net/http也能比较方便的书写Web服务,所以这里选择比较轻量、比较简单的Web框架gin;
    • 因为数据库的业务很少,不需要存储大量的相关信息,只需要存储一个文章的标签、标题、分类和时间等,所以就直接使用轻量级的Sqlite数据库,直接以db文件形式存放;
    • 数据库的ORM工具,国内有比较火的xorm,就直接拿来使用了,文档算是很全面的,实在不行就直接写sql也是可以的。
  • 前端:
+ 如今前端比较流行构建单页面应用,本人目前也是一个前端程序员,就拿自己熟悉的`React`来构建吧。
+ `UI`框架,选用`Google`的`Material-ui`,个人感觉还是比较漂亮的`UI`库。
  • 命令行工具:
+ 我在google上找了会,也有现成的库([cli](https://github.com/urfave/cli))可以用。

初始化项目

  1. 首先我们在$GOPATH/src/github.com/[your_name]/创建一个blog文件夹,姑且就这样命名吧。无所谓~
  2. 使用的是前后端分离的模式,我这里创建如下几个文件夹:
* cmd 用来开发命令行工具
* controllers 用来存放后端的控制器,一些操作,一些功能。\* database 存放数据库的配置和连接数据库的操作。
* front\_web 用来存放`React`项目。
* models 存放使用`xorm`对应数据库表的模板(一些数据结构Struct)
* posts 由于是比较简单的系统,我就直接将客户端上传的文章放在了这个文件夹。
* routers 后端接口(路由)
* static Web静态文件夹
* main.go 整个程序的主入口。![我的目录](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/822b256b8d602a6e687681f86f8a8758fa880e888550f4df4404f8b78e1bbc1d)


1. 使用`gin`创建一个`Http`服务:


    * 使用`get`工具,将`gin`依赖安装到本地,[gin文档](https://github.com/gin-gonic/gin)



    
1
复制代码go get github.com/gin-gonic/gin
* 在`mian.go`中导入`gin`,然后创建服务。
1
复制代码package mainimport (  "io"  "os"  "github.com/gin-gonic/gin")func main() {  r := gin.New()  // 设置日志文件  f, _ := os.Create("gin.log")  gin.DefaultWriter = io.MultiWriter(f, os.Stdout)  // 使用日志中间件  r.Use(gin.Logger())  // 设置静态文件夹  r.Static("/static", "./static")  r.Run(":8888")}
* 运行该`main.go`文件,就可以看到启动信息,然后在浏览器中输入`http://localhost:8888/`就可以访问了,虽然现在什么都没有。

最后

  该项目示例已经写完放在github上了,这只是第一篇,用来介绍功能和怎么样来初始化项目的。大家在留言一起讨论或者去我的github上克隆项目直接跑着看看。之后继续更新,中间遇到的问题和一些解决办法。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

自动化测试selenium在小公司的成功实践 前言 背景

发表于 2018-06-17
1
2
3
4
复制代码本文可能是目前最完整的一篇selenium(java版)实践文章,不是之一。
如果你是java开发人员,本文将帮助你快速搭建整套selenium自动化测试框架,你可以帮助公司升级为自动化测试架构;
如果你是测试人员,那你得按照本文多实践一下,遇到不懂的咨询下公司的java开发,同样你也可以完成自动化测试架构升级。
当然啦,如果目前公司已经是自动化测试了,那本文就当是再次梳理下相关知识吧。

前言

可能提到自动化测试selenium,大家都会想到用python语言来编写脚本。但我们选择了java语言,因为我相信大部分公司java程序员比python程序员多得多。而对于很多测试人员,并不能熟练使用编程语言,所以他们需要别人指导。与其使用更简单的python语言,却看不懂语法,得不到别人帮助;那还不如使用java语言,无论是语法还是编程思路,都可以快速获得java开发人员的帮助。

背景

可能很多公司已经有标准的后端单元测试代码,但是自动化测试需要测试整个系统,前端是直接展示给用户的,所以,前端尤为重要,本文就是基于h5的web前端自动化测试。当然啦,这里推荐对项目进行前后端分离,如果项目没有前后端分离可参考某小公司RESTful、共用接口、前后端分离、接口约定的实践。

目前互联网上关于selenium完整的文章很少,也很难买到一个专门讲selenium的书籍,这让很多测试人员无从下手,而本文会弥补这一问题,尽可能详细完整介绍selenium的实践,提供一个简易版的完整项目代码在github上(因为公司项目代码没有脱敏,不能直接放到github上)。

相关知识

  1. html标签
  2. css样式
  3. js基础
  4. java基础
  5. bat脚本基础

首先html由标签<x></x>组成,详细本文会在真实项目中一一介绍。

正式实践

安装火狐浏览器

因为selenium在火狐浏览器里,可以自动化录制脚本,我们通过脚本录制可以生成出不同的语言脚本,可以省去我们90%的编写脚本工作量。
可以安装最新版的火狐浏览器,然后安装Katalon Recorder (Selenium IDE for Firefox)
使用火狐浏览器打开https://addons.mozilla.org/zh-CN/firefox/addon/katalon-automation-record/?src=search

录制脚本

以百度搜索掘金为例

  1. 地址栏打开百度
  2. 右上角,打开Katalon扩展
  3. 点击Katalon的New
  4. 点击 Record
  5. 网页中输入 掘金网
  6. 打开第一个掘金官网
  7. 在掘金官网搜索我以前写的一篇文章 我是如何重构整个研发项目,促进自动化运维DevOps的落地?
  8. 点击第一条 我是如何重构整个研发项目,促进自动化运维DevOps的落地?
  9. 点击Katalon的stop

每执行一个操作右下角都会提示

录制后的效果图

运行、分析脚本

录制后,我们点击一下play,可以看到火狐浏览器自动化的完成了我们刚刚的操作(关闭弹窗阻止,或者将掘金和百度加入不阻止弹窗列表)

点击Export

可以看到有各种语言 C#、Java、katalon、python2等。
我们先看看python2的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
复制代码# -*- coding: utf-8 -*-
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import Select
from selenium.common.exceptions import NoSuchElementException
from selenium.common.exceptions import NoAlertPresentException
import unittest, time, re

class Test(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.implicitly_wait(30)
self.base_url = "https://www.katalon.com/"
self.verificationErrors = []
self.accept_next_alert = True

def test_(self):
driver = self.driver
driver.get("https://www.baidu.com/index.php?tn=monline_3_dg")
driver.find_element_by_id("kw").click()
driver.find_element_by_id("kw").clear()
driver.find_element_by_id("kw").send_keys(u"掘金网")
driver.find_element_by_xpath("//div[@id='container']/div[2]/div").click()
driver.find_element_by_link_text(u"掘金- juejin.im - 一个帮助开发者成长的社区").click()
# ERROR: Caught exception [ERROR: Unsupported command [selectWindow | win_ser_1 | ]]
driver.find_element_by_xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input").click()
driver.find_element_by_xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input").click()
driver.find_element_by_xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input").clear()
driver.find_element_by_xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input").send_keys(u"我是如何重构整个研发项目,促进自动化运维DevOps的落地?")
driver.find_element_by_xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input").send_keys(Keys.ENTER)
driver.find_element_by_link_text(u"我是如何重构整个研发项目,促进自动化运维DevOps的落地?").click()

def is_element_present(self, how, what):
try: self.driver.find_element(by=how, value=what)
except NoSuchElementException as e: return False
return True

def is_alert_present(self):
try: self.driver.switch_to_alert()
except NoAlertPresentException as e: return False
return True

def close_alert_and_get_its_text(self):
try:
alert = self.driver.switch_to_alert()
alert_text = alert.text
if self.accept_next_alert:
alert.accept()
else:
alert.dismiss()
return alert_text
finally: self.accept_next_alert = True

def tearDown(self):
self.driver.quit()
self.assertEqual([], self.verificationErrors)

if __name__ == "__main__":
unittest.main()

我们再看看java junit脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
复制代码package com.example.tests;

import java.util.regex.Pattern;
import java.util.concurrent.TimeUnit;
import org.junit.*;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import org.openqa.selenium.*;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.support.ui.Select;

public class Test {
private WebDriver driver;
private String baseUrl;
private boolean acceptNextAlert = true;
private StringBuffer verificationErrors = new StringBuffer();

@Before
public void setUp() throws Exception {
driver = new FirefoxDriver();
baseUrl = "https://www.katalon.com/";
driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
}

@Test
public void test() throws Exception {
driver.get("https://www.baidu.com/index.php?tn=monline_3_dg");
driver.findElement(By.id("kw")).click();
driver.findElement(By.id("kw")).clear();
driver.findElement(By.id("kw")).sendKeys("掘金网");
driver.findElement(By.xpath("//div[@id='container']/div[2]/div")).click();
driver.findElement(By.linkText("掘金- juejin.im - 一个帮助开发者成长的社区")).click();
// ERROR: Caught exception [ERROR: Unsupported command [selectWindow | win_ser_1 | ]]
driver.findElement(By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")).click();
driver.findElement(By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")).click();
driver.findElement(By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")).clear();
driver.findElement(By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")).sendKeys("我是如何重构整个研发项目,促进自动化运维DevOps的落地?");
driver.findElement(By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")).sendKeys(Keys.ENTER);
driver.findElement(By.linkText("我是如何重构整个研发项目,促进自动化运维DevOps的落地?")).click();
}

@After
public void tearDown() throws Exception {
driver.quit();
String verificationErrorString = verificationErrors.toString();
if (!"".equals(verificationErrorString)) {
fail(verificationErrorString);
}
}

private boolean isElementPresent(By by) {
try {
driver.findElement(by);
return true;
} catch (NoSuchElementException e) {
return false;
}
}

private boolean isAlertPresent() {
try {
driver.switchTo().alert();
return true;
} catch (NoAlertPresentException e) {
return false;
}
}

private String closeAlertAndGetItsText() {
try {
Alert alert = driver.switchTo().alert();
String alertText = alert.getText();
if (acceptNextAlert) {
alert.accept();
} else {
alert.dismiss();
}
return alertText;
} finally {
acceptNextAlert = true;
}
}
}

python代码量明细比java要少一点,但是本文讲java语言实践。

我们主要关注 java版 @Test注解的那个test方法

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码    driver.get("https://www.baidu.com/index.php?tn=monline_3_dg");
driver.findElement(By.id("kw")).click();
driver.findElement(By.id("kw")).clear();
driver.findElement(By.id("kw")).sendKeys("掘金网");
driver.findElement(By.xpath("//div[@id='container']/div[2]/div")).click();
driver.findElement(By.linkText("掘金- juejin.im - 一个帮助开发者成长的社区")).click();
// ERROR: Caught exception [ERROR: Unsupported command [selectWindow | win_ser_1 | ]]
driver.findElement(By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")).click();
driver.findElement(By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")).click();
driver.findElement(By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")).clear();
driver.findElement(By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")).sendKeys("我是如何重构整个研发项目,促进自动化运维DevOps的落地?");
driver.findElement(By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")).sendKeys(Keys.ENTER);
driver.findElement(By.linkText("我是如何重构整个研发项目,促进自动化运维DevOps的落地?")).click();

可能很多人已经能看懂了

driver.get(“https://www.baidu.com/index.php?tn=monline\_3\_dg");

打开百度

driver.findElement(By.id(“kw”)).click();

通过id定位到html标签,然后点击click();清空文本框.clear();输入 掘金网3个字 sendKeys(“掘金网”);

这里我们看一下百度的搜索框代码

1
复制代码<input type="text" class="s_ipt" name="wd" id="kw" maxlength="100" autocomplete="off">

driver.findElement(By.linkText(“掘金- juejin.im - 一个帮助开发者成长的社区”)).click();

单击掘金网
通过linktext定位到标签并点击。

后面通过div=juejin一层一层定位到input,最后点击进入文章。

认识html标签

HTML <input>标签

<input>标签用于搜集用户信息。
根据不同的 type 属性值,输入字段拥有很多种形式。输入字段可以是文本字段、复选框、掩码后的文本控件、单选按钮、按钮等等。

1
2
3
4
5
复制代码<form action="form_action.asp" method="get">
First name: <input type="text" name="fname" />
Last name: <input type="text" name="lname" />
<input type="submit" value="Submit" />
</form>

详情参考 http://www.w3school.com.cn/tags/tag\_input.asp

HTML <a>标签

1
2
复制代码<a> 标签定义超链接,用于从一张页面链接到另一张页面。
<a> 元素最重要的属性是 href 属性,它指示链接的目标。

详情参考http://www.w3school.com.cn/tags/tag\_a.asp

HTML <div>标签

<div>可定义文档中的分区或节(division/section)。
<div>标签可以把文档分割为独立的、不同的部分。它可以用作严格的组织工具,并且不使用任何格式与其关联。
如果用 id 或 class 来标记<div>,那么该标签的作用会变得更加有效。

1
2
3
4
复制代码<div style="color:#00FF00">
<h3>This is a header</h3>
<p>This is a paragraph.</p>
</div>

详情参考http://www.w3school.com.cn/tags/tag\_div.asp

…………

其他标签不一一介绍,可在参考网站上意义看

认识css

这里只讲1个关键的,比如

1
复制代码<div class="css1 css2"> ********</div>

表示这个div同时使用了css1和css2样式,只需要知道如果没办法在selenium上定位的这个div,可使用css名定位。

如果有兴趣,可再看下其他css相关知识。

js基础

这里讲2个关键

1
复制代码<a onclick="test()">test</a>

上述代码,点击a标签会执行js中的test方法,当selenium无法定位到这个a标签,可以直接调用test()方法。

可以写简单的js脚本,弹窗代码:

1
复制代码alert("hello");

下载谷歌浏览器

下载谷歌浏览器,这里可以使用63.0.3239.84版本。
目前来说,谷歌浏览器版本兼容性还是不错的。

下载selenium driver

https://www.seleniumhq.org/download/

可不下,本文github项目中包含

下载selenium webdriver

https://npm.taobao.org/mirrors/chromedriver/
需下载和谷歌浏览器对应的版本2.40
可不下,本文github项目中包含

下载idea开发工具

https://www.jetbrains.com/idea/

这个比较复杂,建议在java开发人员指导下完成。

selenium

这个版本是简易版,但足够

最终效果

我们通过录制selenium脚本,编辑,提交到git库,由jenkins自动化编译出jar包,通过bat命令在任意一台pc端执行(默认开发人员提交代码后自动执行所有模块)。按功能模块,测试项目,生成测试报告。对测试不通过的模块

最大化

1
复制代码driver.manage().window().maximize();

打开页面

1
复制代码driver.get("https://www.baidu.com");

定位元素

多个相同时,返回第一个,没有找到会抛异常NoSuchElementException

1
复制代码WebElement element = driver.findElement(*);

当返回多个时:

1
复制代码List<WebElement> elements = driver.findElements(*);

定位元素方式

1
复制代码<input class="input_class input_class2" type="text" name="user-name" id="user-id" />

通过id定位

1
复制代码WebElement element = driver.findElement(By.id("user-id"));

通过name定位

1
复制代码WebElement element = driver.findElement(By.name("user-name"));

通过className定位

1
复制代码WebElement element = driver.findElement(By.className("input_class.input_class2"));

注意多个class用小数点隔开,也可以使用cssSelector定位

1
复制代码WebElement element = driver.findElement(By.cssSelector("input"));

通过linkText定位,如:

1
复制代码WebElement element = driver.findElement(By.linkText("我是如何重构整个研发项目,促进自动化运维DevOps的落地?"));

意思就是链接内容定位

通过partialLinkText定位,模糊内容定位,和上相似

1
复制代码WebElement element = driver.findElement(By.linkText("我是如何重构整个研发项目?"));

通过tagName定位

1
复制代码WebElement element = driver.findElement(By.tagName("form"));

通过xpath定位

1
复制代码WebElement element = driver.findElement(By.xpath("//input[@id='passwd-id']"));

这个最为复杂,最简单的版本是

1
复制代码//标签类型[@属性名=属性值]

但也可以定位第几个

1
复制代码//input[4]

其中[]中还可以增加逻辑and or表达式

1
2
复制代码WebElement element = driver.findElement(By.xpath("//input[@type='text' and @name='user-name']"));
WebElement element = driver.findElement(By.xpath("//input[@type='text' or @name='user-name']"));

[]中也可以增加start-with、ends-with、contains,比如

1
2
3
复制代码WebElement element = driver.findElement(By.xpath("//input[start-with(@id,'user-')]"));
WebElement element = driver.findElement(By.xpath("//input[ends-with(@id,'user-')]"));
WebElement element = driver.findElement(By.xpath("//input[contains(@id,'user-')]"));

还可以 任意属性名

1
复制代码WebElement element = driver.findElement(By.xpath("//input[@*='user-name']"));

更多xpath使用方法见
http://www.w3school.com.cn/xpath/index.asp

单击某个元素

1
复制代码.click()

清空input

1
复制代码.clear();

input中输入内容

1
复制代码.sendKeys("掘金网");

如果是上传附件,可直接sendKeys路径

1
复制代码.sendKeys("c:\shao.png");

得到input内容

1
复制代码.getText();

下拉框

1
2
3
复制代码    Select select = new Select(driver.findElement(By.id("frequency")));
select.selectByValue("1");
driver.findElement(By.id("validDays")).click();
1
2
3
4
5
6
复制代码    select.selectByValue("a"); 
select.deselectAll();
select.deselectByValue("a");
select.deselectByVisibleText("");
select.getAllSelectedOptions();
select.getFirstSelectedOption();

单选框

1
2
3
4
复制代码WebElement radio=driver.findElement(By.id("radio"));
radio.click();&emsp;&emsp;&emsp;&emsp; //选择某个选项
radio.clear();&emsp;&emsp;&emsp;&emsp; //清空选项
radio.isSelected();&emsp;&emsp;//判断某个单选项是否被选中

复选框

1
2
3
复制代码WebElement checkbox = driver.findElement(By.id("checkbox"));
checkbox.clear(); //清空选项
checkbox.isSelected(); //是否选中

判断是否可点击

1
复制代码isEnabled()

alert框操作

1
2
3
复制代码Alert alert = driver.switchTo().alert();
alert.accept();&emsp;&emsp;//确定
alert.dismiss();&emsp; //取消

iframe切换(重点)

可能很多老的项目都有iframe,录制脚本的时候正常录制,可执行的时候,却无法执行,这个时候,需要切换iframe

1
2
复制代码driver.switchTo().defaultContent();&emsp;//回到默认的页面
driver.switchTo().frame("leftFrame"); //切换到某个iframe

切换iframe,结束后,记得切换回默认页面。

1
2
3
4
5
6
7
8
9
复制代码        driver.findElement(By.linkText("导入模板")).click();
WebElement iframe = driver.findElement(By.id("layui-layer-iframe1"));
driver.switchTo().frame(iframe);
Thread.sleep(2000);
driver.findElement(By.linkText("引用")).click();
driver.findElement(By.xpath("//button[@type='submit']")).click();
driver.findElement(By.xpath("(//button[@type='button'])[3]")).click();
Thread.sleep(1000);
driver.findElement(By.linkText("学生")).click();

以上摘自项目代码,仅供参考

执行 js

1
2
复制代码    JavascriptExecutor js = (JavascriptExecutor) driver;
js.executeScript("viewDetail('1f50555e409a4597a027ff415ce6c9b4','09','2018')");

执行内部viewDetail方法

延时操作(重要)

很多时候我们需要延时,这时使用

1
复制代码Thread.sleep(1000);//延时1000毫秒

许多错误是因为需要等待时间,尝试增加一个延时,也许这个问题就过去了。

项目代码

假设,我们产品有多个环境,我们定义一个environments数组,(当-1时,提示用户输入),有多个模块(当-1时,提示用户输入),最终代码如下,执行后,错误报告会通过邮件发送到指定邮箱或者其他地方。

运行效果图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
复制代码import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;
import webfunction.*;

import java.util.Scanner;

public class Main {
private static WebDriver driver;
private static String baseUrl;
private boolean acceptNextAlert = true;
/**
* 各个环境
* */
private static String[] environments = {"环境1", "环境2", "环境3", "环境4", "环境5", "环境6"};
/**
* 错误日志
* */
private static StringBuffer verificationErrors = new StringBuffer();
/**
* 是否处于debug模式
*/
private static boolean debug = false;
/**
* -1为手动模式,否则为指定数字
* */
private static String environment = "-1";
/**
* -1为手动模式,否则为指定数字
* */
private static String methods = "-1";

public static void main(String[] args) throws Exception {
//引用火狐浏览器驱动
System.setProperty("webdriver.chrome.driver", "C:\\selenium\\chromedriver.exe");

//定义用户名密码
String uname, upw;
Scanner sc = new Scanner(System.in);
System.out.println("请选择环境");
for (int i = 0; i < environments.length; i++) {
System.out.println(i + ":" + environments[i]);
}
if ("-1".equals(environment)) {
environment = sc.next();
}

System.out.println("请输入需要测试的功能,英文逗号隔开");
if ("-1".equals(methods)) {
methods = sc.next();
}
driver = new ChromeDriver();

System.out.println("您选择的是" + environments[Integer.valueOf(environment)]);
switch (environment) {
case "0":
baseUrl = "http://*.*.*.*/";
uname = "admin";
upw = "admin";
testManage(baseUrl, uname, upw, methods, driver);
break;
case "1":
baseUrl = "http://*.*.*.*/";
uname = "admin";
upw = "admin";
testManage(baseUrl, uname, upw, methods, driver);
break;
case "2":
//等等等……
break;
}

}

private static void testManage(String url, String uname, String upw, String methods, WebDriver driver) throws InterruptedException {
//先登录管理端
WebLogin.webLogin(driver, url, uname, upw);
//然后测试所有模块
String[] strArray = null;
strArray = methods.split(",");
for (int i = 0; i < strArray.length; i++) {
switch (strArray[i]) {
case "0":
try {
// 系统基础管理 - 用户管理 - 新增用户
WebSystemManage.addnewUser(driver, url);
} catch (Exception e) {
verificationErrors.append("系统基础管理 - 用户管理 - 新增用户 出错");
log(e);
}
break;
case "1":
try {
// 系统基础管理 - 用户管理 - 编辑用户
WebSystemManage.editUser(driver, url);
} catch (Exception e) {
System.out.println("系统基础管理 - 用户管理 - 编辑用户 出错");
log(e);
}
break;
default:
break;
}
}
report(verificationErrors);
}

private static void report(StringBuffer verificationErrors) {
//发送邮件
}

/**
* 根据debug变量是否输出日志
* @param e
*/
private static void log(Exception e) {
if (debug) {
e.printStackTrace();
}
}


private static boolean isElementPresent(By by) {
try {
driver.findElement(by);
return true;
} catch (NoSuchElementException e) {
return false;
}
}

private static boolean isAlertPresent() {
try {
driver.switchTo().alert();
return true;
} catch (NoAlertPresentException e) {
return false;
}
}

private static String closeAlertAndGetItsText() {
try {
Alert alert = driver.switchTo().alert();
String alertText = alert.getText();
if (acceptNextAlert) {
alert.accept();
} else {
alert.dismiss();
}
return alertText;
} finally {
acceptNextAlert = true;
}
}
}

代码那么多其实我们只关注 public static void main(String[] args) throws Exception {}内的内容,比如,我们想运行我们最初录制的掘金脚本,只需将那端我要求特别关注的代码放到里面即可,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
复制代码import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;

public class Main {
private static WebDriver driver;

public static void main(String[] args) throws Exception {
//引用火狐浏览器驱动
System.setProperty("webdriver.chrome.driver", "C:\\selenium\\chromedriver.exe");
driver = new ChromeDriver();
driver.manage().window().maximize();
//以下为Katalon Recorder录制后的脚本
driver.get("https://www.baidu.com/index.php?tn=monline_3_dg");
Thread.sleep(2000);
driver.findElement(By.id("kw")).click();
driver.findElement(By.id("kw")).clear();
driver.findElement(By.id("kw")).sendKeys("掘金网");
Thread.sleep(100);
driver.findElement(By.id("su")).click();
Thread.sleep(1000);
driver.findElement(By.xpath("//div[@id='container']/div[2]/div")).click();
driver.findElement(By.linkText("掘金- juejin.im - 一个帮助开发者成长的社区")).click();
Thread.sleep(3000);
// ERROR: Caught exception [ERROR: Unsupported command [selectWindow | win_ser_1 | ]]
driver.findElement(By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")).click();
driver.findElement(By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")).click();
driver.findElement(By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")).clear();
driver.findElement(By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")).sendKeys("我是如何重构整个研发项目,促进自动化运维DevOps的落地?");
driver.findElement(By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")).sendKeys(Keys.ENTER);
Thread.sleep(2000);
driver.findElement(By.linkText("我是如何重构整个研发项目,促进自动化运维DevOps的落地?")).click();
}
}

上述代码中注释内是Katalon Recorder导出的脚本,但是我们增加了一些延时操作,selenium延时有很3种:普通sleep、显示等待方式、隐式等待方式。这里先简单粗暴一下,用Thread.sleep(*);延时,比如打开百度延时2秒、输入“掘金网”延时100毫秒、搜索后延时3秒…………

很遗憾,我们代码报错:

大概意思说超时没有找到那个搜索框,由于各种各样的原因,会导致我们在火狐浏览器中录制的脚本在java代码中的谷歌浏览器里无法兼容,这个时候我们需要去分析一下具体逻辑。
这里是由于新窗口需要切换window,可使用下述代码切换(替换代码中// ERROR: Caught exception [ERROR: Unsupported command [selectWindow | win_ser_1 | ]]这行即可)。

1
2
3
4
5
6
7
8
复制代码        Set<String> windowHandles = driver.getWindowHandles();
String windowHandle = driver.getWindowHandle();
for (String handle : windowHandles) {
if (!handle.equals(driver.getWindowHandle())) {
driver.switchTo().window(handle);
break;
}
}

导出的脚本By.xpath(“//div[@id=’juejin’]/div[2]/div/header/div/nav/ul/li[2]/form/input”)这一句很复杂,我们试着简化它。

1
复制代码<input data-v-5ce25e66="" maxlength="32" placeholder="搜索掘金" class="search-input">

首先搜索下search-input样式,看该页面是否只有一个search-input样式。

果然search-input样式只有一个标签。
于是我们将

1
2
3
复制代码By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")
//改为
By.className("search-input")

最终代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
复制代码import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;

import java.util.Set;

public class Main {
private static WebDriver driver;

public static void main(String[] args) throws Exception {
//引用火狐浏览器驱动
System.setProperty("webdriver.chrome.driver", "C:\\selenium\\chromedriver.exe");
driver = new ChromeDriver();
driver.manage().window().maximize();
driver.get("https://www.baidu.com/index.php?tn=monline_3_dg");
Thread.sleep(2000);
driver.findElement(By.id("kw")).click();
driver.findElement(By.id("kw")).clear();
driver.findElement(By.id("kw")).sendKeys("掘金网");
Thread.sleep(100);
driver.findElement(By.id("su")).click();
Thread.sleep(1000);
driver.findElement(By.xpath("//div[@id='container']/div[2]/div")).click();
driver.findElement(By.linkText("掘金- juejin.im - 一个帮助开发者成长的社区")).click();
Thread.sleep(7000);
Set<String> windowHandles = driver.getWindowHandles();
String windowHandle = driver.getWindowHandle();
for (String handle : windowHandles) {
if (!handle.equals(driver.getWindowHandle())) {
driver.switchTo().window(handle);
break;
}
}
// ERROR: Caught exception [ERROR: Unsupported command [selectWindow | win_ser_1 | ]]
driver.findElement(By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")).click();
driver.findElement(By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")).click();
driver.findElement(By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")).clear();
driver.findElement(By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")).sendKeys("我是如何重构整个研发项目,促进自动化运维DevOps的落地?");
driver.findElement(By.xpath("//div[@id='juejin']/div[2]/div/header/div/nav/ul/li[2]/form/input")).sendKeys(Keys.ENTER);
Thread.sleep(2000);
driver.findElement(By.linkText("我是如何重构整个研发项目,促进自动化运维DevOps的落地?")).click();
}
}

编译打包

得到selenium.jar包,可复制到C:\selenium下,和chromedriver.exe同级。

输入cmd命令

1
2
3
复制代码C:\Users\Administrator>cd C:\selenium

C:\selenium>java -jar selenium2.jar

即可自动化运行,非windows系统下载2.40其他版本https://npm.taobao.org/mirrors/chromedriver/2.40/

github项目运行

https://github.com/qq273681448/selenium

为了防止有读者没有改maven库镜像,所以把lib包都放在项目中了。直接使用idea打开,可能有些配置需要改,可参考

写在最后

至此,一个基础版的selenium框架就搭好了,后续,可以连接数据库,从库中随机取出帐号,进行项目测试。也可以配合bat脚本,实现自动化测试以及报告生成。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Tomcat 是怎样处理搜索引擎爬虫请求的?

发表于 2018-06-12

每个置身于互联网中的站点,都需要搜索引擎的收录,以及在适时在结果中的展现,从而将信息提供给用户、读者。

而搜索引擎如何才能收录我们的站点呢?

这就涉及到一个「搜索引擎的爬虫」爬取站点内容的过程。只有被搜索引擎爬过并收录的内容才有机会在特定query命中之后在结果中展现。

这些搜索引擎爬内容的工具,又被称为爬虫、Sprider,Web crawler 等等。我们一方面欢迎其访问站点以便收录内容,一方面又因其对于正常服务的影响头疼。毕竟 Spider 也是要占用服务器资源的, Spider 太多太频繁的资源占用,正常用户请求处理就会受到影响。所以一些站点干脆直接为搜索引擎提供了单独的服务供其访问,其他正常的用户请求走另外的服务器。

说到这里需要提一下,对于是否是 Spider 的请求识别,是通过HTTP 请求头中的User-Agent 字段来判断的,每个搜索引擎有自己的独立标识。而且通过这些内容,管理员也可以在访问日志中了解搜索引擎爬过哪些内容。

此外,在对搜索引擎的「爬取声明文件」robots.txt中,也会有类似的User-agent 描述。比如下面是taobao 的robots.txt描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
> 复制代码User-agent:  Baiduspider
> Allow: /article
> Allow: /oshtml
> Disallow: /product/
> Disallow: /
>
> User-Agent: Googlebot
> Allow: /article
> Allow: /oshtml
> Allow: /product
> Allow: /spu
> Allow: /dianpu
> Allow: /oversea
> Allow: /list
> Disallow: /
>
> User-agent: Bingbot
> Allow: /article
> Allow: /oshtml
> Allow: /product
> Allow: /spu
> Allow: /dianpu
> Allow: /oversea
> Allow: /list
> Disallow: /
>
> User-Agent: 360Spider
> Allow: /article
> Allow: /oshtml
> Disallow: /
>
> User-Agent: Yisouspider
> Allow: /article
> Allow: /oshtml
> Disallow: /
>
> User-Agent: Sogouspider
> Allow: /article
> Allow: /oshtml
> Allow: /product
> Disallow: /
>
> User-Agent: Yahoo! Slurp
> Allow: /product
> Allow: /spu
> Allow: /dianpu
> Allow: /oversea
> Allow: /list
> Disallow: /
>
>

我们再来看 Tomcat对于搜索引擎的请求做了什么特殊处理呢?

对于请求涉及到 Session,我们知道通过 Session,我们在服务端得以识别一个具体的用户。那 Spider 的大量请求到达后,如果访问频繁同时请求量大时,就需要创建巨大量的 Session,需要占用和消耗很多内存,这无形中占用了正常用户处理的资源。

为此, Tomcat 提供了一个 「Valve」,用于对 Spider 的请求做一些处理。

首先识别 Spider 请求,对于 Spider 请求,使其使用相同的 SessionId继续后面的请求流程,从而避免创建大量的 Session 数据。

这里需要注意,即使Spider显式的传了一个 sessionId过来,也会弃用,而是根据client Ip 来进行判断,即对于 相同的 Spider 只提供一个Session。

我们来看代码:

1
2
3
> 复制代码// If the incoming request has a valid session ID, no action is requiredif (request.getSession(false) == null) {    // Is this a crawler - check the UA headers    Enumeration<String> uaHeaders = request.getHeaders("user-agent");    String uaHeader = null;    if (uaHeaders.hasMoreElements()) {        uaHeader = uaHeaders.nextElement();    }    // If more than one UA header - assume not a bot    if (uaHeader != null && !uaHeaders.hasMoreElements()) {        if (uaPattern.matcher(uaHeader).matches()) {            isBot = true;            if (log.isDebugEnabled()) {                log.debug(request.hashCode() +                        ": Bot found. UserAgent=" + uaHeader);            }        }    }    // If this is a bot, is the session ID known?    if (isBot) {        clientIp = request.getRemoteAddr();        sessionId = clientIpSessionId.get(clientIp);        if (sessionId != null) {            request.setRequestedSessionId(sessionId); // 重用session        }    }}getNext().invoke(request, response);if (isBot) {    if (sessionId == null) {        // Has bot just created a session, if so make a note of it        HttpSession s = request.getSession(false);        if (s != null) {            clientIpSessionId.put(clientIp, s.getId()); //针对Spider生成session            sessionIdClientIp.put(s.getId(), clientIp);            // #valueUnbound() will be called on session expiration            s.setAttribute(this.getClass().getName(), this);            s.setMaxInactiveInterval(sessionInactiveInterval);            if (log.isDebugEnabled()) {                log.debug(request.hashCode() +                        ": New bot session. SessionID=" + s.getId());            }        }    } else {        if (log.isDebugEnabled()) {            log.debug(request.hashCode() +                    ": Bot session accessed. SessionID=" + sessionId);        }    }}
>
>

判断Spider 是通过正则

1
2
3
> 复制代码private String crawlerUserAgents =    ".*[bB]ot.*|.*Yahoo! Slurp.*|.*Feedfetcher-Google.*";
>
>

// 初始化Valve的时候进行compile

1
2
3
> 复制代码uaPattern = Pattern.compile(crawlerUserAgents);
>
>

这样当 Spider 到达的时候就能通过 User-agent识别出来并进行特别处理从而减小受其影响。

这个 Valve的名字是:「CrawlerSessionManagerValve」,好名字一眼就能看出来作用。

其他还有问题么?我们看看,通过ClientIp来判断进行Session共用。

最近 Tomcat 做了个bug fix,原因是这种通过ClientIp的判断方式,当 Valve 配置在Engine下层,给多个Host 共用时,只能有一个Host生效。 fix之后,对于请求除ClientIp外,还有Host和 Context的限制,这些元素共同组成了 client标识,就能更大程度上共用Session。

修改内容如下:

关于Session管理等相关的内容,可以看下我之前的几篇文章:对于过期的session,Tomcat做了什么?

深入Tomcat源码分析Session到底是个啥!

Tomcat的Session持久化策略

关于 Valve 的功能及使用,可以看这几篇文章:阀门(Valve)常打开,快发请求过来 | Tomcat的AccessLogValve介绍

如何避免Manager应用被人利用

总结下, 该Valve 通过标识识别出 Spider 请求后,给其分配一个固定的Session,从而避免大量的Session创建导致我资源占用。

默认该Valve未开启,需要在 server.xml中 增加配置开启。另外我们看上面提供的 正则 pattern,和taobao 的robots.txt对比下,你会出现并没有包含国内的这些搜索引擎的处理,这个时候怎么办呢?

在配置的时候传一下进来就OK啦,这是个public 的属性

1
2
3
> 复制代码public void setCrawlerUserAgents(String crawlerUserAgents) {    this.crawlerUserAgents = crawlerUserAgents;    if (crawlerUserAgents == null || crawlerUserAgents.length() == 0) {        uaPattern = null;    } else {        uaPattern = Pattern.compile(crawlerUserAgents);    }}
>
>

相关阅读:

深度揭秘乱码问题背后的原因及解决方式

怎样阅读源代码?

读源码时,我们到底在读什么?

Tomcat进程自动退出问题

Tomcat与内存泄露处理

关注『 Tomcat那些事儿 』,发现更多精彩文章!了解各种常见问题背后的原理与答案。深入源码,分析细节,内容原创,欢迎关注。

觉得不错?转发也是支持,谢谢****

更多精彩内容:

一台机器上安装多个Tomcat 的原理(回复001)

监控Tomcat中的各种数据 (回复002)

启动Tomcat的安全机制(回复003)

乱码问题的原理及解决方式(回复007)

Tomcat 日志工作原理及配置(回复011)

web.xml 解析实现(回复 012)

线程池的原理( 回复 014)

Tomcat 的集群搭建原理与实现 (回复 015)

类加载器的原理 (回复 016)

类找不到等问题 (回复 017)

代码的热替换实现(回复 018)

Tomcat 进程自动退出问题 (回复 019)

为什么总是返回404? (回复 020)

…

PS: 对于一些 Tomcat常见问 题,在公众号的【常见问题】菜单中,有需要的朋友欢迎关注查看。

aa

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

那些年,我们踩过的PHP的坑

发表于 2018-06-11

某女:你能让这个论坛的人都吵起来,我就跟你吃饭。
PHP程序员:PHP是世界上最好的语言!
某论坛炸锅了,各种吵架……
某女:服了你了,我们走吧!
PHP程序员:今天不行,我一定要说服他们,PHP必须是最好的语言。

有人用的语言,就有人骂,骂声越大,用的人也就越多。世上没有完美的语言,最合适的语言就是最好的语言,我们要做的,就是扬长避短,少踩那些坑,下面直接进入主题。

0x01, 弱类型

==和===异同这种太过低级的坑就直接跳过了,先看一个稍微隐蔽点的坑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码function translate($keyword)
{
$trMap = [
'baidu' => '百度',
'sougou' => '搜狗',
'360' => '360',
'google' => '谷歌'
];
foreach ($trMap as $key => $value) {
if (strpos($keyword, $key) !== false) {
return $value;
}
}
return '其他';
}

echo translate("baidu") . "\n";
echo translate("360") . "\n";

期待的结果是

1
2
复制代码百度
360

实际运行结果是

1
2
复制代码百度
其他

仔细检查,没有string和int的混用,比较也都是用的 !== ,没有用==,为什么还会掉坑里?

问题出在了array上面,虽然你写的是

1
2
3
4
5
6
复制代码    $trMap = [ 
'baidu' => '百度',
'sougou' => '搜狗',
'360' => '360',
'google' => '谷歌'
];

但是PHP给你处理成了

1
2
3
4
5
6
7
8
9
10
复制代码array(4) {
["baidu"]=>
string(6) "百度"
["sougou"]=>
string(6) "搜狗"
[360]=>
string(3) "360"
["google"]=>
string(6) "谷歌"
}

360变成了int类型,这个时候strpos不该报错吗?不,当然是原谅它啦,它选择兼容int

If needle is not a string, it is converted to an integer and applied as the ordinal value of a character.

360的hex表示是0x168,所以当你这样调用时,它能匹配

1
复制代码translate("\x1\x68")

那么正确的写法是怎么样的呢?稍加改动即可

1
复制代码strpos($keyword, $key) //改为 strpos($keyword, (string) $key)

可怕之处在于

  • 自以为用了===就安全了,忽视了弱类型无处不在这个隐患
  • 你可能并没有仔细看每一个函数的说明,没有逐个核对每个参数的类型
  • 引发的bug不一定能重现,也有可能平时不会触发,但是留下了安全漏洞

如何100%的避免弱类型的坑?答案是换强类型语言。如果不能换呢?通过以下准则,虽然做不到100%避免,但是做到99.99%是有希望的。

  1. 能用===/!==的地方,绝不用==/!=,知道类型的情况下,先强转再用===比较
  2. 调用函数的时候,如果你知道参数类型,在调用时强制转换一下,不能嫌麻烦

我说的是弱类型,不是动态类型,两者不是一码事,不要误会。Python是动态类型强类型,PHP是动态类型弱类型,C语言是静态类型弱类型。如果可以选择,我宁可PHP放弃弱类型,因为弱类型带来的麻烦,已经超出它的便利了。提供一个strict运行模式也行,给足大家十年八年时间慢慢迁移。

0x02, 空字典json序列化成了[]

随着APP的流行,PHP很多时候不是跟浏览器端的JS交互,而是跟Java和ObjC这样的静态类型语言交互,返回值的类型定义,就很重要了,举个栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码$ret1 = [ 
'choices' => ['鱼香肉丝', '宫保鸡丁'],
'answers' => [
'张三' => 0,
'李四' => 1,
'赵云' => 0,
],
];

$ret2 = [
'choices' => [],
'answers' => [],
];

echo json_encode($ret1) . "\n";
echo json_encode($ret2) . "\n";

输出

1
2
复制代码{"choices":["\u9c7c\u9999\u8089\u4e1d","\u5bab\u4fdd\u9e21\u4e01"],"answers":{"\u5f20\u4e09":0,"\u674e\u56db":1,"\u8d75\u4e91":0}}
{"choices":[],"answers":[]}

客户端在定义这个model的时候,可能是这样定义的

1
2
3
4
复制代码class ResultDTO {
lateinit var choices: List<String>
lateinit var answers: Map<String, Int>
}

当返回ret1的时候,一切顺风顺水,皆大欢喜。如果返回ret2呢,客户端抗议了

com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.util.LinkedHashMap out of START_ARRAY token

原因是什么呢?PHP的json_encode面对一个空的array的时候,它很为难,它不知道应该当它是list还是map,所以它只能一刀切,认为它就是list,于是客户端就不高兴了。解决办法不是没有,依然是强制转换。

1
2
3
4
复制代码$ret2 = [
'choices' => [],
'answers' => (object) [],
];

但是这样就带来一个问题,如果answers不是写死的,而是某个API的返回值,你并不确定它是不是会返回空的,它也没有义务帮你cast成object,因为JSON序列化是跟前端交互的事情,不应该放到后端service层面解决。那么你只能自己动手了,手动把返回值中可能出现空map的地方,全部强制转换一遍。

PHP的关联数组的确很强大,算法设计的也不错,性能也很好,但是它不是没有代价的,上面的栗子算是其中一个。如果PHP也像其它语言一样,区分map和list,可能会省事一些,毕竟区分{}和[],对程序员来说并不会增加很多学习成本。

0x03, 健忘的FPM

近些年,Swoole和WorkerMan那样的CLI部署方式,慢慢被中国人知悉和应用,然而跟FPM或者mod_php相比,CLI方式还是太过非主流,在绝对垄断的FPM/mod_php面前,CLI在缓慢成长中。FPM的缺点很明显,每个请求结束的时候,你在PHP代码里创建的对象都被清理了,你执行过的代码,就跟没执行过一样,不留痕迹。

在hello world那样的微型应用中,好像问题不大,稍微大一点的项目,我们为了DRY,为了少做重复劳动,为了提高开发效率,不得不使用框架,然后问题就来了,用PHP写的PHP框架,由于FPM的健忘,框架从init开始,到读取配置文件,到初始化各个组件,这种工作在每个请求到来的时候,都要重复的做一次,如果你需要读一个100M的元数据,那么每个HTTP请求来时,你都要读一次并解析一次,当你HTTP请求结束返回时,你解析过的100M元数据,又被销毁了,下一个请求来时,你依然要重复做。

本来PHP 5.6已经可以吊打Python 3.6的性能了,PHP 7.1都不屑于跟Python比性能了,快几倍了。但是一旦引入同体量的框架,比如PHP用Laravel,Python用Django,剧情就反转了,Django竟然可以吊打PHP7加持的Laravel了。一个百米运动员就算跑的再快,每次枪响后都要先穿鞋带,穿好鞋带再穿鞋,然后再跑,跑完了把鞋脱下,再把鞋带抽出。就算它100米只要1秒就能跑完,光穿鞋的时间就够别的选手跑个来回了。

所以包括PHP之父本人在内,都对Laravel这样的封装深的厚框架表示质疑,在需要考虑性能的时候,主流人士往往推荐不用框架,或者用极简的框架,要么就是那些C写的框架,比如yaf和phalcon。框架性能问题算是曲线解决了,那么用户自己的逻辑呢?这个就比较麻烦了。分情况探讨,简单类型,如string,可以用yaconf这个扩展,可以做到不重复读取。如果是复杂的数据结构,比如树状结构,就没法用这种方式解决了。有没有解决办法呢?也不是没有,你可以写个脚本,把数据转换成PHP代码,然后通过opcache缓存起来,也能缓解一下问题。要彻底解决,只能写个C扩展让它常驻内存了,但这就超出一般PHP开发的能力范围了。

FPM这种方式并非PHP首创,在fastcgi出现之前,CGI都是这么干的,而且还是每个请求新开一个进程,比FPM还要开销大。然而到了21世纪,还在用FPM这种健忘型运行模式的,常见语言里就只剩PHP了。可能再过十年,FPM也渐渐被Swoole这样的不健忘的给取代了。

0x04,多线程支持

这里不讨论Apache的MPM是否支持多线程,也不讨论PHP的扩展是否支持多线程,更不讨论PHP到底能不能利用多线程或者多核,这里只讨论纯粹的PHP代码,能否创建和管理线程。前几年,PHP是完全不支持多线程,现在呢?据说有了pthreads,然后打开它的文档,发现

WarningThe pthreads extension cannot be used in a web server environment. Threading in PHP should therefore remain to CLI-based applications only.

WarningThe pthreads extension can only be used with PHP 7.2+. This is due to ZTS mode being unsafe in prior PHP versions.

两个限制

  1. 只能用在CLI下面
  2. 只支持PHP 7.2+

没用过多线程的人,自然不能体会多线程的便捷之处,跟多进程相比,数据共享在进程内部要容易的多。现代语言支持多线程是很自然的事情,跟PHP对比最多的Python,早就有了原生线程的支持,虽然因为GIL做不了CPU密集型应用,但是做个IO密集型还是很方便的。多线程只是锦上添花,不是雪中送炭,好在PHP的多进程支持还算OK,咱们就用多进程好了,最多共享数据结构的时候,想办法绕开便是。线程池 + 执行队列,变成进程池 + 执行队列。

0x05, 32bit平台下,没有8字节的long类型

PHP的int是平台相关的,32位平台下是4字节,64位平台下是8字节,为了代码的健壮性和可移植性,我们只能假定int就是4字节的类型。但是我们很多时候需要8字节类型,因为

  1. 精确到毫秒的时间戳需要long
  2. 很多平台对接需要long,比如阿里巴巴

这个时候就需要GMP和BCMath这样的库了,比起语言直接支持8字节的long,麻烦了一些。

0x06, 数组函数设计的太差,使用不便

PHP提供了一大堆array_xxxx函数,而没有把这些函数作为数组的方法,这种设计,乍看之下倒也没什么问题,但是有三个函数,在这种设计之下,实用性大打折扣。这三个函数是

1
2
3
复制代码array array_map ( callable $callback , array $array1 [, array $... ] )
mixed array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] )
array array_filter ( array $array [, callable $callback [, int $flag = 0 ]] )

举个栗子,把一个数组中的数求平方,并且把平方后大于100的数相加,用普通的写法是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码$arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];

function foo_a($num_arr) {
$sum = 0;

foreach ($num_arr as $n) {
$v = $n * $n;
if ($v > 100) {
$sum += $v;
}
}

return $sum;
}
echo foo_a($arr) . "\n";

如果是简单的加减乘除,这种写法倒也OK,如果是比较复杂的逻辑,每一步的操作都会提出出来封装成相应的函数。我们来试试函数式写法,

1
2
3
4
5
6
7
8
9
10
复制代码function foo_b($num_arr) {
return array_sum(
array_filter(
array_map(function ($v) { return $v * $v; }, $num_arr), function($v){
return $v > 100;
})
);
}

echo foo_b($arr) . "\n";

看起来可读性比较差,也比较丑陋,这都拜PHP数组函数设计不合理所赐。假如可以这么写

1
2
3
4
5
复制代码function foo_c($num_arr) {
return $num_arr.map(function ($v) { return $v * $v;})
.filter(function ($v) {return $v > 100;})
.sum()
}

可读性和实用性是不是提高了很多?只要把map/filter/reduce这3个定义成数组的方法,并且返回数组,就可以这么写了,能不能再简洁一点呢?我们继续

1
2
3
4
5
复制代码function foo_c($num_arr) {
return $num_arr.map ($v -> $v * $v)
.filter($v -> $v > 100)
.sum()
}

有人可能要说我抄袭了,这不就是Java 8的lambda了嘛,对的,这就是Java 8的lambda。Java 8那点儿语法糖就吃饱了吗?显然不够,我们还可以再进一步简化下去。

1
2
3
4
5
复制代码function foo_c($num_arr) {
return $num_arr.map {$it * $it}
.filter {$it > 100}
.sum()
}

给只接受一个参数的lambda一个默认的参数,名字叫it,是不是又简洁了一些?还可以再继续吗?当然可以

1
2
3
复制代码function foo_c($num_arr) = $num_arr.map {$it * $it}
.filter {$it > 100}
.sum()

看到这里,可能已经有人看出来这是哪门语言的语法了,对的,就是它。

顺便拿PHP最喜欢比较的Python对比一下,感受一下list comprehension的魅力

1
复制代码sum([y for y in [x * x for x in num_arr] if y > 100])

不懂Python的人是这么写Python的,求平方写成这样,哈哈

1
复制代码list(map(lambda x: x * x, num_arr))

0x07, 函数命名风格太过不一致

PHP有nl2br这样的简写,还有htmlspecialchars_decode这样的长名字,据说当年PHP早期版本,用函数名字的长度作为hash,名字长度分布的均匀有助于减少hash冲突。听起来像是黑子们拿来喷PHP的,或者像PHP粉出来钓鱼的。但是看了这个

Re: Flexible function naming 我震惊了,PHP之父如是说

On 12/16/2013 07:30 PM, Rowan Collins wrote:

The core functions which follow neither rule include C-style
abbreviations like “strptime” which couldn’t be automatically swapped to
either format, and complete anomalies like
“nl2br”. If you named those
functions as part of a consistent style, you would probably also follow
stronger naming conventions than Rasmus did when he named
“htmlspecialchars”.

Well, there were other factors in
play there. htmlspecialchars was a
very early function. Back when PHP had less than 100 functions and the
function hashing mechanism was strlen(). In order to get a nice hash
distribution of function names across the various function
name lengths
names were picked specifically to make them fit into a specific length
bucket. This was circa late 1994 when PHP was a tool just for my own
personal use and I wasn’t too worried about not being able to remember
the few
function names.

-Rasmus

竟然是真的,太惊人了。据说后来到了PHP3的时候,替换掉了这个设计。而PHP在命名一致化的路上也一直在努力,但是考虑到兼容性,彻底解决可能还需要很多年的努力。

0x08, magic_quotes…

自动给你把GPC(GET/POST/COOKIE)变量中的特殊字符转义掉,幸好PHP 5.4已经删除这个特性了,不过有的比较传统的框架还保留着这个功能。我就想问问,你知道我要怎么用这些值吗?你知道哪些字符在我这边算特殊字符?自作主张一刀切,跟怕染HIV挥刀自宫的思路是一致的。再举个跟这个算是同类的栗子,配置对运行时行为影响过多过于复杂。

1
复制代码@fopen('http://example.com/not-existing-file', 'r');

很简单的一行代码,然而,它的行为却依赖诸多环境配置

如果PHP使用 –disable-url-fopen-wrapper编译, 它將不工作. (文档没有说, “不工作”是什么意思; 返回 null, 抛出异常?)
注意这点已在 PHP 5.2.5 中移除.
如果 allow_url_fopen 在 php.ini 中禁用, 也將不工作. (为什么? 无从得知.)
由于 @ , non-existent file 的警告將不打印.
但如果在php.ini中设置了scream.enabled, 它又將打印.
或者如果用
ini_set 手动设置 scream.enabled.
但, 如果 error_reporting 级别没设置, 又不同.
如果打印出来了, 精确去向依赖于 display_errors , 再一次还是在 php.ini. 或者 ini_set中.

最好的语言,隐藏了最多的黑魔法。要避开这个坑,只能尽量保证所有环境下面,编译参数一致,配置参数一致。

0x09, Error和Exception完全不同的机制

PHP 错误 (内部, 称为 trigger_error)不能被 try/catch 捕获。
同样, 异常不能通过 set_error_handler 安装的错误处理器触发错误。
作为替代, 有一个单独的 set_exception_handler 可以处理未捕获的异常。
Fatal 错误 (例如, new ClassDoesntExist()) 不能被任何东西捕获,大量的完全无害的操作会抛出 fatal 错误, 由 于一些有争议的原因被迫终结你的程序。

以上,一般框架层面会帮你解决,应用层面不需要操太多心。

0x0A, 更多的坑

eev.ee/blog/2012/0… 老外的吐槽,不过它的版本比较低,有些问题已经解决了,英文不好的看译文,五大受损, 全面解析PHP的糟糕设计 - 开源中国社区

实际上我提到的第8个和第9个坑,也在上面的文章中有提到,我就复制了来,其它的我觉得不深的坑,我倒觉得无所谓,没那么严重。

诚然,PHP的坑再多,只要用的人水平够高,也是可以写出完全正确的代码来的,然而我们大部分人都是普通人,坑的存在,或多或少都是负面影响。很多工作了5年以上的PHPer,也还会不留神掉到这些坑里。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

初识Spring —— Bean的装配(一)

发表于 2018-06-09

这篇文章来自我的博客

正文之前

终于开始学 Spring 了,接触到的第一个内容就是 Bean,所以想要用两篇文章来总结一下自己所学的

第一篇有两个内容:

  1. JavaBean 的概念
  2. Spring 装配 Bean

正文

1. JavaBean 的概念

JavaBean 是什么

这里的 Bean 当然不是直译过来的意思,关于Bean的定义,维基百科解释的很清楚了:

JavaBeans 是 Java 中一种特殊的类,可以将多个对象封装到一个对象(bean)中,特点是可序列化,提供无参构造器,提供 getter 方法和 setter 方法访问对象的属性。名称中的 “Bean” 是用于Java的可重用软件组件的惯用叫法

Bean的例子
  1. 创建Bean类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码import java.io.Serializable;

public class UserBean implements Serializable {
//变量声明为私有
private String play;
private Boolean isUser;

//无参构造器
public UserBean(){}

//常规 set、get操作
public void setPlay(String play) { this.play = play; }

public String getPlay() { return play; }

public void setUser(Boolean user) { isUser = user; }

//布尔类型的 getter 采用了不同的名称,用 is 更加能表示类型
public Boolean isUser() { return isUser; }
}
  1. 使用Bean类
1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码public class UserBeanTest {
public static void main(String[] args){
//实例化
UserBean userBean = new UserBean();
userBean.setPlay("I want to play");
userBean.setUser(true);

//如果是用户,就打印语句
if(userBean.isUser()){
System.out.println(userBean.getPlay());
}
}
}

引用《Spring实战》(Spring in action) 中的一句话:

在传统的Java应用中,bean的生命周期很简单。使用Java关键字new进行bean实例化,然后该bean就可以使用了。一旦该bean不再被使用,则由Java自动进行垃圾回收

2. Spring 中的 Bean

在 Spring 中,我们对 Bean 进行配置并加以使用,来得到我们需要的结果

在上述的例子中,如果我们要创建一个 Bean,我们就要使用关键字 New,但是,在 Spring 中,Bean 的创建是由 Spring 容器进行的,也就是说,在 Spring 中使用 Bean 的时候,不是由关键字 New 来创建实例了

今天主要说说 Spring 中单个 Bean 的装配,只有一个 Bean,下一篇文章会讲述 DI(依赖注入):

  1. 自动装配
  2. 使用 XML 装配
  3. 使用 Java 装配
1. 自动装配

引用《Spring实战》中的一句话:尽管你会发现这些显示装配技术非常有用,但是在便利性方面,最强大的还是 Spring 的自动化配置

我们用一个 Demo 来做解释,首先创建两个类:Student 类和 StudentTest 类,以及一个配置文件 spring-config.xml:

我们要先编写配置文件,使其启用组件扫描,来发现带有注解 @Component 注解的类,因为我之前由写过关于 Spring MVC 的 demo,所以有关 @Component、 @Service 和 @Controller 的区别做一个小小的解释(个人理解):

@Component 泛指组件类,就是在自身定位不明确的时候使用,比如我们这个Demo
@Service 表明这是业务层的组件
@Controller 表明这是控制层的组件
在业务层和控制层也可以用 @Component,但是这会导致代码不易读

回到配置文件:

这一行:

1
复制代码<context:component-scan base-package="bean"/>

指示了 Spring 要扫描的组件所处于的位置(包名),如果不设定 base-package,则扫描配置文件所处于的包

然后我们看一下 Student 类:

这里只是为了测试,所以极大程度地简化,只需要一句控制台输出语句

由于我们之前配置了组件扫描,所以点击旁边的标志,就能够跳转到我们添加了 @Component 注解的 Student 类

万事俱备,就差测试了,我们使用 Junit 来进行测试,需要导入两个 JAR:

junit-4.12.jar
hamcrest-all-1.3.jar

针对测试文件中的内容一步一步来:

  • @RunWith:这个单元测试指定的执行类(目前还不是很了解)
  • @ContextConfiguration:配置文件的位置,因为我是用 XML 文件进行配置,所以填写配置文件位置
  • @Autowired:自动装配 Bean,在这里可以看到没有使用 New 关键字,但是能够在运行时得到这个 Bean
  • @Test:运行测试的方法

能够看到控制台输出了 Student 类中的语句,证明装配成功

2. 使用 XML 装配

这一篇文章讲述的只是简单的装配,所以有很多有关依赖注入的方式都将跳过,由下一篇文章来讲

所以这里只演示在 XML 文件中创建一个 Bean 并进行装配

首先我们新建一个配置文件 spring-bean.xml:

在其中创建用元素创建一个叫做“student”的 Bean,class属性用来指定 Bean 的类型

然后我们修改测试文件中的上下文的配置信息:

我们将 @ContextConfiguration 注解的值改为了配置 Bean 的文件

输出结果:

证明装配成功

3. 使用 Java 装配

在《Spring实战》中大力推崇使用 Java 作为装配,可能是我不太习惯,之前都是把配置信息写在 XML 文件里,不过这里还是要说一下:

我们将创建一个类,作为存放配置信息的类,先加上 @Configuration,说明这是一个配置类:

接下来,我们要在配置类中使用一个注解:@Bean:

我们在配置类中写一个方法,这个方法能够创建所需要的实例并将其返回,并将 @Bean 添加至这个方法,这个方法的名称是作为 Bean 默认的名称:

然后我们要修改测试类,将配置上下文的文件改为 StudentConfig.class,表明 Java 配置类的位置:

然后得出测试结果,证明装配成功:

关于刚才说到的方法的名称是作为 Bean 默认的名称,这里做个测试:

先修改配置类中的方法名,加一个字符“s”:

然后修改测试类,在这个测试中,我们没有创建实例,只是测试一下看看能不能在上下文中发现这个 Bean :

测试后,抛出 NoSuchBeanDefinitionException,表示没有发现名叫“student”的 Bean

如果我把刚才修改的方法名改回来,就会通过测试:

关于最基础的 Bean 的装配就说到这里了,下一篇文章中将会详细讲述这三种方式的细节

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

谈谈如何抓取ajax动态网站

发表于 2018-06-09

什么是ajax呢,简单来说,就是加载一个网页完毕之后,有些信息你你还是看不到,需要你点击某个按钮才能看到数据,或者有些网页是有很多页数据的,而你在点击下一页的时候,网页的url地址没有变化,但是内容变了,这些都可以说是ajax。如果还听不懂,我给你看看百度百科的解释吧,下面就是。

Ajax 即“Asynchronous Javascript And XML”(异步 JavaScript 和 XML),是指一种创建交互式网页应用的网页开发技术。

Ajax = 异步 JavaScript 和 XML(标准通用标记语言的子集)。

Ajax 是一种用于创建快速动态网页的技术。

Ajax 是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。 [

通过在后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。

传统的网页(不使用 Ajax)如果需要更新内容,必须重载整个网页页面。

下面说下例子,我抓取过的ajax网页最难的就是网易云音乐的评论,感兴趣的可以看看利用python爬取网易云音乐,并把数据存入mysql

这里的评论就是ajax加载的,其他的那个抓今日头条妹子图片的也算是ajax加载的,只不过我把它简单化了。还有很多,就不说了,说下我今天要说的ajax网站吧!

http://www.kfc.com.cn/kfccda/storelist/index.aspx

这个是肯德基的门面信息

这里有很多页数据,每一页的数据都是ajax加载的。如果你直接用python请求上面那个url的话,估计什么数据都拿不到,不信的话可以试试哈。这时候,我们照常打开开发者工具。先把所有请求清楚,把持续日志打上勾,然后点击下一页,你会看到

上面那个请求就是ajax请求的网页,里面就会有我们需要的数据,我们看看是什么样的请求

是个post请求,请求成功状态码为200,请求url上面也有了,下面的from data就是我们需要post的数据,很容易就可以猜到pageIndex就是页数,所以我们可以改变这个值来进行翻页。

这个网页就分析完了,这样就是解决ajax动态网页了,是不是觉得很简单,其实不是的,只是这个网页比较简单的,因为表单(from data)的数据并没有进行加密,如果进行加密的话估计你的找js文件看看参数是怎样加密的了,这就是我之前写的网易云音乐评论的爬取。看这些混淆的js寻找加密方法的话有时会让你很头痛,所以经常有人会选择用selenium这些来进行爬取,但是用这些会使爬虫的性能降低,所以这个方法在工作里是不允许的。所以必须学会怎样应对这些ajax。

贴下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码import requests  
page = 1
while True:
   url = 'http://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?op=cname'
   data = {
           'cname': '广州',
           'pid': '',
           'pageIndex': page,
           'pageSize': '10'
   }
   response = requests.post(url, data=data)
   print(response.json())
   if response.json().get('Table1', ''):
       page += 1
   else:
       break

可以看到去掉from data,不用十行代码就可以把数据都爬下来了,所以说这个网站适合练手,大家可以去试试。

写在最后

下篇文章我会写下复杂点的ajax请求,这个网站

http://drugs.dxy.cn/

不知道有多少人想看,想看的话点个赞吧!或者你可以自己先试试哈

推荐文章

如何爬取asp动态网页?搞定可恶的动态参数,这一文告诉你!

利用python爬取网易云音乐,并把数据存入mysql

日常学python

代码不止bug,还有美和乐趣

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Cloud Zuul中使用Swagger汇总A

发表于 2018-06-05

有很多读者问过这样的一个问题:虽然使用Swagger可以为Spring MVC编写的接口生成了API文档,但是在微服务化之后,这些API文档都离散在各个微服务中,是否有办法将这些接口都整合到一个文档中?之前给大家的回复都只是简单的说了个思路,昨天正好又有人问起,索性就举个例子写成博文供大家参考吧。

如果您还不了解Spring Cloud Zuul和Swagger,建议优先阅读下面两篇,有一个初步的了解:

  • Spring Cloud构建微服务架构:服务网关(基础)
  • Spring Boot中使用Swagger2构建强大的RESTful API文档

准备工作

上面说了问题的场景是在微服务化之后,所以我们需要先构建两个简单的基于Spring Cloud的微服务,命名为swagger-service-a和swagger-service-b。

下面只详细描述一个服务的构建内容,另外一个只是名称不同,如有疑问可以在文末查看详细的代码样例。

  • 第一步:构建一个基础的Spring Boot应用,在pom.xml中引入eureka的依赖、web模块的依赖以及swagger的依赖(这里使用了我们自己构建的starter,详细可点击查看)。主要内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.10.RELEASE</version>
<relativePath/>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>com.spring4all</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
<version>1.7.0.RELEASE</version>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
  • 第二步:编写应用主类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码@EnableSwagger2Doc
@EnableDiscoveryClient
@SpringBootApplication
public class Application {

public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}

@RestController
class AaaController {
@Autowired
DiscoveryClient discoveryClient;

@GetMapping("/service-a")
public String dc() {
String services = "Services: " + discoveryClient.getServices();
System.out.println(services);
return services;
}
}
}

其中,@EnableSwagger2Doc注解是我们自制Swagger Starter中提供的自定义注解,通过该注解会初始化默认的Swagger文档设置。下面还创建了一个通过Spring MVC编写的HTTP接口,用来后续在文档中查看使用。

  • 第三步:设置配置文件内容:
1
2
3
4
5
6
复制代码spring.application.name=swagger-service-a
server.port=10010

eureka.client.serviceUrl.defaultZone=http://eureka.didispace.com/eureka/

swagger.base-package=com.didispace

其中,eureka服务端的配置采用了本站的公益eureka,大家可以通过eureka.didispace.com/查看详细以及使用方法。另外,swagger.base-package参数制定了要生成文档的package,只有com.didispace包下的Controller才会被生成文档。

注意:上面构建了swagger-service-a服务,swagger-service-b服务可以如法炮制,不再赘述。

构建API网关并整合Swagger

在Spring Cloud构建微服务架构:服务网关(基础)一文中,已经非常详细的介绍过使用Spring Cloud Zuul构建网关的详细步骤,这里主要介绍在基础网关之后,如何整合Swagger来汇总这些API文档。

  • 第一步:在pom.xml中引入swagger的依赖,这里同样使用了我们自制的starter,所以主要的依赖包含下面这些:
1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>com.spring4all</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
<version>1.7.0.RELEASE</version>
</dependency>
  • 第二步:在应用主类中配置swagger,具体如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
复制代码@EnableSwagger2Doc
@EnableZuulProxy
@SpringCloudApplication
public class Application {

public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}

@Component
@Primary
class DocumentationConfig implements SwaggerResourcesProvider {
@Override
public List<SwaggerResource> get() {
List resources = new ArrayList<>();
resources.add(swaggerResource("service-a", "/swagger-service-a/v2/api-docs", "2.0"));
resources.add(swaggerResource("service-b", "/swagger-service-b/v2/api-docs", "2.0"));
return resources;
}

private SwaggerResource swaggerResource(String name, String location, String version) {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion(version);
return swaggerResource;
}
}
}

说明:@EnableSwagger2Doc上面说过是开启Swagger功能的注解。这里的核心是下面对SwaggerResourcesProvider的接口实现部分,通过SwaggerResource添加了多个文档来源,按上面的配置,网关上Swagger会通过访问/swagger-service-a/v2/api-docs和swagger-service-b/v2/api-docs来加载两个文档内容,同时由于当前应用是Zuul构建的API网关,这两个请求会被转发到swagger-service-a和swagger-service-b服务上的/v2/api-docs接口获得到Swagger的JSON文档,从而实现汇总加载内容。

测试验证

将上面构建的两个微服务以及API网关都启动起来之后,访问网关的swagger页面,比如:http://localhost:11000/swagger-ui.html,此时可以看到如下图所示的内容:

可以看到在分组选择中就是当前配置的两个服务的选项,选择对应的服务名之后就会展示该服务的API文档内容。

代码示例

本文示例读者可以通过查看下面仓库的中的swagger-service-a、swagger-service-b、swagger-api-gateway三个项目:

  • Github
  • Gitee

如果您对这些感兴趣,欢迎star、follow、收藏、转发给予支持!

开源项目

针对本文开发一个简化的开源项目,欢迎Star支持:swagger-butler

以下专题教程也许您会有兴趣
  • Spring Boot基础教程
  • Spring Cloud基础教程

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Golang 我对变量产生了这些想法 变量名:程序员给地

发表于 2018-06-04

最近在学习Golang的过程中,发现一个有意思的事情,有的文章说函数调用传参时 slice 是引用传递,有的说是值传递。为什么同一个东西大家会不同认识?为了搞清楚其本质,我进行了以下内容的研究:

  1. 变量的变量名、变量值、变量地址在内存中是怎么样的?
  2. 指针的定义是什么?引用的定义是什么?二者有什么关系?
  3. 函数传参中值传递、指针传递与引用传递到底有什么不一样?
  4. Go中 slice 在传入函数时到底是不是引用传递?如果不是,在函数内为什么能修改其值?

为了避免文章写的过长,看了想瞌睡,分成两篇文章来解释这个问题,本文先解决问题1跟2,下一篇说明余下的问题。

变量名:程序员给地址取的外号

上学的时候,老师讲变量是存在内存中的,内存就像一排排抽屉组成的,每个抽屉上面有个编号,我们定义一个变量,就是把想放的东西放到这个对应编号的抽屉里。比如: int a = 10,用图来表示下:

这里:变量的名字叫 a ,变量的值是:10,变量的地址是:0x 00000001。那么问题来了,变量的值我们知道是放在了抽屉里(内存中),每个抽屉有编号(地址),但是变量的名字 a 存放在哪里呢?或者说它会存在于内存中吗?

大家想一个问题,如果变量的名字要存放在内存中,那么肯定分配一个空间给它,保存它的空间有个地址,这个地址是不是又得有个地方存起来程序才能找到?如果真是这样设计,那么代码根本没发写、无法运行了。

其实变量名仅仅是写给程序员看的,让我们写代码的时候知道这个变量有什么用,能够通过名字调用变量的值。因为如果直接给你一个地址 0x 23004123,你知道这是要干嘛吗?代码经过编译后,最终都会转换成机器码,我们定义的变量名就都不存在了,存在的只有地址跟值。

指针其实很普通

有了上面的理解,再来一个特殊的变量:指针变量。什么叫指针变量呢?其实就是这个变量里边存放的是一个变量的地址,通过这个地址,机器可以找到对应变量的值,例如:int * pa = &a,就表示变量 pa 抽屉里放的是 a 的地址,它的类型是:int*,继续看图:

这里需要重要说明的是:指针pa与a的关系是:a抽屉里边放的是变量值10,pa放的是变量的地址:0x00000001,这里一定要记住,下面说引用的时候才更容易理解。

引用就是变量的另一名字

继续谈引用,引用与指针我们经常傻傻分不清,因为它们的行为确实非常诡异,看起来效果非常相似,看代码:

由于引用的概念是在 c++ 中引入的,因此下面的代码使用c++,仅仅是一些打印而已,放心看下去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
perl复制代码int main() {    int a = 10;// 变量
   int * pa = &a; // 指针
   int & b = a; // 引用

   printf("a: %d\n", a);// a: 10
   printf("*pa: %d\n", *pa);// *pa: 10
   printf("b: %d\n", b);// b: 10

   *pa = 20;    printf("a: %d\n", a);// a: 20
   printf("*pa: %d\n", *pa);// *pa: 20
   printf("b: %d\n", b);// b: 20

   b = 30;    printf("a: %d\n", a);// a: 30
   printf("*pa: %d\n", *pa);// *pa: 30
   printf("b: %d\n", b);// b: 30

   a = 40;    printf("a: %d\n", a);// a: 40
   printf("*pa: %d\n", *pa);// *pa: 40
   printf("b: %d\n", b);// b: 40
   return 0;
}

通过上面的代码我们发现,指针与引用都能达到一个效果:都有能力修改a的值,指针前面讲过了,因为它保存了a的地址,通过解引用操作后,实际上就是打开了a的抽屉,因此可以进行修改。那么引用又是怎么办到的?这里注意一个细节:*pa = 20; c = 30;a = 40。我们看到操作c的时候与操作a是一样的方式:直接使用变量名,但是pa要想改变a的值,必须进行 *pa 操作(解引用),如果直接 pa=20,这仅仅是改变的pa的值,让他指向了另外一个地址。

为什么引用与变量是一样的操作方式?先来看一下引用的定义:

引用就是某一变量的一个别名,对引用的操作与对变量直接操作完全一样。

那么别名是什么意思呢?

看到了吧?a就是b,b就是a。系统并不会为引用额外分配空间进行存储,甚至可以简单理解为:这个别名仅仅是为了给程序员看的,到机器码层面的时候,他们都会变成地址:0x 00000001。

有码为证

通过上面的分析不知道你理解了几分?或者你是不是对指针与引用还是半信半疑?没关系,写点代码证明一下即可,我们要证明的是:

  • 引用是变量的别名,那么它的地址应该与变量一致;
  • 指针保存的是变量的地址,那么它的值是变量的地址,它自身的地址与变量不同。

为了证明,程序设计如下:定义一个变量,分别赋值给指针、引用,然后检查他们对应的值与地址。

1
2
perl复制代码int main() {    int a = 10;    printf("%d\n", a);    printf("%p\n", &a);        printf("~~~~~~~~~~~~~~\n");    int * b = &a;    printf("%p\n", b);    printf("%p\n", &b);        printf("~~~~~~~~~~~~~~\n");    int & c = a;    printf("%d\n", c);    printf("%p\n", &c);        return 0;
}

获得输出:

1
2
3
4
5
6
7
8
asciidoc复制代码10 // 变量a的值
0x7ffee3c7a768 // 变量a的地址
~~~~~~~~~~~~~~
0x7ffee3c7a768 // 指针的值,是变量a的地址
0x7ffee3c7a760 // 指针变量自己的地址
~~~~~~~~~~~~~~
10 // 变量a的值
0x7ffee3c7a768 // 引用变量c的地址,与变量a的地址完全一样

在上面如果指针想要打印变量a的值,需要解引用操作:printf(“%d\n”, *b);

小结

  • 变量由三分部分构成:变量名、变量值、变量地址;
  • 变量名实际上只是给程序员看的,编译后的代码中并不存在变量名;
  • 指针变量就是一个变量存储了另外一个变量的地址,系统也会为他分配内存空间来存储这个地址;
  • 引用实际是变量的别名,他跟变量有相同的地址。

下篇预告:

  1. 函数传参中值传递、指针传递与引用传递到底有什么不一样?
  2. 为什么说 slice 是引用类型?
  3. Go中 slice 在传入函数时到底是不是引用传递?如果不是,在函数内为什么能修改其值?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java 9种排序算法详解和示例汇总

发表于 2018-06-02

冒泡排序、选择排序、直接插入排序、二分法排序、希尔排序、快速排序、堆排序、归并排序、基数排序,共9中排序算法详解和代码示例。

排序算法

示例中全部采用从小到大排序,编码方式为本人理解的思路,算法思想也是自己理解的口语表达方式,若想查看更准确的算法思想和代码示例可直接搜索各算法的百科

示例源码地址

一、冒泡排序

1、算法思想

  1. 两两比较,如果后者比前者大则交换位置
  2. 每遍历一圈最大的数就会冒到最后,则确定了本轮比较中的最大值放到最后不动
  3. 循环1、2直至遍历完所有

2、代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码private int[] array = {23, 11, 7, 29, 33, 59, 8, 20, 9, 3, 2, 6, 10, 44, 83, 28, 5, 1, 0, 36};

/**
* 冒泡排序:两两比较,大者交换位置,则每一圈比较最大的数就会冒到最后,循环直至遍历完所有
*/
private void bubbleSort() {

for (int i = 0; i < array.length - 1; i++) {
for (int j = 0; j < array.length - i - 1; j++) {
if (array[j] > array[j + 1]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}

}
  • 时间复杂度O(n²)

二、选择排序

1、算法思想

  1. 找到所有数中最大值下标
  2. 找到最大值的下标与最后一个位置的数值交换位置,这样每次找到的最大值则固定到最后
  3. 循环1、2操作直至遍历找到所有

2、代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
复制代码private int[] array = {23, 11, 7, 29, 33, 59, 8, 20, 9, 3, 2, 6, 10, 44, 83, 28, 5, 1, 0, 36};

/**
* 选择排序:找到当前数中最大的数字,找到后与最后一个位置的数字交换位置,直至循环遍历完所有的数为止
*/
private void selectSort() {

for (int i = 0; i < array.length; i++) {

// 定义最大数字的下标,默认为0
int max = 0;
for (int j = 0; j < array.length - i; j++) {

// 找到比自己大的数就更新下标
if (array[max] < array[j]) {
max = j;
}
}

// 将找到最大的数与最后一个数字交换位置
int temp = array[array.length - i - 1];
array[array.length - i - 1] = array[max];
array[max] = temp;
}

}
  • 时间复杂度O(n²),但是由于选择排序每轮比较只交换一次,所以实际性能要优于冒泡

三、直接插入排序

1、算法思想

  1. 从位置1的数值n开始,将前面已经遍历过的数值集合看成数组m,则将n往m中插入
  2. n插入到集合m中时从后往前比较,如果比n大则往后移一位,如果比较到比n小,则当前位置就是插入n的位置
  3. 通过1、2的操作则可以保证每次插入n后m的集合都是排好的序列
  4. 循环1、2、3操作将集合中所有数值均插入一遍即排序完成

2、代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码private int[] array = {23, 11, 7, 29, 33, 59, 8, 20, 9, 3, 2, 6, 10, 44, 83, 28, 5, 1, 0, 36};

/**
* 直接插入排序:从1开始遍历数组,每个数字都在前面已经遍历的数字中插入
* 从小到大排序的话碰到比它大的则往后移,直到比它小为止
*/
private void insertSort() {
for (int i = 1; i < array.length; i++) {
int temp = array[i];
int j;
// 在前面已经遍历过的数字中比较若小于则往后移
for (j = i - 1; j >= 0; j--) {
if (temp < array[j]) {
array[j + 1] = array[j];
} else {
break;
}
}
array[j + 1] = temp;
}
}
  • 时间复杂度O(n²)

四、二分法排序

  • 二分法排序是直接插入排序的改进版本,直接插入排序插入到前方集合中时采用的方式是逐个比较,二分法则是采用二分比较

1、算法思想

  1. 从位置1的数值为n,将前面已经遍历过的数值集合看成数组m,则将n往m中插入
  2. n插入到集合m中时采用二分法,先比较m中中间的数值,如果比n大则继续比较后面一半集合的中间的数值,直至比较到拆分的集合中左边一半或者右边一半没有值为止,则当前中间值的位置即为n插入到m中的位置
  3. 通过1、2的操作则可以保证每次插入n后m的集合都是排好的序列
  4. 循环1、2、3操作将集合中所有数值均插入一遍即排序完成

2、代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
复制代码private int[] array = {23, 11, 7, 29, 33, 59, 8, 20, 9, 3, 2, 6, 10, 44, 83, 28, 5, 1, 0, 36};

/**
* 二分插入排序:从1开始遍历,已经遍历的数组中头是left,尾是right,遍历到的数字与中间的数字对比
* 若小于中间的数字则right变更成中间数字前面的一个数字,反之则变更left
* 直至最后left>right则插入
*/
private void binaryInsertSort() {
for (int i = 1; i < array.length; i++) {
int temp = array[i];
int left = 0, right = i - 1;
int mid;
while (left <= right) {
mid = (left + right) / 2;
if (temp < array[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
// 将遍历到比他大的数字全部往后移一位
for (int j = i - 1; j >= left; j--) {
array[j + 1] = array[j];
}
array[left] = temp;
}
}
  • 时间复杂度O(nlogn)

五、希尔排序

1、算法思想

  1. 定义一个增量m,集合的长度为n,则将集合拆分成n/m组,每组内部进行比较排序
  2. 每组内比较的方法无要求,可以用插入或者二分法都行
  3. 假如要排序一段集合为{4,1,2,3},定义m为2,则拆分成两组两两比较,即为4和2比,1和3比
  4. 因此按照1、2的思路每比较一次都可以将m组内的数值排序
  5. 不断变化m的值,多次分组遍历之后即可排序

2、代码示例

  • m的变化方式有多种,不同的变化方式可能排序结果和效率不同。此处示例采用的方式是m=m/2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
复制代码private int[] array = {23, 11, 7, 29, 33, 59, 8, 20, 9, 3, 2, 6, 10, 44, 83, 28, 5, 1, 0, 36};

/**
* 希尔排序:定义一个增量m,比较的数字集合总数为n,则将集合分成n/m组,每组进行插入排序
* 随后m递减,多次比较之后就可得出排序后的集合
*/
private void shellSort() {
int m = array.length;
while (true) {
// 本次增量的变化方式为 m/2
m = m / 2;
// 分组后的数组下标为n/m的摩
for (int i = 0; i < m; i++) {
// 分组后数组的数据为原数组下标摩为i的数
for (int j = i + m; j < array.length; j += m) {
// 每组内部进行插入排序(此处使用直接插入排序方式,也可使用二分法插入)
int temp = array[j];
int k;
// 在前面已经遍历过的数字中比较若小于则往后移
for (k = j - m; k >= i; k -=m) {
if (temp < array[k]) {
array[k + m] = array[k];
} else {
break;
}
}
array[k + m] = temp;
}
}

if (m == 1) {
break;
}
}
}
  • 时间复杂度O(nlogn)

六、快速排序

1、算法思想

  1. 快速排序的思想主要是先设置一个基准点m,这里我们假设每次设置的基准点都是每一组的第一个数值
  2. 拿着基准点m在集合中进行比较,找到它应该放置的位置
  3. 比较方式主要是定义集合中最左边的下标left,最右边的下标right,从左边开始比较,比m小则left++,找到比m大的则停住,将left下标的值赋值成right下标的值,然后同理比较right,比m大的则right–,找到比m小的就赋值成left下标的值。当left==right之后则比较完成
  4. 经过步骤3的比较之后则可以找到m点排序所在的位置,然后集合被分成前后两半,各自按照1、2、3的方式排序,递归至全部拆分比较完成后即排序完成
  • 由于步骤3思想较复杂一点,特此引用《啊哈!算法》一书中的插图演示一下,图中以第一个点6为基准点,找到6排序后应该所在的位置

快速排序

快速排序

快速排序

快速排序

2、代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
复制代码private int[] array = {23, 11, 7, 29, 33, 59, 8, 20, 9, 3, 2, 6, 10, 44, 83, 28, 5, 1, 0, 36};

/**
* 快速排序:找到某个点排序之后它应该所在的位置
*/
private void quickSort() {
quickSort(0, array.length - 1);
}

/**
* 找到开始和结束位置之间以第一个数为基数,这个基数应该所在的位置
* 找到之后以基数为中心点拆分成前后两段,依次递归进行本操作,直至最后遍历完所有基数为止
*
* @param low 开始的点下标
* @param high 结束的点下标
*/
private void quickSort(int low, int high) {
if (low >= high) {
return;
}
int mid = getMiddle(low, high);
quickSort(low, mid - 1);
quickSort(mid + 1, high);
}

/**
* 通过比较获取最开始基数最后所在的位置
*
* @param low 最开始的位置
* @param high 结束的位置
* @return 最后基数所在的位置
*/
private int getMiddle(int low, int high) {
int temp = array[low];
while (low < high) {
while (low < high && array[high] >= temp) {
high--;
}
array[low] = array[high];

while (low < high && array[low] <= temp) {
low++;
}
array[high] = array[low];
}
array[low] = temp;
return low;
}
  • 时间复杂度O(nlogn)

七、堆排序

1、算法思想

  1. 将数组构建成大堆二叉树,即所有节点的父节点的值都大于叶子节点的完全二叉树
  2. 若叶子节点比父节点大,则交换位置
  3. 根节点即为最大值,则将根节点与最后的的一个叶子节点交换位置
  4. 重复1,2操作,每次都找最大值则放置最后即可排序完成
  • 由于堆排序运用到了完全二叉树的数据结构,较难理解,特地在网上找了个算法演示的图片参考

堆排序

2、代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
复制代码private int[] array = {23, 11, 7, 29, 33, 59, 8, 20, 9, 3, 2, 6, 10, 44, 83, 28, 5, 1, 0, 36};

/**
* 堆排序,将数组构建成大堆二叉树,即父节点比叶子节点大的二叉树
* 从小到大排序的话则每次直接将根节点放置到最后一位,循环往复直至遍历完所有为止
*/
private void heapSort() {

// 先构建一次大堆二叉树,做一个基本的排序
buildMaxHeap();

for (int i = array.length - 1; i > 0; i--) {
// 将最大值与最后一个位置的数交换
exchangeValue(0, i);

// 重新构建大堆二叉树,从0开始往下检测是否需要重新构建大堆
maxHeap(i, 0);
}

}


/**
* 构建大堆二叉树,从最底层开始往上构建,最底层的父节点则是总长度的一半
*/
private void buildMaxHeap() {
int length = array.length;
for (int i = length / 2 - 1; i >= 0; i--) {
maxHeap(length, i);
}
}


/**
* 构建大堆二叉树的节点,若修改了顺序,则递归重新构建下一层
*
* @param length 构建数据数组长度
* @param node 构建堆排序的父节点
*/
private void maxHeap(int length, int node) {
int left = 2 * node + 1;
int right = 2 * node + 2;
// 找到一个节点和他的孩子节点中的最大值下标
int maxIndex = node;
if (left < length && array[left] > array[maxIndex]) {
maxIndex = left;
}
if (right < length && array[right] > array[maxIndex]) {
maxIndex = right;
}

// 如果不是父节点最大,则跟最大的孩子节点交换
if (maxIndex != node) {
exchangeValue(node, maxIndex);
maxHeap(length, maxIndex);
}
}



/**
* 交换两个下标的数值
*
* @param first 第一个下标
* @param second 第二个下标
*/
private void exchangeValue(int first, int second) {
int temp = array[first];
array[first] = array[second];
array[second] = temp;
}
  • 时间复杂度O(nlogn)

八、归并排序

1、算法思想

  1. 将数据集合两分拆开
  2. 循环拆分至每组只剩一个为止
  3. 将拆分的数组进行排序组合
  4. 两两合并,直至合并成一个数组即排序完成
  • 算法思想参考下图
    归并排序

2、代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
复制代码private int[] array = {23, 11, 7, 29, 33, 59, 8, 20, 9, 3, 2, 6, 10, 44, 83, 28, 5, 1, 0, 36};

/**
* 归并排序:将数据集合两分拆开,直至最小之后两两排序合并
*/
private void mergeSort() {
int[] temp = new int[array.length];
mergeSort(temp, 0, array.length - 1);
}


/**
* 查分数组,如果数组不能拆分了,则直接返回,拆分之后合并
*/
private void mergeSort(int[] temp, int start, int end) {

if (start >= end) {
return;
}

int mid = (start + end) / 2;
mergeSort(temp, start, mid);
mergeSort(temp, mid + 1, end);
mergeArray(temp, start, mid + 1, end);
}


/**
* 将数组array,以mid为中心,前后两个数组进行合并
*/
private void mergeArray(int[] temp, int start, int mid, int end) {

// 定义指针下标,记录前后段是够可以继续移动
int minA = start, minB = mid;
for (int i = start; i <= end; i++) {
if (minA >= mid || minB > end) {
// 如果a或者b用完了,则直接用对方的
if (minA >= mid) {
temp[i] = array[minB];
minB++;
} else {
temp[i] = array[minA];
minA++;
}
} else {
// 都没用完则比较大小
if (array[minA] < array[minB]) {
temp[i] = array[minA];
minA++;
} else {
temp[i] = array[minB];
minB++;
}
}
}

System.arraycopy(temp, start, array, start, end - start + 1);
}
  • 时间复杂度O(nlogn)

九、基数排序

1、算法思想

  1. 基数排序又称桶排序,具体思想就是将数值当成数组的下标保存
  2. 将所有数值拿出个位来比较,例如值为m的就存入下标为m的数组中
  3. 将比较后的数组拿出即为按个位排序好的数组,再将这个排序好的数组按十位排序
  4. 比较完个十百千所有位数以后即排序完成
  • 步骤一思想参考图
    基数排序

2、代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
复制代码private int[] array = {23, 11, 7, 29, 33, 59, 8, 20, 9, 3, 2, 6, 10, 44, 83, 28, 5, 1, 0, 36};

/**
* 基数排序,先按个位将所有数字按照个位的值放入0-9的二维数组中,依次取出之后再按十位
* 如此循环直至个十百千等等所有位数遍历完为止
*/
private void radixSort() {

// 定义二位数组用来存储每个基数以及基数下的数值
int[][] temp;

// 定义一维数组记录基数下保存了几位
int[] position;

int radix = 1;

while (true) {
position = new int[10];
temp = new int[10][array.length];

for (int i = 0; i < array.length; i++) {
int value = (array[i] / radix) % 10;
temp[value][position[value]] = array[i];
position[value]++;
}

// 判断是否所有的数值都在0位上,都在0位上则表示排序完成
if (position[0] == array.length) {
break;
}

int index = 0;
for (int i = 0; i < 10; i++) {
for (int j = 0; j < position[i]; j++) {
array[index] = temp[i][j];
index++;
}
}

radix = radix * 10;
}
}
  • 基数排序的时间复杂度为O(d(n+r)),r为基数,d为位数

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…890891892…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%