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

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


  • 首页

  • 归档

  • 搜索

Spring IOC容器原理解析---初始化过程

发表于 2020-05-21

Spring是一个非常优秀的开源框架,对于Java开发人员来说,很多时候我们的开发工作都是围绕着它来进行的。因此我们不应该仅仅局限于会使用它,更应该去学习他的工作原理,这样当遇到问题时可以更好的定位与解决。

今天的文章主要结合源码来讲解SpringIOC的原理,也是自己这段时间的学习总结,分享出来,也希望能够对大家有那么些许的帮助。

使用的Spring源码版本为5.2.2.RELEASE,大家可自行去github下载。

1 BeanFactory和ApplicationContext

在Spring的IOC容器中主要有BeanFactory和ApplicationContext两条路径,这两者的区别如下:

  • BeanFactory是IOC容器的顶层接口,其定义了一个容器需要满足的最基本的功能。
  • ApplicationContext继承自BeanFactory,对容器进行了扩展(资源发现、事件支持、本地化),是高级的容器。

BeanFactory的类图如下:

ApplicationContext的类图如下:

通过上面的类图我们能够发现,BeanFactory路径下的容器实现类为DefaultListableBeanFactory。ApplicationContext路径的实现有如下:

  • ClassPathXmlApplicationContext 从classPath下加载配置
  • FileSystemXmlApplicationContext 从文件系统加载配置
  • XmlWebApplicationContext 加载配置至web容器

2 使用ClassPathXmlApplicationContext

我们以ClassPathXmlApplicationContext类的源码查看Spring IOC的初始化过程,在看源码之前我们先写一个demo,目录结构如下:

代码逻辑很简单,在application.xml中配置了一个User对象,然后创建ClassPathXmlApplicationContext并从中获取该对象,application.xml的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码<?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:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd"
default-autowire="byName" default-lazy-init="false">
<bean id="user" class="com.xh.spring.User">
<property name="age" value="18"/>
<property name="name" value="张三"/>
</bean>
</beans>

使用debug查看,我们可以看见创建后的容器对象如下:

通过上面的截图我们可以看到ClassPathXmlApplicationContext中的BeanFactory是一个DefaultListableBeanFactory对象,在该容器对象中有一个为beanDefinitionMap的ConcurrentHashMap实例,这个Map就是用来存储对象信息的,在BeanDefinition中存储了bean的描述,如下:

通过上面的例子我们能够看到我们指定一个xml的路径,即可在在创建好的容器容器中找到对该配置文件中配置的对象的描述信息,这便是Spring IOC需要完成的工作,概括如下:

  • 资源的定位
  • 将资源中的数据解析为一个个的BeanDefinition对象
  • 将这些BeanDefinition对象注册到一个Map中

3 源码解析

了解了Spring IOC容器初始化所完成的事情后,我们正式开始查看源码,通过源码来详细的了解这三个过程都是怎样实现的。

创建容器时,最终调用的ClassPathXmlApplicationContext构造方法如下所示,容器的初始化逻辑在调用的refresh()方法中实现

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码public ClassPathXmlApplicationContext(
String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
throws BeansException {

super(parent);
// 设置资源路径 用于之后的加载BeanDefinitions
setConfigLocations(configLocations);
if (refresh) {
// IOC容器初始化逻辑
// 包括对Resource定位、载入和注册的过程
refresh();
}
}

refresh()方法的定义在AbstractApplicationContext类中,其源码如下:

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
复制代码public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 容器初始化的准备工作,设置一些初始化数据 设置active标记
prepareRefresh();
// 创建IOC容器
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

IOC容器的初始化逻辑是在obtainFreshBeanFactory()方法中定义的,我们一步步点击进去,会进入AbstractRefreshableApplicationContext类中的refreshBeanFactory()方法,该方法的处理逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码@Override
protected final void refreshBeanFactory() throws BeansException {
// 关闭存在的容器
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
// 创建容器对象,使用的是DefaultListableBeanFactory
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
customizeBeanFactory(beanFactory);
// 加载BeanDefinitions
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}

上面代码的主要逻辑在loadBeanDefinitions(beanFactory)中,点进去会发现这个方法是调用的AbstractXmlApplicationContext类中的方法,源码如下

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
复制代码@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// 创建一个XmlBeanDefinitionReader对象
// 用来解析xml文件生成的resource对象
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);

beanDefinitionReader.setEnvironment(this.getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));

// Allow a subclass to provide custom initialization of the reader,
// then proceed with actually loading the bean definitions.
initBeanDefinitionReader(beanDefinitionReader);
loadBeanDefinitions(beanDefinitionReader);
}

protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
Resource[] configResources = getConfigResources();
if (configResources != null) {
reader.loadBeanDefinitions(configResources);
}
String[] configLocations = getConfigLocations();
if (configLocations != null) {
// 在我们的例子中最终会走该方法
reader.loadBeanDefinitions(configLocations);
}
}

不知大家是否还记得在我们创建ClassPathXmlApplicationContext对象的逻辑中,会调用一个setConfigLocations的方法。在上面的方法中会使用这个值进行资源的定位加载,我们点进去上面第二个if中调用的loadBeanDefinitions方法,会发现这是AbstractBeanDefinitionReader中的一个方法,其源码如下:

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
复制代码@Override
public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException {
Assert.notNull(locations, "Location array must not be null");
int count = 0;
for (String location : locations) {
count += loadBeanDefinitions(location);
}
return count;
}
@Override
public int loadBeanDefinitions(String location) throws BeanDefinitionStoreException {
return loadBeanDefinitions(location, null);
}
public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException {
ResourceLoader resourceLoader = getResourceLoader();
if (resourceLoader == null) {
throw new BeanDefinitionStoreException(
"Cannot load bean definitions from location [" + location + "]: no ResourceLoader available");
}

if (resourceLoader instanceof ResourcePatternResolver) {
// Resource pattern matching available.
try {
Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
int count = loadBeanDefinitions(resources);
if (actualResources != null) {
Collections.addAll(actualResources, resources);
}
if (logger.isTraceEnabled()) {
logger.trace("Loaded " + count + " bean definitions from location pattern [" + location + "]");
}
return count;
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"Could not resolve bean definition resource pattern [" + location + "]", ex);
}
}
else {
// Can only load single resources by absolute URL.
Resource resource = resourceLoader.getResource(location);
int count = loadBeanDefinitions(resource);
if (actualResources != null) {
actualResources.add(resource);
}
if (logger.isTraceEnabled()) {
logger.trace("Loaded " + count + " bean definitions from location [" + location + "]");
}
return count;
}
}

上面这段代码的逻辑是通过路径定位资源,并将资源转换为Resource对象,之后会调用BeanDefinitionReader中的loadBeanDefinitions(Resource resource)方法,我们使用的是XmlBeanDefinitionReader对象,继续点进去查看,其源码如下:

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
复制代码public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
Assert.notNull(encodedResource, "EncodedResource must not be null");
if (logger.isTraceEnabled()) {
logger.trace("Loading XML bean definitions from " + encodedResource);
}

Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (currentResources == null) {
currentResources = new HashSet<>(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
try {
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
// 真正开始加载BeanDefinitions的逻辑
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
finally {
inputStream.close();
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"IOException parsing XML document from " + encodedResource.getResource(), ex);
}
finally {
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}

在上面的代码中我们会发现在这里会调用一个doLoadBeanDefinitions的方法,这是真正处理加载BeanDefinitions的逻辑,其源码如下

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
复制代码protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {

try {
// 生成Document对象
Document doc = doLoadDocument(inputSource, resource);
// 注册BeanDefinition
int count = registerBeanDefinitions(doc, resource);
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + count + " bean definitions from " + resource);
}
return count;
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (SAXParseException ex) {
throw new XmlBeanDefinitionStoreException(resource.getDescription(),
"Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
}
catch (SAXException ex) {
throw new XmlBeanDefinitionStoreException(resource.getDescription(),
"XML document from " + resource + " is invalid", ex);
}
catch (ParserConfigurationException ex) {
throw new BeanDefinitionStoreException(resource.getDescription(),
"Parser configuration exception parsing XML from " + resource, ex);
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(resource.getDescription(),
"IOException parsing XML document from " + resource, ex);
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(resource.getDescription(),
"Unexpected exception parsing XML document from " + resource, ex);
}
}

从上面的代码可以发现,在Spring中是使用Document对象来解析配置的内容并进行BeanDefinition的注册工作,registerBeanDefinitions方法的源码如下

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
复制代码@Override
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
doRegisterBeanDefinitions(doc.getDocumentElement());
}
protected void doRegisterBeanDefinitions(Element root) {

BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);

if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
// We cannot use Profiles.of(...) since profile expressions are not supported
// in XML config. See SPR-12458 for details.
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
}

preProcessXml(root);
// 解析文档中的标签
parseBeanDefinitions(root, this.delegate);
postProcessXml(root);

this.delegate = parent;
}

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
parseDefaultElement(ele, delegate);
}
else {
// 解析用户自定义的标签
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
// import标签
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
importBeanDefinitionResource(ele);
}
// alias标签
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
processAliasRegistration(ele);
}
// bean标签
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
processBeanDefinition(ele, delegate);
}
// beans标签
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
// recurse
doRegisterBeanDefinitions(ele);
}
}

上面的代码是解析解析文档中的标签,这里我们查看下解析bean标签的逻辑,processBeanDefinition方法,源码如下

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
复制代码protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try {
// Register the final decorated instance.
// 注册BeanDefinition的逻辑
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
}
catch (BeanDefinitionStoreException ex) {
getReaderContext().error("Failed to register bean definition with name '" +
bdHolder.getBeanName() + "'", ele, ex);
}
// Send registration event.
getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
}
}
public static void registerBeanDefinition(
BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
throws BeanDefinitionStoreException {

// Register bean definition under primary name.
String beanName = definitionHolder.getBeanName();
// 注册BeanDefinition
// 调用的是DefaultListableBeanFactory中的方法
registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

// Register aliases for bean name, if any.
String[] aliases = definitionHolder.getAliases();
if (aliases != null) {
for (String alias : aliases) {
registry.registerAlias(beanName, alias);
}
}
}

最终会调用DefaultListableBeanFactory中的registerBeanDefinition方法,将BeanDefinition注册到容器中,使用容器中的beanDefinitionMap参数进行存储,这是一个ConcurrentHashMap对象,使用对象名称作为key,BeanDefinition作为值,可以跟我们先前例子的debug的结果是一致的。

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
复制代码@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {

Assert.hasText(beanName, "Bean name must not be empty");
Assert.notNull(beanDefinition, "BeanDefinition must not be null");

if (beanDefinition instanceof AbstractBeanDefinition) {
try {
((AbstractBeanDefinition) beanDefinition).validate();
}
catch (BeanDefinitionValidationException ex) {
throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
"Validation of bean definition failed", ex);
}
}

BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
if (existingDefinition != null) {
if (!isAllowBeanDefinitionOverriding()) {
throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
}
else if (existingDefinition.getRole() < beanDefinition.getRole()) {
// e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
if (logger.isInfoEnabled()) {
logger.info("Overriding user-defined bean definition for bean '" + beanName +
"' with a framework-generated bean definition: replacing [" +
existingDefinition + "] with [" + beanDefinition + "]");
}
}
else if (!beanDefinition.equals(existingDefinition)) {
if (logger.isDebugEnabled()) {
logger.debug("Overriding bean definition for bean '" + beanName +
"' with a different definition: replacing [" + existingDefinition +
"] with [" + beanDefinition + "]");
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("Overriding bean definition for bean '" + beanName +
"' with an equivalent definition: replacing [" + existingDefinition +
"] with [" + beanDefinition + "]");
}
}
this.beanDefinitionMap.put(beanName, beanDefinition);
}
else {
if (hasBeanCreationStarted()) {
// Cannot modify startup-time collection elements anymore (for stable iteration)
synchronized (this.beanDefinitionMap) {
this.beanDefinitionMap.put(beanName, beanDefinition);
List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);
updatedDefinitions.addAll(this.beanDefinitionNames);
updatedDefinitions.add(beanName);
this.beanDefinitionNames = updatedDefinitions;
removeManualSingletonName(beanName);
}
}
else {
// Still in startup registration phase
this.beanDefinitionMap.put(beanName, beanDefinition);
this.beanDefinitionNames.add(beanName);
removeManualSingletonName(beanName);
}
this.frozenBeanDefinitionNames = null;
}
if (existingDefinition != null || containsSingleton(beanName)) {
resetBeanDefinition(beanName);
}
}

至此使用ClassPathXmlApplicationContext容器的初始化源码就分析结束了。写的比较粗糙,也就是简单的梳理了下容器的初始化流程,具体的内容需要大家去下载源码一步步的进行查看。

4 简单的总结

BeanFactory和ApplicationContext的关系

  • BeanFactory是容器的顶层接口,定义了容器应该具备的基本功能。
  • ApplicationContext继承自BeanFactory,对BeanFactory进行了扩展,是更高级的容器。

IOC的初始化过程

1、BeanDefinition资源的定位与加载(加载为Resource对象)

2、BeanDefinition的解析,使用的是(Document相关API)

3、BeanDefinition的注册,存入一个叫beanDefinitionMap的ConcurrentHashMap的对象。

Spring的IOC容器中是使用BeanDefinition来进行定义的,来自各个地方的资源,Spring都会将其转换为BeanDefinition。

扫码关注公众号

本文转载自: 掘金

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

应届生拿到50w年薪offer,分享我的秋招经历

发表于 2020-05-21

我的秋招结果

因为在秋招的一开始我就拿到了一开始定的目标offer,所以后来我就没有进行海投,而是有选择的进行投递。总体来说投的公司不多,但是投的公司,都拿到offer了,其中包括百度,字节跳动,华为,网易,拼多多,京东,小米,去哪儿,keep,有赞等,几乎都是都是sp或者ssp offer,最高的薪资达到了年薪50w。

我的秋招经历

时间

(1) 准备

分享一句人生格言:不要说明天,要就现在!

秋招的准备时间最好是从拿到实习offer就开始了,很多同学春招的时候拿到了不错的offer,到了实习的时候就放飞自我了,这是非常错误的做法,反观有些同学虽然春招结果不是很理想(比如我,我当时春招只拿到了爱奇艺一个offer),但是在实习的过程中是不断学习总结进步,最后在秋招中取得了比较大的进步。 所以我建议秋招的准备一定要越早越好!

(2)秋招过程

提前批

很多公司都有设置提前批,并且大部分不会不影响正式秋招,所以提前批是个秋招前热身练手的好机会,也是查缺补漏的最佳时机(每次面试其实都是对你整个知识系统的狂轰乱炸,每次面试之后好好总结,知识城堡就又加了一道壁垒),所以一定要把握提前批的机会,提前批一般来说七月初就会开启,有的公司甚至六月份就开启了,所以在实习期间就要关注,抓紧了!

正式秋招

秋招正式开始后,我比较建议投递的时间点是前期偏中期进行投递,因为这个时候会有一些关于该公司的面经分享出来,大家可以摸摸这个公司的面试套路和方向,做一些针对性的准备。

补录和春招

补录和春招有时候会有一些“优惠政策”,比如免笔试,所以秋招中没能拿到心仪公司offer的同学,千万不能错过。

一般在十月份左右,因为一些同学拒掉offer,会导致一些公司的hc空出来,这个时候就会一些公司会进行一轮补招,这个时候大家也得注意关注招聘信息,时刻准备着。

另外在第二年的春天,很多公司都有春招,其中不乏BATTMD等,是进大厂的最后一次机会。

简历

(1) 模版

我当时用的是五百丁这个平台,另外自己也从网上下载了不少模版,文末可以获取,最后,我也用markdow自己写了一版简历,觉得也挺方便的。markdown的语法也不难,琢磨一下一个小时就掌握了。也是比较推荐的方式。

(2) 内容

联系方式和求职意向

联系方式求职意向尽量放在左上角(左右格式简历),最上方(上下格式),方便HR或者面试官联系。

专业技能

专业技能一定要慎重,不要什么都往上写,如果不是特别有信心,尽量不要用“精通”和“熟悉“等词汇,一般都写了解就可以。

项目

项目一定要按照你最想要被问到的顺序从上往下写,并且不要列流水账,比如”开发了xxx项目,功能是xxx,使用了redis,zookeeper,kafka组件“,最好是用”使用了xxx技术,解决xxx问题“这样的格式,比如”使用了消息中间件kafka进行限流削峰,使用redis作为缓存,缓解数据库的压力“等等。

实习经历

实习经历我建议放在比较显眼的位置,在秋招中一段好的实习经历还是比较加分的(证明春招已经被认可过一次了,印象分就会好 很多),另外和项目描述一样,我建议也是以”参与了xxx项目,提高了自己xxx能力的格式“描述自己的实习经历,表明自己通过实习得到了哪些进步。

招聘信息

招聘信息的获取渠道主要有以下三种:

(1)牛客网

我秋招的时候,招聘信息基本都是从牛客获取的。简历投递的话优先考虑内推,牛客有很多公司的员工内推,内推好处显而易见:简历优先被处理,有的是直接组内直推,响应非常快,另外内推进度可以直接问内推人,不用自己干等着结果着急。

(2)官网

对于自己心仪的公司,一定要经常关注官网的动态。

(3) 公众号

互联派,互联镖局等,许多公众号到了春招/秋招的时候,都会整理相应的招聘信息。

知识体系

接下来就是最终要的干货环节了。下面是我总结的各个部分的知识点,每个部分都有推荐的必读书籍,一般来说只需要把推荐的书籍吃透就够了。

1.计算机网络

阅读书籍:《图解http》《图解TCP/IP》《TCP/IP详解卷1》


2.数据库

阅读书籍:《高性能mysql》《mysql技术内幕:INNODB存储引擎》


3. 算法

阅读书籍:《剑指offer》这本书应该说是基础,尽量每一题都能自己手写 剑指Offer练习题

《程序员代码面试指南 IT名企算法与数据结构题目最优解》左神的书里整理了很多优质的题目和解法,整本书较厚,可以挑一些重点题目看。其中二分查找及其延伸题目、快排、堆排、冒泡排序、直接选择、插入排序、归并排序等排序算法、树的遍历是面试常考题,一定要能够顺畅的写出来,并能分析和比较各个算法的复杂度。而动态规划、贪心则是笔试必备题型,建议在leetcode刷刷这两个专题的题目。建议在整个秋招阶段,每天都刷几题leetcode,算法这东西,还是很吃手感的。

4.数据结构

阅读书籍:数据结构(严蔚敏)


5.操作系统

阅读书籍:《Linux内核设计的艺术》


6.java阅读书籍:《java编程思想》《java并发编程实战》 《深入理解java虚拟机》《Java 8 实战》《Spring实战》《看透Spring MVC》基础概念


容器


JUC


JVM


多线程


Spring


Linux阅读书籍:《Linux Shell脚本攻略》


设计模式《Head First设计模式》


7. 海量数据

教你如何迅速秒杀掉:99%的海量数据处理面试题

8. 项目

项目分为两个方面:

(1)实习项目 实习的时候,不应该只关注自己做的项目,也应该关注一下组内其他同事的工作,遇到了什么问题,实习生做的工作其实大部分都比较简单,甚至是“打杂”,所以关注其他同事遇到的问题,积极参与讨论,内化为自己的经验,可以作为项目的亮点。

(2)个人项目 个人项目有一个关键点是,尽量要自己动手做一遍!很多同学在网上随便找了个开源项目,或者看看教学视频之后记记笔记就觉得掌握了,这样其实面试官往往聊两句就知道这个项目不是你自己动手写过的。所以我建议一定要自动动手写一遍。关于项目的建议,大部分同学的选择是:1.黑马视频2.叶神项目(1和2更推荐的是叶神的视频,黑马的视频有点太杂,内容太多了。)3.其他教学视频4.开源项目我的建议是简历至少写两个项目,一般面试可能就只会问一个项目,所以要把自己最有信心的项目放在第一个。另外的话,项目一定要准备一到两个亮点,有的同学可能会说:我的项目没有亮点,就是用了一些技术,调用了一些库呀,有什么亮点。其实亮点第一就是你用这些技术解决了什么问题,另外有一些通用的套路,就是往JVM靠,比如说发现系统周期性超时严重,然后通过一系列jvm的命令发现是gc导致,接着就修改垃圾回收的参数让系统恢复稳定等等,这些在《深入理解Java虚拟机》的第五章里面有很多案例,其实都可以往自己的项目上套,不就成了自己项目的亮点了吗~

写在最后:

秋招道阻且长,心态一定要放好,心态一定要放好,心态一定要放好!!

本文转载自: 掘金

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

【奇技淫巧】使用 Navigation + Dynamic

发表于 2020-05-20

androidx navigation 2.3.0 加入了对 dynamic feature module 的导航支持,因此我们利用这个来分离出多个功能 module 来实现模块化

navigation 2.3.0 更新

navigation 2.3.0 更新

国内基本不用的 dynamic feature module

Android App Bundle 是官方 18 年推出的动态发布方案,类似国内各种插件化方案。不过它需要 Google Play Store 支持,这导致在国内无法使用

借着 navigation 组件支持 dynamic feature module 间导航的契机,我们可以使用 dynamic feature module 来拆分功能模块以实现模块化

传统的拆分方案大概是这样,feature module 之间相互隔离,app module 依赖各个 feature module 间接依赖 base 库,公共库

传统架构

传统架构

而使用 dynamic feature module ,其结构是这样的

dynamic feature 架构

dynamic feature 架构

dynamic feature module 也可以按需安装,也就是说,它们可能不包含在用户最初下载的 APK 中,而是在运行时安装。而我们可以直接将它们包含到 APK 中

使用 dynamic feature module

首先我们在 base lib 中引入依赖

1
2
3
4
5
6
7
复制代码dependencies {
def nav_version = "2.3.0-alpha06"

api "androidx.navigation:navigation-fragment-ktx:$nav_version"
api "androidx.navigation:navigation-ui-ktx:$nav_version"
api "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"
}

我们在 app module 中的 res/navigation 目录下创建 main_nav.xml

main_nav.xml

main_nav.xml

接着我们在 activity_main 中设置默认的 host

默认 host

默认 host

❝
这里不同于正常 navigation 的用法,没有使用 NavHostFragment,而是使用 DynamicNavHostFragment

❞

直接跳转 fragment

我们创建 dynamic feature module ,取名为 feature1

创建 dynamic feature module

创建 dynamic feature module

包名前部分需保证与 applicationId 相同

包名前部分需保证与 applicationId 相同

❝
这里 dynamic feature module 的包名前部分要和 applicationId 即 app module 包名相同,否则后续的 include 操作会有问题

❞

选择加载模式

选择加载模式

这里我们选择在安装时集成该 module

接着我们在该 module 下创建一个 fragment 取名为 Feature1OneFragment

之后我们直接在 main_nav.xml 中引入 该 fragment 并加入 action

直接引入 fragment

直接引入 fragment

接着我们就可以在 app 下的 MainFragment 打开 Feature1OneFragment

启动 fragment

启动 fragment

❝
我的 demo 中 feature2 是直接引入 fragment,因此跳转的是 Feature2OneFragment

❞

直接跳转 activity

在 feature1 中创建 activity (demo 中为 feature2)

跳转 activity

跳转 activity

同样需要指定 moduleName

启动activity

启动activity

使用 dynamic feature module 内部的 graph

我们可以为 dynamic feature module 单独配置 navigation graph,这样就可以处理 dynamic feature module 内部的跳转了

在 feature1 中创建 feature1_nav.xml ,其中 startDestination 为 Feature1OneFragment

feature1_nav.xml

feature1_nav.xml

在 main_nav.xml 我们需要使用另外一种方式来使用该 graph

include-dynamic

include-dynamic

我们使用了一个新的标签 include-dynamic,同时我们看到了几个没用过的属性

  • graphPackage 为 dynamic feature module 的包名
  • graphResName 为 dynamic feature module 内部 graph 的名字
  • moduleName 为 module 名

❝
注意:这里的 graphPackage 可以省略

  1. 如果 module 的包名没用按照前文的格式配置会导致无法找到 graphId 的异常
  2. include-dynamic 标签的 id 要与 feature1_nav.xml navigation 标签下的 id 一致,或者后者不设置 id

❞

这样从 app module 导航到 feature1 的 startDestination 后便可使用其内部的逻辑进行后续的导航了

include 跳转

include 跳转

feature module 间跳转

暂不支持 deep link

暂不支持 deep link

Navigation 组件暂不支持 Dynamic include graph 的 deep link

因此我目前也没有找到特别优雅的方式,已知的方案如下

  • 反射
  • 使用 ServiceLoader
  • 使用依赖注入

demo

关于我

我是 Fly_with24

  • 掘金
  • 简书
  • Github

本文转载自: 掘金

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

Flutter 上默认的文本和字体知识点

发表于 2020-05-20

我们都知道在 Flutter 中可以通过 fontFamily 来引入第三方字体,例如通常会将 svg 图标转换为 iconfont.ttf 来实现矢量图标的入,而一般情况下我们是不会设置 fontFamily 来使用第三方字体, 那默认情况下 Flutter 使用的是什么字体呢?

会出现这个疑问,是因为有一天设计给我发了下面那张图,问我 “为什么应用在苹果平台上的英文使用的是 PingFang SC 字体而不是 .SF UI Display ” ? 正如下图所示,它们的 G 字母在显示效果上会有所差异,比如 平方的 G 有明显的转折线。

这时候我不禁产生的好奇,在 Flutter 中引擎默认究竟是如何选择字体?

通过官方解释,在
typography.dart 源码中可以看到,

  • Flutter 默认在 Android 上使用的是 Roboto 字体;
  • 在 iOS 上使用的是 .SF UI Display 或者 .SF UI Text 字体。

The default font on Android is Roboto and on iOS it is .SF UI Display or .SF UI Text (SF meaning San Francisco). If you want to use a different font, then you will need to add it to your app.

那理论上在 iOS 使用的就是 .SF UI Display 字体才对,因为如下源码所示,在 Typography 中当 platform 是 iOS 时,使用的就是 Cupertino 相关的 TextTheme,而 Typography 中的 white 和 black 属性最终会应用到 ThemeData 的 defaultTextTheme、 defaultPrimaryTextTheme 和 defaultAccentTextTheme 中,所以应该是使用 .SF 相关字体才会,为什么会显示的是 PingFang SC 的效果?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码 factory Typography({
TargetPlatform platform = TargetPlatform.android,
TextTheme black,
TextTheme white,
TextTheme englishLike,
TextTheme dense,
TextTheme tall,
}) {
assert(platform != null || (black != null && white != null));
switch (platform) {
case TargetPlatform.iOS:
black ??= blackCupertino;
white ??= whiteCupertino;
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
black ??= blackMountainView;
white ??= whiteMountainView;
}
englishLike ??= englishLike2014;
dense ??= dense2014;
tall ??= tall2014;
return Typography._(black, white, englishLike, dense, tall);
}

为了搞清不同系统上字体的区别,在查阅了资料后可知:

  • 默认在 iOS 上:
+ 中文字体:`PingFang SC`
+ 英文字体:`.SF UI Text` 、`.SF UI Display`
  • 默认在 Android 上:
+ 中文字体:`Source Han Sans` / `Noto`
+ 英文字体:`Roboto`

也就是就 iOS 上除了 .SF 相关的字体外,还有 PingFang 字体的存在,这时候我突然想起在之前的 《Flutter完整开发实战详解(十七、 实用技巧与填坑二)》 中,因为国际化多语言在 .SF 会出现显示异常,所以使用了 fontFamilyFallback 强行指定了 PingFang SC 。

1
2
3
复制代码  getCopyTextStyle(TextStyle textStyle) {
return textStyle.copyWith(fontFamilyFallback: ["PingFang SC", "Heiti SC"]);
}

终于破案了,因为当 fontFamily 没有设置时,就会使用 fontFamilyFallback 中的第一个值将作为首选字体,而在 fontFamilyFallback 中是顺序匹配的,当fontFamily 和 fontFamilyFallback 两者都不提供,则将使用默认平台字体。

而在 1.12.13 版本下测试发现 .SF 导致的问题已经修复了,所以只需要将 fontFamilyFallback 相关的代码去除即可。

那在 iOS 上使用 .SF 字体有什么好处? 按照网络上的说法是:

SF Text 的字距及字母的半封闭空间,比如 "a"! 上半部分会更大,因其可读性更好,适用于更小的字体; SF Display 则适用于偏大的字体。具体分水岭就是 20pt , 即字体小于 20pt 时用 Text ,大于等于 20pt 时用 Display 。

更棒的是由于 SF 属于动态字体,Text 和 Display 两种字体族是系统动态匹配的,也就是说你不用费心去自己手动调节,系统自动根据字体的大小匹配这两种显示模式。

那能不能在 Android 上也使用.SF 字体呢?按照官方的说法:

  • 在使用 Material package 时,在 Android 上使用的是 ·Roboto font· ,而 iOS 使用的是 San Francisco font(SF) ;
  • 在使用 Cupertino package 时,默认主题始终使用 San Francisco font(SF) ;

但是因为 San Francisco font license 限制了该字体只能在 iOS、macOS 或 tvOS 上运行使用,所以如果使用了 Cupertino 主题的话,在 Android 上运行时使用 fallback font。

所以你觉得能不能在 Android 上使用?

最后再补充下,在官方的 architecture 中有提到,在 Flutter 中的文本呈现逻辑是有分层的,其中:

  • 衍生自 Minikin 的 libtxt 库用于字体选择,分隔行等;
  • HartBuzz 用于字形选择和成型;
  • Skia作为 渲染 / GPU后端;
  • 在 Android / Fuchsia 上使用 FreeType 渲染,在 iOS 上使用CoreGraphics 来渲染字体 。

那读完本篇,你奇奇怪怪的知识点有没有增加?

本文转载自: 掘金

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

【C#】CsvHelper 使用手册

发表于 2020-05-20

本文代码基于 CsvHelper 15.0.5

简介

CsvHelper 是一个用于读写 CSV 文件的.NET库。极其快速,灵活且易于使用。

CsvHelper 建立在.NET Standard 2.0 之上,几乎可以在任何地方运行。

Github 地址:github.com/joshclose/c…

模块

模块 功能
CsvHelper 读写 CSV 数据的核心类。
CsvHelper.Configuration 配置 CsvHelper 读写行为的类。
CsvHelper.Configuration.Attributes 配置 CsvHelper 的特性。
CsvHelper.Expressions 生成 LINQ 表达式的类。
CsvHelper.TypeConversion 将 CSV 字段与 .NET 类型相互转换的类。

读取

测试类

1
2
3
4
5
6
复制代码public class Foo
{
public int ID { get; set; }

public string Name { get; set; }
}

csv 文件数据

1
2
3
复制代码ID,Name
1,Tom
2,Jerry

读取所有记录

1
2
3
4
5
6
7
复制代码using (var reader = new StreamReader("foo.csv"))
{
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
var records = csv.GetRecords<Foo>();
}
}

读取 csv 文件时,空行将被忽略,若空行中包含空格,将报错。

如果是 Excel 编辑的 CSV 文件,空行将会变成仅包含分隔符 , 的行,也会报错。

逐条读取

1
2
3
4
5
6
7
8
9
10
复制代码using (var reader = new StreamReader("foo.csv"))
{
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
while (csv.Read())
{
var record = csv.GetRecord<Foo>();
}
}
}

GetRecords<T> 方法通过 yield 返回一个 IEnumerable<T>,并不会将内容一次全部读进内存,除非调用了 ToList 或 ToArray 方法。所以这种逐条读取的写法没有太多必要。

读取单个字段

1
2
3
4
5
6
7
8
9
10
11
复制代码using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
csv.Read();
csv.ReadHeader();

while (csv.Read())
{
var id = csv.GetField<int>(0);
var name = csv.GetField<string>("Name");
}
}

逐行读取时,可以不管标题行,但是,这里不行。

csv.Read(); 这句是读取标题,如果没有的话,while 循环第一次取到的是标题,肯定会报错。

csv.ReadHeader(); 这句是给标题赋值,如果没有的话,csv.GetField<string>("Name") 会报找不到标题。

使用 TryGetField 可以防止意外的报错。

1
复制代码csv.TryGetField(0, out int id);

写入

写入所有记录

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码var records = new List<Foo>
{
new Foo { ID = 1, Name = "Tom" },
new Foo { ID = 2, Name = "Jerry" },
};

using (var writer = new StreamWriter("foo.csv"))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteRecords(records);
}
}

逐条写入

1
2
3
4
5
6
7
8
9
10
复制代码using (var writer = new StreamWriter("foo.csv"))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
foreach (var record in records)
{
csv.WriteRecord(record);
}
}
}

逐字段写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码using (var writer = new StreamWriter("foo.csv"))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteHeader<Foo>();
csv.NextRecord();

foreach (var record in records)
{
csv.WriteField(record.ID);
csv.WriteField(record.Name);
csv.NextRecord();
}
}
}

特性

Index

Index 特性用于标记字段顺序。

在读取文件时,如果没有标题,就只能通过顺序来确定字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码public class Foo
{
[Index(0)]
public int ID { get; set; }

[Index(1)]
public string Name { get; set; }
}

using (var reader = new StreamReader("foo.csv"))
{
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
csv.Configuration.HasHeaderRecord = false;

var records = csv.GetRecords<Foo>().ToList();
}
}

csv.Configuration.HasHeaderRecord = false 配置告知 CsvReader 没有标题。必须要加这一行,否则会默认第一行为标题而跳过,导致最后的结果中少了一行。如果数据量比较多,会很难发现这个 bug。

在写入文件的时候,会按 Index 顺序写入。如果不想写入标题,也需要添加 csv.Configuration.HasHeaderRecord = false;

Name

如果字段名称和列名不一致,可以使用 Name 属性。

1
2
3
4
5
6
7
8
复制代码public class Foo
{
[Name("id")]
public int ID { get; set; }

[Name("name")]
public string Name { get; set; }
}

NameIndex

NameIndex 用于处理 CSV 文件中的同名列。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码public class Foo
{
...

[Name("Name")]
[NameIndex(0)]
public string FirstName { get; set; }

[Name("Name")]
[NameIndex(1)]
public string LastName { get; set; }
}

Ignore

忽略字段

Optional

读取时如果找不到匹配的字段,则忽略。

1
2
3
4
5
6
7
复制代码public class Foo
{
...

[Optional]
public string Remarks { get; set; }
}

Default

当读取的字段为空时 Default 特性可为其指定默认值。

Default 特性仅在读取时有效,写入时是不会将空值替换为默认值写入的。

NullValues

1
2
3
4
5
6
7
复制代码public class Foo
{
...

[NullValues("None", "none", "Null", "null")]
public string None { get; set; }
}

读取文件时,若 CSV 文件中某字段的值为空,那么读取后的值是 "",而非 null,标记 NullValues 特性后,若 CSV 文件中的某字段值为 NullValues 指定的值,则读取后为 null。

若同时标记了 Default 特性,则此特性不起作用。

坑爹的是,在写入文件时,此特性并不起作用。因此会引起读写不一致的问题。

Constant

Constant 特性为字段指定一个常量值,读写时都使用此值,无论指定了什么其他映射或配置。

Format

Format 指定类型转换时使用的字符串格式。

例如数字和时间类型,我们经常会指定其格式。

1
2
3
4
5
6
7
8
9
10
复制代码public class Foo
{
...

[Format("0.00")]
public decimal Amount { get; set; }

[Format("yyyy-MM-dd HH:mm:ss")]
public DateTime JoinTime { get; set; }
}

BooleanTrueValues 和 BooleanFalseValues

这两个特性用于将 bool 转换成指定的形式显示。

1
2
3
4
5
6
7
8
复制代码public class Foo
{
...

[BooleanTrueValues("yes")]
[BooleanFalseValues("no")]
public bool Vip { get; set; }
}

NumberStyles

1
2
3
4
5
6
7
8
复制代码public class Foo
{
...

[Format("X2")]
[NumberStyles(NumberStyles.HexNumber)]
public int Data { get; set; }
}

比较有用是 NumberStyles.HexNumber 和 NumberStyles.AllowHexSpecifier,这两个枚举的作用差不多。此特性仅在读取时有效,写入时并不会转成 16 进制写入。这会导致读写不一致,可以用 Format 特性指定写入格式。

映射

如果无法给要映射的类添加特性,在这种情况下,可以使用 ClassMap 方式进行映射。

使用映射和使用特性效果是一样的,坑爹的地方也一样坑爹。以下示例用属性实现了上面特性的功能。

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
复制代码public class Foo2
{
public int ID { get; set; }

public string Name { get; set; }

public decimal Amount { get; set; }

public DateTime JoinTime { get; set; }

public string Msg { get; set; }

public string Msg2 { get; set; }

public bool Vip { get; set; }

public string Remarks { get; set; }

public string None { get; set; }

public int Data { get; set; }
}

public class Foo2Map : ClassMap<Foo2>
{
public Foo2Map()
{
Map(m => m.ID).Index(0).Name("id");
Map(m => m.Name).Index(1).Name("name");
Map(m => m.Amount).TypeConverterOption.Format("0.00");
Map(m => m.JoinTime).TypeConverterOption.Format("yyyy-MM-dd HH:mm:ss");
Map(m => m.Msg).Default("Hello");
Map(m => m.Msg2).Ignore();
Map(m => m.Vip)
.TypeConverterOption.BooleanValues(true, true, new string[] { "yes" })
.TypeConverterOption.BooleanValues(false, true, new string[] { "no" });
Map(m => m.Remarks).Optional();
Map(m => m.None).TypeConverterOption.NullValues("None", "none", "Null", "null");
Map(m => m.Data)
.TypeConverterOption.NumberStyles(NumberStyles.HexNumber)
.TypeConverterOption.Format("X2");
}
}

在使用映射前,需要先注册

1
复制代码csv.Configuration.RegisterClassMap<Foo2Map>();

ConvertUsing

ConvertUsing 允许使用一个委托方法实现类型转换。

1
2
3
4
5
6
7
复制代码// 常数
Map(m => m.Constant).ConvertUsing(row => 3);

// 把两列聚合在一起
Map(m => m.Name).ConvertUsing(row => $"{row.GetField<string>("FirstName")} {row.GetField<string>("LastName")}");

Map(m => m.Names).ConvertUsing(row => new List<string> { row.GetField<string>("Name") } );

配置

Delimiter

分隔符

1
复制代码csv.Configuration.Delimiter = ",";

HasHeaderRecord

此配置前文已经提到过,是否将第一行作为标题

1
复制代码csv.Configuration.HasHeaderRecord = false;

IgnoreBlankLines

是否忽略空行,默认 true

1
复制代码csv.Configuration.IgnoreBlankLines = false;

无法忽略一个仅包含空格或 , 的行。

AllowComments

是否允许注释,注释以 # 开头。

1
复制代码csv.Configuration.AllowComments = true;

Comment

获取或设置用于表示注释掉的行的字符。默认是 #。

1
复制代码csv.Configuration.Comment = '/';

BadDataFound

设置一个函数,该函数会在数据不正确时触发,可用于记录日志。

IgnoreQuotes

获取或设置一个值,该值指示在解析时是否应忽略引号并将其与其他任何字符一样对待。

默认是 false,如果字符串中有引号,必须是 3 个 " 连在一起,读取到的字符串中才会有一个 ",如果是 1 个则忽略,2 个则报错。

如果为 true,则会将 " 当做字符串原样返回。

1
复制代码csv.Configuration.IgnoreQuotes = true;

CsvWriter 中是没有这个属性的,一旦字符串中包含 ",写出来就是 3 个 " 连在一起。

TrimOptions

去除字段首尾空格

1
复制代码csv.Configuration.TrimOptions = TrimOptions.Trim;

PrepareHeaderForMatch

PrepareHeaderForMatch 定义了属性名称与标题进行匹配的函数。标题和属性名称均通过该函数运行。此功能可用于删除标题中的空格,或者当标题和属性名称大小写不一致时统一大小写后比较。

1
复制代码csv.Configuration.PrepareHeaderForMatch = (string header, int index) => header.ToLower();

本文转载自: 掘金

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

不会看 Explain执行计划,劝你简历别写熟悉 SQL优化

发表于 2020-05-20

个人博客地址:www.chengxy-nds.top,2000G 技术资源自取

昨天中午在食堂,和部门的技术大牛们坐在一桌吃饭,作为一个卑微技术渣仔默默的吃着饭,听大佬们高谈阔论,研究各种高端技术,我TM也想说话可实在插不上嘴。

聊着聊着突然说到他上午面试了一个工作6年的程序员,表情挺复杂,他说:我看他简历写着熟悉SQL语句调优,就问了下 Explain 执行计划怎么看?结果这老哥一问三不知,工作6年这么基础的东西都不了解!

感受到了大佬的王之鄙视,回到工位我就开始默默写这个,哎~ 我TM也不太懂 Explain ,老哥你这是针对我啊!哭唧唧~
在这里插入图片描述

Explain有什么用

当Explain 与 SQL语句一起使用时,MySQL 会显示来自优化器关于SQL执行的信息。也就是说,MySQL解释了它将如何处理该语句,包括如何连接表以及什么顺序连接表等。

  • 表的加载顺序
  • sql 的查询类型
  • 可能用到哪些索引,哪些索引又被实际使用
  • 表与表之间的引用关系
  • 一个表中有多少行被优化器查询
    …..

Explain有哪些信息

Explain 执行计划包含字段信息如下:分别是 id、select_type、table、partitions、type、possible_keys、key、key_len、ref、rows、filtered、Extra 12个字段。

下边我们会结合具体的SQL示例,详细的解读每个字段以及每个字段中不同参数的含义,以下所有示例数据库版本为 MySQL.5.7.17。

1
2
3
4
5
6
复制代码mysql> select version() from dual;
+------------+
| version() |
+------------+
| 5.7.17-log |
+------------+

我们创建三张表 one、two、three,表之间的关系 one.two_id = two.two_id AND two.three_id = three.three_id。

Explain执行计划详解

一、id

id: :表示查询中执行select子句或者操作表的顺序,id的值越大,代表优先级越高,越先执行。 id大致会出现 3种情况:

1、id相同

看到三条记录的id都相同,可以理解成这三个表为一组,具有同样的优先级,执行顺序由上而下,具体顺序由优化器决定。

1
2
3
4
5
6
7
8
复制代码mysql> EXPLAIN SELECT * FROM one o,two t, three r WHERE o.two_id = t.two_id AND t.three_id = r.three_id;
+----+-------------+-------+------------+--------+---------------+---------+---------+----------------------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------+---------+---------+----------------------+------+----------+----------------------------------------------------+
| 1 | SIMPLE | o | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 100 | NULL |
| 1 | SIMPLE | t | NULL | ALL | PRIMARY | NULL | NULL | NULL | 2 | 50 | Using where; Using join buffer (Block Nested Loop) |
| 1 | SIMPLE | r | NULL | eq_ref | PRIMARY | PRIMARY | 4 | xin-slave.t.three_id | 1 | 100 | NULL |
+----+-------------+-------+------------+--------+---------------+---------+---------+----------------------+------+----------+----------------------------------------------------+
2、id不同

如果我们的 SQL 中存在子查询,那么 id的序号会递增,id值越大优先级越高,越先被执行 。当三个表依次嵌套,发现最里层的子查询 id最大,最先执行。

1
2
3
4
5
6
7
8
复制代码mysql> EXPLAIN select * from one o where o.two_id = (select t.two_id from two t where t.three_id = (select r.three_id  from three r where r.three_name='我是第三表2'));
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | PRIMARY | o | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 50 | Using where |
| 2 | SUBQUERY | t | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 50 | Using where |
| 3 | SUBQUERY | r | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 50 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
3、以上两种同时存在

将上边的 SQL 稍微修改一下,增加一个子查询,发现 id的以上两种同时存在。相同id划分为一组,这样就有三个组,同组的从上往下顺序执行,不同组 id值越大,优先级越高,越先执行。

1
2
3
4
5
6
7
8
9
复制代码mysql>  EXPLAIN select * from one o where o.two_id = (select t.two_id from two t where t.three_id = (select r.three_id  from three r where r.three_name='我是第三表2')) AND o.one_id in(select one_id from one where o.one_name="我是第一表2");
+----+-------------+-------+------------+--------+---------------+---------+---------+--------------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------+---------+---------+--------------------+------+----------+-------------+
| 1 | PRIMARY | o | NULL | ALL | PRIMARY | NULL | NULL | NULL | 2 | 50 | Using where |
| 1 | PRIMARY | one | NULL | eq_ref | PRIMARY | PRIMARY | 4 | xin-slave.o.one_id | 1 | 100 | Using index |
| 2 | SUBQUERY | t | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 50 | Using where |
| 3 | SUBQUERY | r | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 50 | Using where |
+----+-------------+-------+------------+--------+---------------+---------+---------+--------------------+------+----------+-------------+

二、select_type

select_type:表示 select 查询的类型,主要是用于区分各种复杂的查询,例如:普通查询、联合查询、子查询等。

1、SIMPLE

SIMPLE:表示最简单的 select 查询语句,也就是在查询中不包含子查询或者 union交并差集等操作。

2、PRIMARY

PRIMARY:当查询语句中包含任何复杂的子部分,最外层查询则被标记为PRIMARY。

3、SUBQUERY

SUBQUERY:当 select 或 where 列表中包含了子查询,该子查询被标记为:SUBQUERY 。

4、DERIVED

DERIVED:表示包含在from子句中的子查询的select,在我们的 from 列表中包含的子查询会被标记为derived 。

5、UNION

UNION:如果union后边又出现的select 语句,则会被标记为union;若 union 包含在 from 子句的子查询中,外层 select 将被标记为 derived。

6、UNION RESULT

UNION RESULT:代表从union的临时表中读取数据,而table列的<union1,4>表示用第一个和第四个select的结果进行union操作。

1
2
3
4
5
6
7
8
9
10
复制代码mysql> EXPLAIN select t.two_name, ( select one.one_id from one) o from (select two_id,two_name from two where two_name ='') t  union (select r.three_name,r.three_id from three r);

+------+--------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+------+--------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-----------------+
| 1 | PRIMARY | two | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 50 | Using where |
| 2 | SUBQUERY | one | NULL | index | NULL | PRIMARY | 4 | NULL | 2 | 100 | Using index |
| 4 | UNION | r | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 100 | NULL |
| NULL | UNION RESULT | <union1,4> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary |
+------+--------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-----------------+

三、table

查询的表名,并不一定是真实存在的表,有别名显示别名,也可能为临时表,例如上边的DERIVED、 <union1,4>等。

四、partitions

查询时匹配到的分区信息,对于非分区表值为NULL,当查询的是分区表时,partitions显示分区表命中的分区情况。

1
2
3
4
5
复制代码+----+-------------+----------------+---------------------------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------------+---------------------------------+-------+---------------+---------+---------+------+------+----------+-------------+
| 1 | SIMPLE | one | p201801,p201802,p201803,p300012 | index | NULL | PRIMARY | 9 | NULL | 3 | 100 | Using index |
+----+-------------+----------------+---------------------------------+-------+---------------+---------+---------+------+------+----------+-------------+

五、type

type:查询使用了何种类型,它在 SQL优化中是一个非常重要的指标,以下性能从好到坏依次是:system > const > eq_ref > ref > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

1、system

system: 当表仅有一行记录时(系统表),数据量很少,往往不需要进行磁盘IO,速度非常快。

2、const

const:表示查询时命中 primary key 主键或者 unique 唯一索引,或者被连接的部分是一个常量(const)值。这类扫描效率极高,返回数据量少,速度非常快。

1
2
3
4
5
6
复制代码mysql> EXPLAIN SELECT * from three where three_id=1;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| 1 | SIMPLE | three | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100 | NULL |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
3、eq_ref

eq_ref:查询时命中主键primary key 或者 unique key索引, type 就是 eq_ref。

1
2
3
4
5
6
7
复制代码mysql> EXPLAIN select o.one_name from one o ,two t where o.one_id = t.two_id ; 
+----+-------------+-------+------------+--------+---------------+----------+---------+--------------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------+----------+---------+--------------------+------+----------+-------------+
| 1 | SIMPLE | o | NULL | index | PRIMARY | idx_name | 768 | NULL | 2 | 100 | Using index |
| 1 | SIMPLE | t | NULL | eq_ref | PRIMARY | PRIMARY | 4 | xin-slave.o.one_id | 1 | 100 | Using index |
+----+-------------+-------+------------+--------+---------------+----------+---------+--------------------+------+----------+-------------+
4、ref

ref:区别于eq_ref ,ref表示使用非唯一性索引,会找到很多个符合条件的行。

1
2
3
4
5
6
7
复制代码mysql> select o.one_id from one o where o.one_name = "xin" ; 
+--------+
| one_id |
+--------+
| 1 |
| 3 |
+--------+
1
2
3
4
5
6
mysql> EXPLAIN select o.one_id from one o where o.one_name = "xin" ; 
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | o | NULL | ref | idx_name | idx_name | 768 | const | 1 | 100 | Using index |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
5、ref_or_null

ref_or_null:这种连接类型类似于 ref,区别在于 MySQL会额外搜索包含NULL值的行。

1
2
3
4
5
6
复制代码mysql> EXPLAIN select o.one_id from one o where o.one_name = "xin" OR o.one_name IS NULL; 
+----+-------------+-------+------------+-------------+---------------+----------+---------+-------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------------+---------------+----------+---------+-------+------+----------+--------------------------+
| 1 | SIMPLE | o | NULL | ref_or_null | idx_name | idx_name | 768 | const | 3 | 100 | Using where; Using index |
+----+-------------+-------+------------+-------------+---------------+----------+---------+-------+------+----------+--------------------------+
6、index_merge

index_merge:使用了索引合并优化方法,查询使用了两个以上的索引。

下边示例中同时使用到主键one_id 和 字段one_name的idx_name 索引 。

1
2
3
4
5
6
复制代码mysql> EXPLAIN select * from one o where o.one_id >1 and o.one_name ='xin'; 
+----+-------------+-------+------------+-------------+------------------+------------------+---------+------+------+----------+------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------------+------------------+------------------+---------+------+------+----------+------------------------------------------------+
| 1 | SIMPLE | o | NULL | index_merge | PRIMARY,idx_name | idx_name,PRIMARY | 772,4 | NULL | 1 | 100 | Using intersect(idx_name,PRIMARY); Using where |
+----+-------------+-------+------------+-------------+------------------+------------------+---------+------+------+----------+------------------------------------------------+
7、unique_subquery

unique_subquery:替换下面的 IN子查询,子查询返回不重复的集合。

1
复制代码value IN (SELECT primary_key FROM single_table WHERE some_expr)
8、index_subquery

index_subquery:区别于unique_subquery,用于非唯一索引,可以返回重复值。

1
复制代码value IN (SELECT key_column FROM single_table WHERE some_expr)
9、range

range:使用索引选择行,仅检索给定范围内的行。简单点说就是针对一个有索引的字段,给定范围检索数据。在where语句中使用 bettween...and、<、>、<=、in 等条件查询 type 都是 range。

举个栗子:three表中three_id为唯一主键,user_id普通字段未建索引。

1
2
3
4
5
6
复制代码mysql> EXPLAIN SELECT * from three where three_id BETWEEN 2 AND 3;
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| 1 | SIMPLE | three | NULL | range | PRIMARY | PRIMARY | 4 | NULL | 1 | 100 | Using where |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+

从结果中看到只有对设置了索引的字段,做范围检索 type 才是 range。

1
2
3
4
5
6
复制代码mysql> EXPLAIN SELECT * from three where user_id BETWEEN 2 AND 3;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | three | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 33.33 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
10、index

index:Index 与ALL 其实都是读全表,区别在于index是遍历索引树读取,而ALL是从硬盘中读取。

下边示例:three_id 为主键,不带 where 条件全表查询 ,type结果为index 。

1
2
3
4
5
6
复制代码mysql> EXPLAIN SELECT three_id from three ;
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| 1 | SIMPLE | three | NULL | index | NULL | PRIMARY | 4 | NULL | 1 | 100 | Using index |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
11、ALL

ALL:将遍历全表以找到匹配的行,性能最差。

1
2
3
4
5
6
复制代码mysql> EXPLAIN SELECT * from two ;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | two | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 100 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+

六、possible_keys

possible_keys:表示在MySQL中通过哪些索引,能让我们在表中找到想要的记录,一旦查询涉及到的某个字段上存在索引,则索引将被列出,但这个索引并不定一会是最终查询数据时所被用到的索引。具体请参考上边的例子。

七、key

key:区别于possible_keys,key是查询中实际使用到的索引,若没有使用索引,显示为NULL。具体请参考上边的例子。

当 type 为 index_merge 时,可能会显示多个索引。

八、key_len

key_len:表示查询用到的索引长度(字节数),原则上长度越短越好 。

  • 单列索引,那么需要将整个索引长度算进去;
  • 多列索引,不是所有列都能用到,需要计算查询中实际用到的列。

注意:key_len只计算where条件中用到的索引长度,而排序和分组即便是用到了索引,也不会计算到key_len中。

九、ref

ref:常见的有:const,func,null,字段名。

  • 当使用常量等值查询,显示const,
  • 当关联查询时,会显示相应关联表的关联字段
  • 如果查询条件使用了表达式、函数,或者条件列发生内部隐式转换,可能显示为func
  • 其他情况null

十、rows

rows:以表的统计信息和索引使用情况,估算要找到我们所需的记录,需要读取的行数。

这是评估SQL 性能的一个比较重要的数据,mysql需要扫描的行数,很直观的显示 SQL 性能的好坏,一般情况下 rows 值越小越好。

1
2
3
4
5
6
复制代码mysql> EXPLAIN SELECT * from three;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | three | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 100 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+

十一、filtered

filtered 这个是一个百分比的值,表里符合条件的记录数的百分比。简单点说,这个字段表示存储引擎返回的数据在经过过滤后,剩下满足条件的记录数量的比例。

在MySQL.5.7版本以前想要显示filtered需要使用explain extended命令。MySQL.5.7后,默认explain直接显示partitions和filtered的信息。

十二、Extra

Extra :不适合在其他列中显示的信息,Explain 中的很多额外的信息会在 Extra 字段显示。

1、Using index

Using index:我们在相应的 select 操作中使用了覆盖索引,通俗一点讲就是查询的列被索引覆盖,使用到覆盖索引查询速度会非常快,SQl优化中理想的状态。

什么又是覆盖索引?

一条 SQL只需要通过索引就可以返回,我们所需要查询的数据(一个或几个字段),而不必通过二级索引,查到主键之后再通过主键查询整行数据(select * )。

one_id表为主键

1
2
3
4
5
6
复制代码mysql> EXPLAIN SELECT one_id from one ;
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-------------+
| 1 | SIMPLE | one | NULL | index | NULL | idx_two_id | 5 | NULL | 3 | 100 | Using index |
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-------------+

注意:想要使用到覆盖索引,我们在 select 时只取出需要的字段,不可select *,而且该字段建了索引。

1
2
3
4
5
6
复制代码mysql> EXPLAIN SELECT * from one ;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | one | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 100 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
2、Using where

Using where:查询时未找到可用的索引,进而通过where条件过滤获取所需数据,但要注意的是并不是所有带where语句的查询都会显示Using where。

下边示例create_time 并未用到索引,type 为 ALL,即MySQL通过全表扫描后再按where条件筛选数据。

1
2
3
4
5
6
复制代码mysql> EXPLAIN SELECT one_name from one where create_time ='2020-05-18';
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | one | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 33.33 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
3、Using temporary

Using temporary:表示查询后结果需要使用临时表来存储,一般在排序或者分组查询时用到。

1
2
3
4
5
6
复制代码mysql> EXPLAIN SELECT one_name from one where one_id in (1,2) group by one_name;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | one | NULL | range| NULL | NULL | NULL | NULL | 3 | 33.33 | Using where; Using temporary; Using filesort |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
4、Using filesort

Using filesort:表示无法利用索引完成的排序操作,也就是ORDER BY的字段没有索引,通常这样的SQL都是需要优化的。

1
2
3
4
5
6
复制代码mysql> EXPLAIN SELECT one_id from one  ORDER BY create_time;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
| 1 | SIMPLE | one | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 100 | Using filesort |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+

如果ORDER BY字段有索引就会用到覆盖索引,相比执行速度快很多。

1
2
3
4
5
6
复制代码mysql> EXPLAIN SELECT one_id from one  ORDER BY one_id;
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| 1 | SIMPLE | one | NULL | index | NULL | PRIMARY | 4 | NULL | 3 | 100 | Using index |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
5、Using join buffer

Using join buffer:在我们联表查询的时候,如果表的连接条件没有用到索引,需要有一个连接缓冲区来存储中间结果。

先看一下有索引的情况:连接条件 one_name 、two_name 都用到索引。

1
2
3
4
5
6
7
复制代码mysql> EXPLAIN SELECT one_name from one o,two t where o.one_name = t.two_name;
+----+-------------+-------+------------+-------+---------------+----------+---------+----------------------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+----------+---------+----------------------+------+----------+--------------------------+
| 1 | SIMPLE | o | NULL | index | idx_name | idx_name | 768 | NULL | 3 | 100 | Using where; Using index |
| 1 | SIMPLE | t | NULL | ref | idx_name | idx_name | 768 | xin-slave.o.one_name | 1 | 100 | Using index |
+----+-------------+-------+------------+-------+---------------+----------+---------+----------------------+------+----------+--------------------------+

接下来删掉 连接条件 one_name 、two_name 的字段索引。发现Extra 列变成 Using join buffer,type均为全表扫描,这也是SQL优化中需要注意的地方。

1
2
3
4
5
6
7
复制代码mysql> EXPLAIN SELECT one_name from one o,two t where o.one_name = t.two_name;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| 1 | SIMPLE | t | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 100 | NULL |
| 1 | SIMPLE | o | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 33.33 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
6、Impossible where

Impossible where:表示在我们用不太正确的where语句,导致没有符合条件的行。

1
2
3
4
5
6
复制代码mysql> EXPLAIN SELECT one_name from one WHERE 1=2;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------+
| 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | Impossible WHERE |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------+
7、No tables used

No tables used:我们的查询语句中没有FROM子句,或者有 FROM DUAL子句。

1
2
3
4
5
6
复制代码mysql> EXPLAIN select now();
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
| 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | No tables used |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+

Extra列的信息非常非常多,这里就不再一一列举了,详见 MySQL官方文档 :https://dev.mysql.com/doc/refman/5.7/en/explain-output.html#jointype\_index\_merge
在这里插入图片描述

总结

上边只是简单介绍了下 Explain 执行计划各个列的含义,了解它不仅仅是要应付面试,在实际开发中也经常会用到。比如对慢SQL进行分析,如果连执行计划结果都不会看,那还谈什么SQL优化呢?


整理了几百本各类技术电子书和视频课程,送给小伙伴们。公号内回复【666】自行领取。和一些小伙伴们建了一个技术交流群,一起探讨技术、分享技术资料,旨在共同学习进步,如果感兴趣就扫码加入我们吧!

本文转载自: 掘金

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

Java中操作XML的几种方式

发表于 2020-05-20

1.Java中操作XML常见的几种方式

1.DOM

DOM(Document Object Model)文档对象模型是JAXP(Java API for XML Program)的一部分。Java DOM解析器负责解析XML文件并创建相应的DOM对象,这些DOM对象以树结构链接在一起。解析器将整个XML结构读入内存。

preson.xml:

1
2
3
4
5
6
7
8
9
10
复制代码<?xml version="1.0" encoding="UTF-8" standalone="no"?><person>
<p1>
<name>张三</name>
<age>20</age>
<sex>man</sex><sex>man</sex></p1>
<p1>
<name>李四</name>
<age>30</age>
</p1>
</person>

continents.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
复制代码<?xml version="1.0" encoding="UTF-8"?>
<continents>
<europe>
<slovakia>
<capital>
Bratislava
</capital>
<population>
421000
</population>
</slovakia>
<hungary>
<capital>
Budapest
</capital>
<population>
1759000
</population>
</hungary>
<poland>
<capital>
Warsaw
</capital>
<population>
1735000
</population>
</poland>
</europe>
<asia>
<china>
<capital>
Beijing
</capital>
<population>
21700000
</population>
</china>

<vietnam>
<capital>
Hanoi
</capital>
<population>
7500000
</population>
</vietnam>
</asia>
</continents>

DOM:

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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
复制代码public class TestDOM {
public static void selectAll() throws ParserConfigurationException, IOException, SAXException {
System.out.println("-----------------------------------------------------");

//1.创建解析器工厂
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
//2.根据解析器工厂创建解析器
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
//3.根据xml文件生成Document
Document document = documentBuilder.parse("src\\main\\java\\com\\ly\\jaxp\\person.xml");
NodeList nodeList = document.getElementsByTagName("name");
for (int n = 0; n < nodeList.getLength(); n++) {
Node node = nodeList.item(n);
String textContent = node.getTextContent();
System.out.println(textContent);
}
}

public static void selectOne() throws ParserConfigurationException, IOException, SAXException {
System.out.println("-----------------------------------------------------");
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document document = documentBuilder.parse("src\\main\\java\\com\\ly\\jaxp\\person.xml");
NodeList nodeList = document.getElementsByTagName("name");
Node node = nodeList.item(0);
String textContent = node.getTextContent();
System.out.println(textContent);
}

public static void addNode() throws ParserConfigurationException, IOException, SAXException, TransformerException {
System.out.println("-----------------------------------------------------");

DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document document = documentBuilder.parse("src\\main\\java\\com\\ly\\jaxp\\person.xml");
NodeList nodeList = document.getElementsByTagName("p1");
Node node = nodeList.item(0);
//1.创建标签
Element element = document.createElement("sex");
//2.创建标签中的内容
Text text = document.createTextNode("man");
//3.标签追加内容
element.appendChild(text);
//4.节点追加标签
node.appendChild(element);

//5.进行回写,xslt
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
transformer.transform(new DOMSource(document), new StreamResult("src\\main\\java\\com\\ly\\jaxp\\person.xml"));
}

public static void updateNode() throws ParserConfigurationException, IOException, SAXException, TransformerException {
System.out.println("-----------------------------------------------------");

DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document document = documentBuilder.parse("src\\main\\java\\com\\ly\\jaxp\\person.xml");
NodeList nodeList = document.getElementsByTagName("sex");
Node node = nodeList.item(0);
//1.更改节点内容
node.setTextContent("woman");

//2.进行回写
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
transformer.transform(new DOMSource(document), new StreamResult("src\\main\\java\\com\\ly\\jaxp\\person.xml"));
}

public static void deleteNode() throws ParserConfigurationException, IOException, SAXException, TransformerException {
System.out.println("-----------------------------------------------------");
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document document = documentBuilder.parse("src\\main\\java\\com\\ly\\jaxp\\person.xml");
NodeList nodeList = document.getElementsByTagName("sex");
Node node = nodeList.item(0);
//1.拿到要删除节点的父节点,根据父节点删除子节点
Node parentNode = node.getParentNode();
parentNode.removeChild(node);

TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
transformer.transform(new DOMSource(document), new StreamResult("src\\main\\java\\com\\ly\\jaxp\\person.xml"));
}

public static void listElement() throws ParserConfigurationException, IOException, SAXException {
System.out.println("-----------------------------------------------------");

DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document document = documentBuilder.parse("src\\main\\java\\com\\ly\\jaxp\\person.xml");
list1(document);
}

private static void list1(Node node) {
if (node.getNodeType() == Node.ELEMENT_NODE) {
System.out.println(node.getNodeName());
}
NodeList childNodes = node.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node item = childNodes.item(i);
list1(item);
}
}

public static void iterator() throws ParserConfigurationException, IOException, SAXException {
File file = new File("src\\main\\java\\com\\ly\\jaxp\\user.xml");
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document document = documentBuilder.parse(file);

//1.规范化
document.getDocumentElement().normalize();
//2.强转为DocumentTraversal对象
DocumentTraversal documentTraversal = (DocumentTraversal) document;
//3.获取迭代器
NodeIterator nodeIterator = documentTraversal.createNodeIterator(document.getDocumentElement(), NodeFilter.SHOW_ELEMENT, null, true);
for (Node node = nodeIterator.nextNode(); node != null; node = nodeIterator.nextNode()) {
String nodeName = node.getNodeName();
System.out.println(nodeName);
}

System.out.println("-----------------------------------------------------");
NodeIterator textNodeIterator = documentTraversal.createNodeIterator(document.getDocumentElement(), NodeFilter.SHOW_TEXT, null, true);
for (Node node = textNodeIterator.nextNode(); node != null; node = textNodeIterator.nextNode()) {
String textContent = node.getTextContent().trim();
System.out.println(textContent);
}
}

public static void iteratorCustomizeFilter() throws IOException, SAXException, ParserConfigurationException {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document document = documentBuilder.parse("src\\main\\java\\com\\ly\\jaxp\\continents.xml");

document.getDocumentElement().normalize();
DocumentTraversal documentTraversal = (DocumentTraversal) document;

MyFilter myFilter = new MyFilter();
NodeIterator nodeIterator = documentTraversal.createNodeIterator(document.getDocumentElement(), NodeFilter.SHOW_ELEMENT, myFilter, true);
for (Node node = nodeIterator.nextNode(); node != null; node = nodeIterator.nextNode()) {
String nodeName = node.getNodeName();
String textContent = node.getTextContent();
System.out.println(nodeName + ":" + textContent);
}
}

static class MyFilter implements NodeFilter {

@Override
public short acceptNode(Node n) {
if (n.getNodeType() == Node.ELEMENT_NODE) {
String nodeName = n.getNodeName();
if (Objects.equals("slovakia", nodeName) || Objects.equals("poland", nodeName)) {
return NodeFilter.FILTER_ACCEPT;
}
}
return NodeFilter.FILTER_REJECT;
}
}

public static void treeWalker() throws ParserConfigurationException, IOException, SAXException {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document document = documentBuilder.parse("src\\main\\java\\com\\ly\\jaxp\\continents.xml");
document.getDocumentElement().normalize();

DocumentTraversal documentTraversal = (DocumentTraversal) document;
TreeWalker treeWalker = documentTraversal.createTreeWalker(document.getDocumentElement(), NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, null, true);
treeWalkerRecursion(treeWalker);
}

private static void treeWalkerRecursion(TreeWalker walker) {
Node currentNode = walker.getCurrentNode();
if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
System.out.println(currentNode.getNodeName());
}
if (currentNode.getNodeType() == Node.TEXT_NODE) {
System.out.println(currentNode.getTextContent());
}
//深度递归
for (Node n = walker.firstChild(); n != null; n = walker.nextSibling()) {
treeWalkerRecursion(walker);
}
//回溯
walker.setCurrentNode(currentNode);
}

public static void createXML() throws ParserConfigurationException, TransformerException {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document document = documentBuilder.newDocument();
Element rootElement = document.createElementNS("zetcode.com", "users");
document.appendChild(rootElement);

rootElement.appendChild(createUser(document, "1", "Robert", "Brown", "programer"));
rootElement.appendChild(createUser(document, "2", "Pamela", "Kyle", "writer"));
rootElement.appendChild(createUser(document, "3", "Peter", "Smith", "teacher"));


TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
//编码
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
//这两个设置表示缩进
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");

DOMSource domSource = new DOMSource(document);

File file = new File("src\\main\\java\\com\\ly\\jaxp\\create.xml");
StreamResult fileResult = new StreamResult(file);

StreamResult consoleResult = new StreamResult(System.out);

transformer.transform(domSource, fileResult);
transformer.transform(domSource, consoleResult);
}

private static Node createUser(Document document, String id, String firstName, String lastName, String occupation) {
Element element = document.createElement("user");
element.setAttribute("id", id);
element.appendChild(createUserElement(document, "firstName", firstName));
element.appendChild(createUserElement(document, "lastName", lastName));
element.appendChild(createUserElement(document, "occupation", occupation));
return element;
}

private static Node createUserElement(Document document, String name, String value) {
Element element = document.createElement(name);
element.appendChild(document.createTextNode(value));
return element;
}

public static void main(String[] args) throws IOException, SAXException, ParserConfigurationException, TransformerException {
// selectAll();
// selectOne();
// addNode();
// updateNode();
// deleteNode();
// listElement();
// traversal();
// iteratorCustomizeFilter();
// treeWalker();
// createXML();
}
}

2.SAX

SAX(Simple API for XML)是DOM的替代方法,基于事件驱动解析XML。SAX只读。

SAX:

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
复制代码public class TestSAX {
private SAXParser createInstance() {
SAXParser parser = null;
try {
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
parser = saxParserFactory.newSAXParser();
} catch (ParserConfigurationException | SAXException e) {
e.printStackTrace();
}
return parser;
}

private List<User> parseUserList() {
MyHandler myHandler = new MyHandler();
try {
File file = Paths.get("src\\main\\java\\com\\ly\\jaxp\\user.xml").toFile();
SAXParser saxParser = createInstance();
//这里会进行Handler执行
saxParser.parse(file, myHandler);
} catch (SAXException | IOException e) {
e.printStackTrace();
}
return myHandler.getUserList();
}

public static void main(String[] args) {
TestSAX testSAX = new TestSAX();
List<User> userList = testSAX.parseUserList();
for (User user : userList) {
System.out.println(user.toString());
}
}
}

hanlder:

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
复制代码public class MyHandler extends DefaultHandler {
private List<User> list = new ArrayList<>();
private User user;

private boolean bfn = false;
private boolean bln = false;
private boolean boc = false;

//每解析一个标签的时候都会回调该方法
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if (Objects.equals("user", qName)) {
user = new User();
int id = Integer.parseInt(attributes.getValue("id"));
user.setId(id);
}

switch (qName) {
case "firstname":
bfn = true;
break;
case "lastname":
bln = true;
break;
case "occupation":
boc = true;
break;
default:
break;
}
}

//每次遇到textContent的时候会回调该方法
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
if (bfn) {
user.setFirstName(new String(ch, start, length));
bfn = false;
}
if (bln) {
user.setLastName(new String(ch, start, length));
bln = false;
}
if (boc) {
user.setOccupation(new String(ch, start, length));
boc = false;
}
}

//每次遇到结束标签的时候会回调该方法
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if (Objects.equals("user", qName)) {
list.add(user);
}
}

public List<User> getUserList() {
return list;
}
}

因为SAX是基于事件驱动的,所以它提供了几个事件处理器:EntityResolver、DTDHandler、ContentHandler、ErrorHandler。我们一般常用的是ContentHandler和ErrorHandler,ContentHandler提供了解析XML时回调的方法,ErrorHandler提供了解析出现异常时的处理方法。DefaultHandler实现了这四个处理器,但真正的进行处理,类似于模板方法模式,交给子类来实现,因此我们一般会自定一个Handler实现我们需要的方法即可。

3.JAXB

JAXB是可以把XML转为Java Object,也可以把Java Object转为XML,本身是由JDK提供的,在JAVA11之后需要引入jar包。

maven:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.2.11</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.2.11</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.2.11</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>

Java Object:

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
复制代码@XmlRootElement(name = "book") //根标签
@XmlType(propOrder = {"author", "name", "publisher", "isbn"}) //排序
public class Book {
private String name;
private String author;
private String publisher;
private String isbn;

@XmlElement(name = "title")//别名
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getAuthor() {
return author;
}

public void setAuthor(String author) {
this.author = author;
}

public String getPublisher() {
return publisher;
}

public void setPublisher(String publisher) {
this.publisher = publisher;
}

public String getIsbn() {
return isbn;
}

public void setIsbn(String isbn) {
this.isbn = isbn;
}

@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", author='" + author + '\'' +
", publisher='" + publisher + '\'' +
", isbn='" + isbn + '\'' +
'}';
}
}

@XmlRootElement(namespace = "com.zetcode", name = "bookStore")
public class BookStore {
private ArrayList<Book> bookList;
private String name;
private String location;

@XmlElementWrapper(name = "bookList")
@XmlElement(name = "book")
// @XmlTransient
public ArrayList<Book> getBookList() {
return bookList;
}

public void setBookList(ArrayList<Book> bookList) {
this.bookList = bookList;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getLocation() {
return location;
}

public void setLocation(String location) {
this.location = location;
}

@Override
public String toString() {
return "BookStore{" +
"bookList=" + bookList +
", name='" + name + '\'' +
", location='" + location + '\'' +
'}';
}
}

JAXB:

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
复制代码public class TestJAXB {
private static final String BOOKSTORE_XML = "src\\main\\java\\com\\ly\\jaxp\\bookstore.xml";

public static void toXML() throws JAXBException {
ArrayList<Book> list = new ArrayList<>();

Book book1 = new Book();
book1.setIsbn("978-0060554736");
book1.setName("The Game");
book1.setAuthor("Neil Strauss");
book1.setPublisher("Harpercollins");
list.add(book1);

Book book2 = new Book();
book2.setIsbn("978-3832180577");
book2.setName("Feuchtgebiete");
book2.setAuthor("Charlotte Roche");
book2.setPublisher("Dumont Buchverlag");
list.add(book2);

BookStore bookStore = new BookStore();
bookStore.setName("Fraport Bookstore");
bookStore.setLocation("Livres belles");
bookStore.setBookList(list);

JAXBContext jaxbContext = JAXBContext.newInstance(BookStore.class);
Marshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
// marshaller.setProperty(Marshaller.JAXB_FRAGMENT, false);

marshaller.marshal(bookStore, System.out);

marshaller.marshal(bookStore, new File(BOOKSTORE_XML));
}

public static void formXML() throws JAXBException {
JAXBContext jaxbContext = JAXBContext.newInstance(BookStore.class);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
BookStore bookStore = (BookStore) unmarshaller.unmarshal(new File(BOOKSTORE_XML));
System.out.println(bookStore);
}

public static void main(String[] args) throws JAXBException {
//1.需要自己创建JAXB
toXML();
formXML();
//2.java7以后,可以直接使用静态方式
// JAXB.marshal();
// JAXB.unmarshal();
}
}

4.XStream

XStream是一种OXMapping技术,可以很方便的将Java Bean转为XML,反之亦然。

User:

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
复制代码@XStreamAlias(value = "user")
public class User {
@XStreamAlias(value = "username")
private String userName;
@XStreamAlias(value = "email")
private String email;

public String getUserName() {
return userName;
}

public void setUserName(String userName) {
this.userName = userName;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", email='" + email + '\'' +
'}';
}
}

XStream:

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
复制代码public class TestXstream {

public static <T> T formXML(Class<T> clazz, String xml) {
XStream xStream = new XStream();
//解决安全框架未启动问题
// XStream.setupDefaultSecurity(xStream);
// xStream.allowTypes(new Class[]{User.class});
//非注解别名
xStream.alias("user",clazz);
//使用注解别名
// xStream.processAnnotations(clazz);
T t = (T) xStream.fromXML(xml);
return t;
}

public static String toXML(Class clazz, Object object) {
XStream xStream = new XStream();
// XStream.setupDefaultSecurity(xStream);
// xStream.allowTypes(new Class[]{User.class});
//非注解别名
xStream.alias("user",clazz);
//使用注解别名
// xStream.processAnnotations(clazz);
String xml = xStream.toXML(object);
return xml;
}

public static void main(String[] args) {
User user = new User();
user.setUserName("张三");
user.setEmail("123456");
String xml = toXML(user.getClass(), user);
System.out.println(xml);

User user1 = formXML(User.class, xml);
System.out.println(user1);
}
}

本文转载自: 掘金

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

我常用的自动化部署技巧,贼好用,推荐给大家!

发表于 2020-05-20

SpringBoot实战电商项目mall(30k+star)地址:github.com/macrozheng/…

摘要

SpringBoot+Jenkins自动化部署技巧,远程部署同样适用,附通用自动化脚本!本文将从半自动化部署讲起,到自动化部署,讲解一套生产环境切实可用的自动化部署方案!

半自动化部署

之前写过的SpringBoot应用打包Docker镜像都是通过Maven插件来实现的,由于远程服务器需要开发2375端口,存在一定的安全隐患。这次介绍另一种方法,使用DockerFile+Jar+自动化脚本的形式来部署。由于需要一定的手动操作,我把它称之为半自动化部署。

项目打包

  • 这次我们不使用Docker的Maven插件来打包,先在pom.xml中注释掉它;

  • 然后使用Maven的package命令直接将应用打成Jar包;

  • 此时在target目录下就会生成一个Jar包,我们打包Docker镜像的时候会用到它。

DockerFile

主要是定义了如何将Jar包打包成Docker镜像,对DockerFile不了解的朋友可以看下《使用Dockerfile为SpringBoot应用构建Docker镜像》,具体内容如下。

1
2
3
4
5
6
7
8
9
10
复制代码# 该镜像需要依赖的基础镜像
FROM java:8
# 将当前目录下的jar包复制到docker容器的/目录下
ADD mall-tiny-jenkins-1.0-SNAPSHOT.jar /mall-tiny-jenkins-1.0-SNAPSHOT.jar
# 声明服务运行在8088端口
EXPOSE 8088
# 指定docker容器启动时运行jar包
ENTRYPOINT ["java", "-jar","/mall-tiny-jenkins-1.0-SNAPSHOT.jar"]
# 指定维护者的名字
MAINTAINER macro

自动化脚本

可以作为通用脚本来使用的模板脚本,只需改变其中的一些参数即可,具体执行流程为:停止旧服务->删除旧容器->删除旧镜像->打包新镜像->运行新镜像。

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
复制代码#!/usr/bin/env bash
# 定义应用组名
group_name='mall-tiny'
# 定义应用名称
app_name='mall-tiny-jenkins'
# 定义应用版本
app_version='1.0-SNAPSHOT'
# 定义应用环境
profile_active='qa'
echo '----copy jar----'
docker stop ${app_name}
echo '----stop container----'
docker rm ${app_name}
echo '----rm container----'
docker rmi ${group_name}/${app_name}:${app_version}
echo '----rm image----'
# 打包编译docker镜像
docker build -t ${group_name}/${app_name}:${app_version} .
echo '----build image----'
docker run -p 8088:8088 --name ${app_name} \
--link mysql:db \
-e 'spring.profiles.active'=${profile_active} \
-e TZ="Asia/Shanghai" \
-v /etc/localtime:/etc/localtime \
-v /mydata/app/${app_name}/logs:/var/logs \
-d ${group_name}/${app_name}:${app_version}
echo '----start container----'

下面讲下自动化脚本里面值得注意的地方:

  • group_name、app_name、app_version可以用来定义打包镜像的属性;
  • profile_active可以让你的应用使用不同环境下的配置,比如使用qa可以启用测试环境的配置,使用prod可以启用生产环境配置,真正的一包多用;
  • docker rmi这步一定要有,如果不删除旧镜像,当新镜像打包的时候会产生none镜像;
  • docker run命令中的-e TZ="Asia/Shanghai"时区一定要设置,否则容器时间会和宿主机会相差8个小时。

部署运行

  • 直接上传我们的应用Jar包、DockerFile文件和自动化部署脚本到指定目录下;

  • 将自动化脚本修改为可执行;
1
复制代码chmod +x run.sh
  • 使用./run.sh命令直接运行脚本即可。

结合Jenkins自动化部署

之前的打包、上传文件都是我们手动完成的,其实这些操作也可以让Jenkins来帮我们实现,有了Jenkins才算得上是真正的自动化部署!

学前准备

学习下面的内容需要对Jenkins有一定的了解,不了解的朋友可以看下:《使用Jenkins一键打包部署SpringBoot应用,就是这么6!》

Publish Over SSH

这里推荐安装这款Jenkins插件,它的主要作用是可以通过SSH在不同服务器之间传输文件和执行命令。比如说我们把Jenkins装在了测试服务器上,我们可以使用Jenkins在测试服务器上从Git仓库获取代码,然后打成Jar包。打包完成后我们可以通过这个插件将Jar包传输到正式服务器上去,然后执行正式服务器上的自动化脚本,从而实现正式服务器上的自动化部署。

  • 首先我们可以在系统管理->插件管理中找到该插件,然后进行安装;

  • 然后在系统管理->插件管理中添加相应的SSH配置;

  • 配置完成后创建一个应用的构建任务,源码管理和构建中的Maven打包配置和之前的Jenkins教程中一样,只有最后一步不同,添加构建步骤为通过SSH发送文件并执行命令;

  • 配置好我们的SSH Publisher,主要是源文件路径和目标文件路径,以及需要执行的脚本;

  • 之后执行构建任务即可实现自动化部署了,此方法在两台不同服务器之间同样适用!

总结

从我写过的几篇自动化部署文章中,其实可以看出,Linux下的自动化部署主要是依靠一连串的Linux命令来实现的。Jenkins的自动化部署也是基于这些的,所以要学会自动化部署,Linux命令和Docker命令是必不可少的!

项目源码地址

github.com/macrozheng/…

公众号

mall项目全套学习教程连载中,关注公众号第一时间获取。

公众号图片

本文转载自: 掘金

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

要学就学透彻!Spring Security 中 CSRF

发表于 2020-05-20

上篇文章松哥和大家聊了什么是 CSRF 攻击,以及 CSRF 攻击要如何防御。主要和大家聊了 Spring Security 中处理该问题的几种办法。

今天松哥来和大家简单的看一下 Spring Security 中,CSRF 防御源码。

本文是本系列第 19 篇,阅读本系列前面文章有助于更好的理解本文:

  1. 挖一个大坑,Spring Security 开搞!
  2. 松哥手把手带你入门 Spring Security,别再问密码怎么解密了
  3. 手把手教你定制 Spring Security 中的表单登录
  4. Spring Security 做前后端分离,咱就别做页面跳转了!统统 JSON 交互
  5. Spring Security 中的授权操作原来这么简单
  6. Spring Security 如何将用户数据存入数据库?
  7. Spring Security+Spring Data Jpa 强强联手,安全管理只有更简单!
  8. Spring Boot + Spring Security 实现自动登录功能
  9. Spring Boot 自动登录,安全风险要怎么控制?
  10. 在微服务项目中,Spring Security 比 Shiro 强在哪?
  11. SpringSecurity 自定义认证逻辑的两种方式(高级玩法)
  12. Spring Security 中如何快速查看登录用户 IP 地址等信息?
  13. Spring Security 自动踢掉前一个登录用户,一个配置搞定!
  14. Spring Boot + Vue 前后端分离项目,如何踢掉已登录用户?
  15. Spring Security 自带防火墙!你都不知道自己的系统有多安全!
  16. 什么是会话固定攻击?Spring Boot 中要如何防御会话固定攻击?
  17. 集群化部署,Spring Security 要如何处理 session 共享?
  18. 松哥手把手教你在 SpringBoot 中防御 CSRF 攻击!so easy!

本文主要从两个方面来和大家讲解:

  1. 返回给前端的 _csrf 参数是如何生成的。
  2. 前端传来的 _csrf 参数是如何校验的。

1.随机字符串生成

我们先来看一下 Spring Security 中的 csrf 参数是如何生成的。

首先,Spring Security 中提供了一个保存 csrf 参数的规范,就是 CsrfToken:

1
2
3
4
5
6
复制代码public interface CsrfToken extends Serializable {
String getHeaderName();
String getParameterName();
String getToken();

}

这里三个方法都好理解,前两个是获取 _csrf 参数的 key,第三个是获取 _csrf 参数的 value。

CsrfToken 有两个实现类,如下:

默认情况下使用的是 DefaultCsrfToken,我们来稍微看下 DefaultCsrfToken:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码public final class DefaultCsrfToken implements CsrfToken {
private final String token;
private final String parameterName;
private final String headerName;
public DefaultCsrfToken(String headerName, String parameterName, String token) {
this.headerName = headerName;
this.parameterName = parameterName;
this.token = token;
}
public String getHeaderName() {
return this.headerName;
}
public String getParameterName() {
return this.parameterName;
}
public String getToken() {
return this.token;
}
}

这段实现很简单,几乎没有添加额外的方法,就是接口方法的实现。

CsrfToken 相当于就是 _csrf 参数的载体。那么参数是如何生成和保存的呢?这涉及到另外一个类:

1
2
3
4
5
6
复制代码public interface CsrfTokenRepository {
CsrfToken generateToken(HttpServletRequest request);
void saveToken(CsrfToken token, HttpServletRequest request,
HttpServletResponse response);
CsrfToken loadToken(HttpServletRequest request);
}

这里三个方法:

  1. generateToken 方法就是 CsrfToken 的生成过程。
  2. saveToken 方法就是保存 CsrfToken。
  3. loadToken 则是如何加载 CsrfToken。

CsrfTokenRepository 有四个实现类,在上篇文章中,我们用到了其中两个:HttpSessionCsrfTokenRepository 和 CookieCsrfTokenRepository,其中 HttpSessionCsrfTokenRepository 是默认的方案。

我们先来看下 HttpSessionCsrfTokenRepository 的实现:

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
复制代码public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository {
private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN";
private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class
.getName().concat(".CSRF_TOKEN");
private String parameterName = DEFAULT_CSRF_PARAMETER_NAME;
private String headerName = DEFAULT_CSRF_HEADER_NAME;
private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME;
public void saveToken(CsrfToken token, HttpServletRequest request,
HttpServletResponse response) {
if (token == null) {
HttpSession session = request.getSession(false);
if (session != null) {
session.removeAttribute(this.sessionAttributeName);
}
}
else {
HttpSession session = request.getSession();
session.setAttribute(this.sessionAttributeName, token);
}
}
public CsrfToken loadToken(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return (CsrfToken) session.getAttribute(this.sessionAttributeName);
}
public CsrfToken generateToken(HttpServletRequest request) {
return new DefaultCsrfToken(this.headerName, this.parameterName,
createNewToken());
}
private String createNewToken() {
return UUID.randomUUID().toString();
}
}

这段源码其实也很好理解:

  1. saveToken 方法将 CsrfToken 保存在 HttpSession 中,将来再从 HttpSession 中取出和前端传来的参数做笔记。
  2. loadToken 方法当然就是从 HttpSession 中读取 CsrfToken 出来。
  3. generateToken 是生成 CsrfToken 的过程,可以看到,生成的默认载体就是 DefaultCsrfToken,而 CsrfToken 的值则通过 createNewToken 方法生成,是一个 UUID 字符串。
  4. 在构造 DefaultCsrfToken 是还有两个参数 headerName 和 parameterName,这两个参数是前端保存参数的 key。

这是默认的方案,适用于前后端不分的开发,具体用法可以参考上篇文章。

如果想在前后端分离开发中使用,那就需要 CsrfTokenRepository 的另一个实现类 CookieCsrfTokenRepository ,代码如下:

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
复制代码public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
private String parameterName = DEFAULT_CSRF_PARAMETER_NAME;
private String headerName = DEFAULT_CSRF_HEADER_NAME;
private String cookieName = DEFAULT_CSRF_COOKIE_NAME;
private boolean cookieHttpOnly = true;
private String cookiePath;
private String cookieDomain;
public CookieCsrfTokenRepository() {
}
@Override
public CsrfToken generateToken(HttpServletRequest request) {
return new DefaultCsrfToken(this.headerName, this.parameterName,
createNewToken());
}
@Override
public void saveToken(CsrfToken token, HttpServletRequest request,
HttpServletResponse response) {
String tokenValue = token == null ? "" : token.getToken();
Cookie cookie = new Cookie(this.cookieName, tokenValue);
cookie.setSecure(request.isSecure());
if (this.cookiePath != null && !this.cookiePath.isEmpty()) {
cookie.setPath(this.cookiePath);
} else {
cookie.setPath(this.getRequestContext(request));
}
if (token == null) {
cookie.setMaxAge(0);
}
else {
cookie.setMaxAge(-1);
}
cookie.setHttpOnly(cookieHttpOnly);
if (this.cookieDomain != null && !this.cookieDomain.isEmpty()) {
cookie.setDomain(this.cookieDomain);
}

response.addCookie(cookie);
}
@Override
public CsrfToken loadToken(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, this.cookieName);
if (cookie == null) {
return null;
}
String token = cookie.getValue();
if (!StringUtils.hasLength(token)) {
return null;
}
return new DefaultCsrfToken(this.headerName, this.parameterName, token);
}
public static CookieCsrfTokenRepository withHttpOnlyFalse() {
CookieCsrfTokenRepository result = new CookieCsrfTokenRepository();
result.setCookieHttpOnly(false);
return result;
}
private String createNewToken() {
return UUID.randomUUID().toString();
}
}

和 HttpSessionCsrfTokenRepository 相比,这里 _csrf 数据保存的时候,都保存到 cookie 中去了,当然读取的时候,也是从 cookie 中读取,其他地方则和 HttpSessionCsrfTokenRepository 是一样的。

OK,这就是我们整个 _csrf 参数生成的过程。

总结一下,就是生成一个 CsrfToken,这个 Token,本质上就是一个 UUID 字符串,然后将这个 Token 保存到 HttpSession 中,或者保存到 Cookie 中,待请求到来时,从 HttpSession 或者 Cookie 中取出来做校验。

2.参数校验

那接下来就是校验了。

校验主要是通过 CsrfFilter 过滤器来进行,我们来看下核心的 doFilterInternal 方法:

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
复制代码protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
final boolean missingToken = csrfToken == null;
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for "
+ UrlUtils.buildFullRequestUrl(request));
}
if (missingToken) {
this.accessDeniedHandler.handle(request, response,
new MissingCsrfTokenException(actualToken));
}
else {
this.accessDeniedHandler.handle(request, response,
new InvalidCsrfTokenException(csrfToken, actualToken));
}
return;
}
filterChain.doFilter(request, response);
}

这个方法我来稍微解释下:

  1. 首先调用 tokenRepository.loadToken 方法读取 CsrfToken 出来,这个 tokenRepository 就是你配置的 CsrfTokenRepository 实例,CsrfToken 存在 HttpSession 中,这里就从 HttpSession 中读取,CsrfToken 存在 Cookie 中,这里就从 Cookie 中读取。
  2. 如果调用 tokenRepository.loadToken 方法没有加载到 CsrfToken,那说明这个请求可能是第一次发起,则调用 tokenRepository.generateToken 方法生成 CsrfToken ,并调用 tokenRepository.saveToken 方法保存 CsrfToken。
  3. 大家注意,这里还调用 request.setAttribute 方法存了一些值进去,这就是默认情况下,我们通过 jsp 或者 thymeleaf 标签渲染 _csrf 的数据来源。
  4. requireCsrfProtectionMatcher.matches 方法则使用用来判断哪些请求方法需要做校验,默认情况下,”GET”, “HEAD”, “TRACE”, “OPTIONS” 方法是不需要校验的。
  5. 接下来获取请求中传递来的 CSRF 参数,先从请求头中获取,获取不到再从请求参数中获取。
  6. 获取到请求传来的 csrf 参数之后,再和一开始加载到的 csrfToken 做比较,如果不同的话,就抛出异常。

如此之后,就完成了整个校验工作了。

3.LazyCsrfTokenRepository

前面我们说了 CsrfTokenRepository 有四个实现类,除了我们介绍的两个之外,还有一个 LazyCsrfTokenRepository,这里松哥也和大家做一个简单介绍。

在前面的 CsrfFilter 中大家发现,对于常见的 GET 请求实际上是不需要 CSRF 攻击校验的,但是,每当 GET 请求到来时,下面这段代码都会执行:

1
2
3
4
复制代码if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}

生成 CsrfToken 并保存,但实际上却没什么用,因为 GET 请求不需要 CSRF 攻击校验。

所以,Spring Security 官方又推出了 LazyCsrfTokenRepository。

LazyCsrfTokenRepository 实际上不能算是一个真正的 CsrfTokenRepository,它是一个代理,可以用来增强 HttpSessionCsrfTokenRepository 或者 CookieCsrfTokenRepository 的功能:

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
复制代码public final class LazyCsrfTokenRepository implements CsrfTokenRepository {
@Override
public CsrfToken generateToken(HttpServletRequest request) {
return wrap(request, this.delegate.generateToken(request));
}
@Override
public void saveToken(CsrfToken token, HttpServletRequest request,
HttpServletResponse response) {
if (token == null) {
this.delegate.saveToken(token, request, response);
}
}
@Override
public CsrfToken loadToken(HttpServletRequest request) {
return this.delegate.loadToken(request);
}
private CsrfToken wrap(HttpServletRequest request, CsrfToken token) {
HttpServletResponse response = getResponse(request);
return new SaveOnAccessCsrfToken(this.delegate, request, response, token);
}
private static final class SaveOnAccessCsrfToken implements CsrfToken {
private transient CsrfTokenRepository tokenRepository;
private transient HttpServletRequest request;
private transient HttpServletResponse response;

private final CsrfToken delegate;

SaveOnAccessCsrfToken(CsrfTokenRepository tokenRepository,
HttpServletRequest request, HttpServletResponse response,
CsrfToken delegate) {
this.tokenRepository = tokenRepository;
this.request = request;
this.response = response;
this.delegate = delegate;
}
@Override
public String getToken() {
saveTokenIfNecessary();
return this.delegate.getToken();
}
private void saveTokenIfNecessary() {
if (this.tokenRepository == null) {
return;
}

synchronized (this) {
if (this.tokenRepository != null) {
this.tokenRepository.saveToken(this.delegate, this.request,
this.response);
this.tokenRepository = null;
this.request = null;
this.response = null;
}
}
}

}
}

这里,我说三点:

  1. generateToken 方法,该方法用来生成 CsrfToken,默认 CsrfToken 的载体是 DefaultCsrfToken,现在换成了 SaveOnAccessCsrfToken。
  2. SaveOnAccessCsrfToken 和 DefaultCsrfToken 并没有太大区别,主要是 getToken 方法有区别,在 SaveOnAccessCsrfToken 中,当开发者调用 getToken 想要去获取 csrfToken 时,才会去对 csrfToken 做保存操作(调用 HttpSessionCsrfTokenRepository 或者 CookieCsrfTokenRepository 的 saveToken 方法)。
  3. LazyCsrfTokenRepository 自己的 saveToken 则做了修改,相当于放弃了 saveToken 的功能,调用该方法并不会做保存操作。

使用了 LazyCsrfTokenRepository 之后,只有在使用 csrfToken 时才会去存储它,这样就可以节省存储空间了。

LazyCsrfTokenRepository 的配置方式也很简单,在我们使用 Spring Security 时,如果对 csrf 不做任何配置,默认其实就是 LazyCsrfTokenRepository+HttpSessionCsrfTokenRepository 组合。

当然我们也可以自己配置,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.successHandler((req,resp,authentication)->{
resp.getWriter().write("success");
})
.permitAll()
.and()
.csrf().csrfTokenRepository(new LazyCsrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
}

4.小结

今天主要和小伙伴聊了一下 Spring Security 中 csrf 防御的原理。

整体来说,就是两个思路:

  1. 生成 csrfToken 保存在 HttpSession 或者 Cookie 中。
  2. 请求到来时,从请求中提取出来 csrfToken,和保存的 csrfToken 做比较,进而判断出当前请求是否合法。

好啦,不知道小伙伴们有没有 GET 到呢?如果觉得有收获,记得点个在看鼓励下松哥哦~

本文转载自: 掘金

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

大白话 六问数据中台!你想知道的都在这了! 第一问:数据中台

发表于 2020-05-19

_1

数据中台、相信这四个字大家一定不陌生。因为在2019年、数据中台可谓是最火的概念之一,很多大公司都在布局自己的数据中台。

那么数据中台到底是什么?它和我们熟知的数据平台有啥区别?它为什么会这么火、能给企业带来什么价值呢?数据中台整体架构和全景图又是什么呢?

笔者有幸参与了公司数据中台从0到N的建设,计划从概念到落地,把中台那些事跟您说透,与您一起分享学习。笔者公众号:【胖滚猪学编程】

第一问:数据中台是什么

先不说那些官方的抽象的概念了,我想用我自己的大白话去说数据中台的概念。

那就是如果把前台比作赚钱的。后台比作支持的。那么中台呢就是支持加速赚钱的。

这个比喻我觉得还是很形象的,中台呢它实质就是前台和后台的一个桥梁,并且它能在这当中起到很好的加速效果。这里的加速,可以是效率上的提高,可以是协作上的共赢。

image

举个例子,比如前台业务人员日常要分析广告投放、在哪个平台投放效益最好呢?抖音还是头条呢?这直接涉及到公司的money了。

前台人员要分析这个肯定要有数据吧、就会向后台人员要数据:我需要哪些表你要帮我同步过来数仓里,同步好了你要授权给我,然后你再去配置定时报表任务、配置好了你要再做一个前端的展示页面。这还没完,数据有问题了还得跟你逼逼叨叨!

这个流程下来,前台人员需要向后台人员沟通100句。有了数据中台、一句话都不用说了。上面这些操作,前台人员都可以自行完成。

所以说数据中台给我们业务效率带来了巨大的提升。

那数据中台有没有缺点呢?

我觉得也是有的,本来后台那些单身小哥哥可以蹭这个机会去跟前台妹子打些交道、说不定姻缘就来了,毕竟前台妹子多,结果被这数据中台一搞,一句话都说不上了。这确实是数据中台的一个缺点。

现在大家应该有个初步的印象了,那么我再用官方抽象的语句做一个总结:数据中台是企业级能力复用平台!企业级大数据通过系统化的方式实现统一共享的数据组织。其中共享包括数据、信息、技术、业务的共享等。它以服务化的方式赋能前台数据应用,稳定可靠、高效的支持上层业务的快速创新,为业务快速赋能。

第二问:数据中台和数据平台的区别

因为我们一直以来都是听数据平台这个词听得比较多,所以第二问我们还是要来说一下它们之间的差别。

数据平台你可以把它看成是数据集,那么数据中台呢他就是数据集API,那么它们之间就差在API这三个字母上,API我想应该不需要过多解释呢,大家都知道,比如学JAVA的时候有了JAVA API你才知道怎么使用,那么数据中台相当于在数据平台的基础上告诉你这些数据怎么使用。

另外,数据中台是偏向于业务的,而数据平台是偏技术的

image

但是、数据中台和数据平台也有千丝万缕的联系。数据中台需要依赖大数据平台,大数据平台完成了数据研发的全流程覆盖,而数据中台增加了数据治理和数据服务化的内容。总的来说,数据中台吸收了传统数据仓库、数据湖、大数据平台的优势,同时又解决了数据共享的难题,通过数据应用,实现数据价值的落地。

第三问:数据中台有哪些价值

数据中台的价值,我想用三个关键词来概括:效率、协作、质量

  • 效率:比如数据研发的效率、发现数据的效率。为什么我们每开发一个报表都要改代码呢?为什么数据有问题的问题的时候,我们要找很久才能发现是某某上游的问题呢。
  • 协作:很多应用开发,其实不同的项目组需求大致相同。还是用开发报表来举例,不同业务线项目组开发报表都一个套路,但因为是别的项目组维护的,所以就是得分别开发一遍。就不能协作共赢?
  • 质量:比如数据的一致性、准确性、及时性以及完整性,有没有一个通用的平台来检验这些数据呢。

上面说的还是有一点儿抽象,其实要具体回答这个问题,你首先得大概知道数据中台有哪些功能哪些模块。比如数据地图、元数据管理、数据血缘、数据处理等等都属于数据中台。每个模块都有它的功能,所以它的作用并不是一言两语可以说得清,这里笔者再举一些真实的例子来对比一下:

例一:没有数据中台之前,业务人员根本就不知道HIVE数仓有哪些表,不知道这些表的具体信息(列信息、索引信息、分区信息、责任人信息)。他要出一张报表还要来问你:”hello 帮我看一下HIVE有没有同步这张表吧?hello 帮我看一下这张表是不是分区表吧?”。这个表有问题了,他又要来问你”hello 这张表负责人是谁啊?” 有了数据中台之后,完全不需要管了。(这个是元数据管理给我们带来的便捷)

例二:没有数据中台之前,我们根本就不清楚表的来源和链路,尤其是一些复杂报表的结果表,来源非常复杂可能涉及到多个系统,涉及十几个源表。等到上游业务表要做变更、都不知道会影响哪些报表,线上已经运行上千个报表了啊!要去揪出这些来实在是麻烦!有了数据中台之后,10秒钟就能解决这个问题。(这个是数据血缘给我们带来的便捷)

第四问:数据中台架构

我们说数据中台是服务于公司业务的,因此必须要从自己的业务角度去进行一个全局的规划和架构。不过你依旧可以参考一下典型的架构图:

image

笔者认为可以分为几大部分:

  • 数据采集汇聚(数据库,日志,前端埋点,爬虫系统等)
  • 数据处理和开发(离线计算、实时流计算等)
  • 数据治理(元数据管理、数据血缘、数据质量、数据安全等)
  • 数据服务(智能报表、标签系统、推荐系统、大屏等)

其中、数据采集和数据处理开发,你也可以理解为是数据平台的东西。由于篇幅问题,不对每一个模块作详细说明。笔者将在个人公众号【胖滚猪学编程】详细分享各个模块的概念、功能、以及生产落地方案!

第五问:我们该做数据中台吗?

首先一句话:千万不要跟风。中台不是你想做想做就能做。

因为要做起一个真正意义上的数据中台,一定是站在公司的层面去看待,而不是某个业务部门自己玩玩过家家。因此需要非常大的投入,人力、物力的投入。而这些系统是否能够匹配中台建设的需求,还需要持续打磨。另外必须对公司的整体业务滚瓜烂熟,才能有这种全局的视野去建设中台。

那什么情况下我们可以考虑建设中台呢?

  • 企业是否有大量的数据应用场景?数据中台本身并不能直接产生业务价值,数据中台的本质是支撑快速地孵化数据应用。所以当你的企业有较多数据应用的场景时(一般有3个以上就可以考虑)
  • 企业存在较多的业务数据的孤岛,需要整合各个业务系统的数据,进行关联的分析,此时,你需要构建一个数据中台。比如在我们做电商的初期,仓储、供应链、市场运营都是独立的数据仓库,当时数据分析的时候,往往跨了很多数据系统,为了消除这些数据孤岛,就必须要构建一个数据中台。
  • 当你的团队正在面临效率、质量和成本的苦恼时,面对大量的开发,却不知道如何提高效能,数据经常出问题而束手无策,老板还要求你控制数据的成本,这个时候,数据中台可以帮助你。
  • 当你所在的企业面临经营困难,需要通过数据实现精益运营,提高企业的运营效率的时候,你需要构建一个数据中台,同时结合可视化的Bl数据产品,实现数据从应用到中台的完整构建。
  • 企业规模也是必须要考虑的一个因素,数据中台因为投入大,收益偏长线,所以更适合业务相对稳定的大公司,并不适合初创型的小公司。

第六问:数据中台的参考资料

不得不承认一点,网上关于数据中台的资料太少了,笔者去年中旬从0开始建设数据中台的时候,花了大量时间搜集资料。现在也愿意与大家分享一下我收集到的资料。

书籍推荐:数据中台-让数据用起来。

image

博文推荐:
什么是中台,什么不是中台。所有的中台都是业务中台
到底啥是平台,到底啥是中台?
在构建数据中台之前,你需要知道的几个趋势
火热的数据中台对企业的价值是什么?
你真地需要一个中台吗?
阿里的中台战略其实是个伪命题
从平台到中台 | Elasticsearch 在蚂蚁金服的实践经验
七问七答,亲历者讲阿里中台落地的实践我的一年中台实战录
滴滴出行构建业务中台应对软件复杂度的具体对策与实践
10张图解密阿里数据中台

落地推荐
可以参考阿里的DataWorks产品,上面有很多关于数据中台的原型图可以作为参考。
DataWorks

image

笔者也将在公众号【胖滚猪学编程】上分享自己搭建数据中台的亲身经历,不多说无用概念,直接把生产落地方案分享给你!

最后总结:以用户为中心,以愿景为指引,从战略入手,用科学有效的方法,步步为营沉淀企业级能力,付以必要的组织与系统架构调整,方得中台。

wchat1

本文转载自公众号【胖滚猪学编程】 用漫画让编程so easy and interesting!欢迎关注!

本文转载自: 掘金

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

1…811812813…956

开发者博客

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