大牛带你学Spring源码,非默认标签解析成BeanDefinition定义流程
lipiwang 2024-11-15 22:02 26 浏览 0 评论
Spring源码阅读之非默认标签解析成BeanDefinition定义流程
上一篇文章我们讲解了 Spring 中<bean>标签是怎么被解析成 BeanDefinition 的。而如果不是<bean>、<import>、<beans>、<alias>这四类的其他标签又是怎么被解析成BeanDefinition的呢?比如本篇要讲的 context 相关的标签是怎么被解析成BeanDefinition的。比如常用的<context:component-scan>标签,再比如<context:property-placeholder>标签是怎么被处理的。本文以<context:component-scan>标签为例讲解,我提供了测试例子。感兴趣的同学把<context:property-placeholder>标签的解析流程看看,基本流程差不多。接下来我们就来观摩一下这些标签被解析的全流程吧。(本文来自公众号:z小赵)
在文章开始前,还是老样子,先提供一个测试的例子。方便同学们看完讲解之后自己去 Debug源码。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 首先,我们先在applicationContext.xml文件里面加上context标签 -->
<!-- 同时我们在base-package指定的路径下创建两个类,分别是接口UserService,和其对应的实现类UserServiceImpl -->
<!-- 这里给大家留个小思考,就是我们的文件头里面的内容,为啥会有这么一大堆东西?我在下篇自定义标签中来揭晓答案 -->
<context:component-scan base-package="com.xzhao.service"/>
</beans>
- 提供好测试用例之后,我们就来看看其源码是怎么做的。直接来到上篇文章的开头,即DefaultBeanDefinitionDocumentReader.parseBeanDefinitions()方法。因为<context:component-scan>标签不是DefaultNameSpace的,所以会走delegate.parseCustomElement()方法。那我们就来看看对于这种element它是怎么被解析的。
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 {
// '<context:component-scan>'会走这里。
// 当然 '<context:property-placeholder>'标签也是走这里。
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}
- 进去之后,解析element的实现逻辑如下。
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
// Step1:根据element获取对应的namespaceuri,
// 根据这个我们才能获得其相应的handle,即具体的解析处理器
// 其实在这里返回的值长这个样子:http://www.springframework.org/schema/context
// 相信如果仔细的同学应该不会这玩意儿陌生
String namespaceUri = getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
}
// Step2:这里会首先拿到一个命名空间解析器,即NamespaceResolver对象
// 然后利用NamespaceResolve对象去解析NamespaceUri,从而就得到了一个非常重要的对象
// 命名空间处理器 NamespaceHandler 对象,其实正是这个对象去把element对象解析成BeanDefinition定义的。
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
// Step3:利用的得到的NamespaceHandler去调用其parse方法,
// 即可获得 '<context:component-scan>'标签对应的BeanDefinition对象了
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}
- 接下来大家应该会想,那么多不同的标签,我们应该找那个解析器来解析这个标签呢?所以问题来了,怎么获得解析器?
有了命名空间解析器之后,我们来看看是怎么获得命名空间处理器的。
public NamespaceHandler resolve(String namespaceUri) {
// Step1:获取所有的handlerMappings,即上图红框里面的内容
// 其中key为一个http链接,比如:http://www.springframework.org/context
// 这个key也就是namespaceUri了,而对应的value其实是一个handler,仔细看看其实就是一个class的全路径名字
Map<String, Object> handlerMappings = getHandlerMappings();
// Step2:获取我们指定的handler名字,如果取到不到则直接返回null
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if (handlerOrClassName == null) {
return null;
}
// Step3:由于'<context:component-scan>'标签对应的namespaceUri是 http://www.springframework.org/context
// 所以获取到的一定是className,即是一个字符串对象。
// 如果在一次解析的时候可能已经存在的是其对应的实例对象了,因为每解析完一个标签,则会将当前key的value替换成对象
// 所以就会在此处直接返回
else if (handlerOrClassName instanceof NamespaceHandler) {
return (NamespaceHandler) handlerOrClassName;
}
else {
// Step4:所以将className直接强转为String
String className = (String) handlerOrClassName;
try {
// Step4.1:利用反射机制获取到className对应的Class对象
Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
// Step4.2:检查handlerClass是否是实现了NameSpaceHandler接口,从接口的继承树可以看出
// 所以的handler都是直接或间接的实现了NamespaceHandler接口,从而实现了 init() 、parse() 、decorate() 几个非常重要的方法
if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
"] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
}
// Step4.3:根据Class实例化出对应的namespaceHandler对象,即获取到了我们想要的命名空间解析器了
NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
// Step4.4:执行初始化操作,这一步非常非常重要,
// 点到 namespaceHandler 对应的Handler类里面,我们会发现一些非常眼熟的东西
// 比如此处对应的handler类是ContextNamespaceHandler类。
// 一个小常识,一般标签以什么开头,其对应的handler就是标签开头命名的
namespaceHandler.init();
// Step4.5:将初始化完成的handler对象放到handlerMapppings里面
// 这样做是为了当在有context标签需要解析的时候,则直接从该map里面获取即可,而不需要在重复创建了,相当于充当了一个本地缓存的角色,加速解析工作
handlerMappings.put(namespaceUri, namespaceHandler);
// Step4.6:将创建好的namespaceHandler对象返回
return namespaceHandler;
}
catch (ClassNotFoundException ex) {
throw new FatalBeanException("Could not find NamespaceHandler class [" + className +
"] for namespace [" + namespaceUri + "]", ex);
}
catch (LinkageError err) {
throw new FatalBeanException("Unresolvable class definition for NamespaceHandler class [" +
className + "] for namespace [" + namespaceUri + "]", err);
}
}
}
// 这里简单看下 ContextNamespaceHandler类的init方法干了啥
// 其实非常简单,就是将标签以 <context:xxx> 开头的各种标签进行一个注册操作
// 这里面大家应该比较熟悉的标签 'property-placeholder','component-scan'
public class ContextNamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());
registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());
registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());
registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());
registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
}
}
// 注册操作也比较简单,就是将标签对应的真正解析器放到其父类(NamespaceHandlerSupport类)的parse map中,方便后面开始具体的解析工作
// 从代码也可以看出,'<context:component-scan>'标签对应的真正解析器其实是 ComponentScanBeanDefinitionParser
protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) {
this.parsers.put(elementName, parser);
}
- 到此,我们拿到了命名空间解析器了,接下来看看具体的解析工作是怎么做的。
// 上面我们讲到,将相关的解析器全部注册到了NamespaceHandlerSupport类的parse map里面
// 所以解析工作也肯定是先调用NamespaceHandlerSupport的parse方法获取到对应的解析器
// 然后在调用具体解析器的parse方法执行解析工作获得BeanDefinition定义
public BeanDefinition parse(Element element, ParserContext parserContext) {
// Step1:获取解析器对象,具体怎么找的,就不展开了,很简单,
// 想想就是无非先拿到标签的名字,然后从parse map里面get得到对应的解析器
// 我们重点关注下具体的解析工作,因为所有实现都是在具体的解析类的方法里面实现的
BeanDefinitionParser parser = findParserForElement(element, parserContext);
// Step2:根据解析器对象调用其对应的parse方法,执行具体的解析工作,
// 从而得到BeanDefinition定义对象
return (parser != null ? parser.parse(element, parserContext) : null);
}
- 我们来看看<context:component-scan>标签对应的解析器,即ComponentScanBeanDefinitionParse类的parse方法的实现。这里有个小技巧,一般标签名叫什么,相应的 parse 类的名字也是与其对应的,大家阅读别的标签的源码的时候可以注意下,方便找到对应的解析类。
public BeanDefinition parse(Element element, ParserContext parserContext) {
// Step1:获取 'base-package' 属性对应的值
String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE);
// Step2:将获取到的basePackage做了一些字符串的处理转换工作,比如常见的那种占位符
// 说白了就是字符串的一些替换操作,得到一个spring认为的标准的字符串对象
// 感兴趣的朋友,自行了解相应的内容,这里不做深入探讨
basePackage = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(basePackage);
// Step3:字符串切割,这里我做了个测试,按照规则写了一个字符串,可以切割出多个子串
// 我猜想他的意思应该是base-package可以同时指定多个包路径,
// 之前没有做过同时指定多个包路径的操作,大佬们见笑了
String[] basePackages = StringUtils.tokenizeToStringArray(basePackage,
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
// Actually scan for bean definitions and register them.
// Step4:注释写的也比较明朗,就是首先获得一个scanner,即扫描器,
// 然后拿着扫描器去挨个扫描指定包下的类,从而得到多个BeanDefinition对象
ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
// Step5:得到了多个BeanDefinition定义,又来到了比较熟悉的步骤,注册这些BeanDefinition定义到BeanFactory里面
// 到此,我们关于 '<context:component-scan>'标签解析成对应的BeanDefinition对象也就讲完了
registerComponents(parserContext.getReaderContext(), beanDefinitions, element);
return null;
}
- 接着来看看扫描器是怎么被创建出来的,其实这个里面就是对配置文件的相关属性进行解析赋值操作。
protected ClassPathBeanDefinitionScanner configureScanner(ParserContext parserContext, Element element) {
// Step1:判断当前scanner是否使用默认的过滤器
boolean useDefaultFilters = true;
if (element.hasAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)) {
useDefaultFilters = Boolean.valueOf(element.getAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE));
}
// Delegate bean definition registration to scanner class.
// Step2:将BeanDefinition的构建委托给扫描器去创建
// 创建一个scanner类,使用的是 ClassPathBeanDefinitionScanner 作为扫描器
ClassPathBeanDefinitionScanner scanner = createScanner(parserContext.getReaderContext(), useDefaultFilters);
// Step3:设置BeanDefinition的一些默认属性
scanner.setBeanDefinitionDefaults(parserContext.getDelegate().getBeanDefinitionDefaults());
scanner.setAutowireCandidatePatterns(parserContext.getDelegate().getAutowireCandidatePatterns());
// Step4:配置w恩建如果指定了 'resource-pattern',则设置
if (element.hasAttribute(RESOURCE_PATTERN_ATTRIBUTE)) {
scanner.setResourcePattern(element.getAttribute(RESOURCE_PATTERN_ATTRIBUTE));
}
try {
// Step5:解析 'name-generator' 属性
parseBeanNameGenerator(element, scanner);
}
catch (Exception ex) {
parserContext.getReaderContext().error(ex.getMessage(), parserContext.extractSource(element), ex.getCause());
}
try {
// Step6:解析 'scope-resolver' 属性
parseScope(element, scanner);
}
catch (Exception ex) {
parserContext.getReaderContext().error(ex.getMessage(), parserContext.extractSource(element), ex.getCause());
}
// Step7:解析 'include-filter' 属性
parseTypeFilters(element, scanner, parserContext);
// Step8:返回初始化好的scanner对象,其实这个方法里面是对标签相关属性的解析和设置操作
return scanner;
}
- 得到ClassPathBeanDefinitionScanner扫描器之后,开始使用扫描器去执行真正的扫描工作了,我们来看下具体实现。
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
// Step1:用于存放扫描到的类对应的BeanDefinition定义
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
// Step2:遍历所有指定的扫描包
for (String basePackage : basePackages) {
// Step2.1:查询当前扫描包下的所有候选BeanDefinition定义
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
// Step2.2:遍历每个候选的BeanDefinition
for (BeanDefinition candidate : candidates) {
// Step2.2.1:获取scope的元数据信息,我们通常用的有 'signleton' 和 'property'。
// 而 'signleton'也是默认值。
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
// Step2.2.2:获取候选的BeanDefinition对应的beanName
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
// Step2.2.3:如果当前的候选BeanDefinition实现了AbstractBeanDefinition,
// 则执行BeanDefinition的后置处理操作
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
// Step2.2.4:如果候选的BeanDefinition实现了AnnotatedBeanDefinitio,
// 则执行注解的通用逻辑,通常对BeanDefinition设置一些常用参数,
// 比如是否是懒加载,是否有 depends-on(依赖),role(角色),description(描述)这四个属性
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
// Step2.2.5:检查候选的BeanDefinition
if (checkCandidate(beanName, candidate)) {
// Step2.2.5.1:将BeanDefinition封装成BeanDefinitionHolder对象
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
// Step2.2.5.2:获取代理的BeanDefinitionHolder对象
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
// Step2.2.5.3:将最终的BeanDefinition注册到BeanFactory中,完成BeanDefinition定义的解析工作
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}
- 上面这个方法里面几个比较重要的方法我们来重点看下具体实现。
- indCandidateComponents: 获取指定包下所有的候选BeanDefinition对象
- postProcessBeanDefinition: BeanDefinition对象的后置操作
- checkCandidate: 检查候选的BeanDefinition是否可以被注册到BeanFactory中
// 首先来看第一个方法的,如何获取指定包路径下的候选BeanDefinition定义
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
// Step1:componentsIndex不为null && 索引如果有includeFilters,
// 则根据type去获取BeanDefinition定义,没有遇到过这种使用方式。
// 所以这个地方请求大神赐教
if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
}
else {
// Step2:我们通常的使用方式是这种
return scanCandidateComponents(basePackage);
}
}
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
try {
// Step1:获取要扫描的包下的那些类,比如根据我们的配置可以得到:
// classpath*:com/xzhao/service/**/*.class
// 指定的service下的任何子包和其下面的class
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
// Step2:根据指定的classpath,获取对应下面的所有resource
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
boolean traceEnabled = logger.isTraceEnabled();
boolean debugEnabled = logger.isDebugEnabled();
// Step3:遍历resource,根据resource创建BeanDefinition定义,即候选的BeanDefinition
for (Resource resource : resources) {
if (traceEnabled) {
logger.trace("Scanning " + resource);
}
if (resource.isReadable()) {
try {
// Step3.1:获取resource对应的元数据读取器
// matadataReader包含了三部分内容,分别是resource,简单理解就是class的位置
// 第二部分是class的元数据,主要包含了classLoader,className(com.xzhao.service.UserService)等等信息
// 第三部分是注解的元数据,和class的元数据差不多
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
// step3.2:检查当前元数据是否可以创建BeanDefinition定义,
// 主要是过滤掉不包含(即exclude)的class,返回false
// 保留include的class,返回true
if (isCandidateComponent(metadataReader)) {
// Step3.2.1:创建BeanDefinition定义,ScannedGenericBeanDefinition 间接实现了BeanDefinition接口,
// 这里创建是一个扫描的BeanDefinition定义
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
sbd.setSource(resource);
// Step3.2.2:检测候选bean是否合法,
// 即候选bean(是否是合法的 && 一个具体的bean(感觉可以历程就是是否可以实例化出bean实例)) || (是一个抽象的 && 同时存在注解方法))
if (isCandidateComponent(sbd)) {
if (debugEnabled) {
logger.debug("Identified candidate component class: " + resource);
}
// Step3.2.2.1:检测合法,就作为一个候选的BeanDefinition定义。
candidates.add(sbd);
}
else {
if (debugEnabled) {
logger.debug("Ignored because not a concrete top-level class: " + resource);
}
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not matching any filter: " + resource);
}
}
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to read candidate component class: " + resource, ex);
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not readable: " + resource);
}
}
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
}
return candidates;
}
// 在接着看第二方法,这个方法就比较简单了,就这么简单,大家可以稍微松口气
// 它的作用就是给设置一些通用的属性
protected void postProcessBeanDefinition(AbstractBeanDefinition beanDefinition, String beanName) {
beanDefinition.applyDefaults(this.beanDefinitionDefaults);
if (this.autowireCandidatePatterns != null) {
beanDefinition.setAutowireCandidate(PatternMatchUtils.simpleMatch(this.autowireCandidatePatterns, beanName));
}
}
// 在看最后一个方法,该方法用于最后一步检测当前的BeanDefinition是否合法可以被注册到BeanFactory里面
protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException {
// Step1:当前BeanDefinition是否已经在BeanFactory里面注册过,如果没有则可以正常注册
if (!this.registry.containsBeanDefinition(beanName)) {
return true;
}
// Step2:如果已经在BeanFactory里面注册了,
// 则获取已经注册了的BeanDefinition定义,然后尝试获取存在的BeanDefinition的父BeanDefinition
BeanDefinition existingDef = this.registry.getBeanDefinition(beanName);
BeanDefinition originatingDef = existingDef.getOriginatingBeanDefinition();
if (originatingDef != null) {
existingDef = originatingDef;
}
// Step3:拿已经存在的BeanDefinition和将要注册的BeanDefinition比较
// 已存在的实现了ScannedGenericBeanDefinition则可以覆盖
// 或者已存在的BeanDefinition的source != 新的BeanDefinition的source,则也可以创建
// 或者已存在的BeanDefinition != 新的BeanDefinition,则也可以创建
// 以上三种情况下,可以将新创建的BeanDefinition注册到BeanFactory里面
if (isCompatible(beanDefinition, existingDef)) {
return false;
}
throw new ConflictingBeanDefinitionException("Annotation-specified bean name '" + beanName +
"' for bean class [" + beanDefinition.getBeanClassName() + "] conflicts with existing, " +
"non-compatible bean definition of same name and class [" + existingDef.getBeanClassName() + "]");
}
总结
至此,我们就完成了<context:component-scan>标签扫描类得到BeanDefinitions定义,并将得到的BeanDefinitions依次注册到BeanFactory里面。我们只需要在被注册的类上设置@Component @Service @Controller @Repository这种注解就可以被<component-scan>标签扫描到。
总结一下就是先拿到指定的包路径,然后读取该路径下的所有被声明了注解的类,然后拿到这些类的元数据,再然后根据这些元数据创建出ScannedGenericBeanDefinition对象,最后将符合条件的ScannedGenericBeanDefinition注册到BeanFactory里面。就完成了改标签的工作。
最后,本篇文章篇幅较长,需要慢慢理解其中的逻辑,不明白的地方就多看几遍再理解。
到此我们知道了 spring 提供的自定义的标签是如何解析成BeanDefinition定义的。
感谢大家支持!多多转发关注不迷路~~~~~~
相关推荐
- 前端入门——css 网格轨道详细介绍
-
上篇前端入门——cssGrid网格基础知识整体大概介绍了cssgrid的基本概念及使用方法,本文将介绍创建网格容器时会发生什么?以及在网格容器上使用行、列属性如何定位元素。在本文中,将介绍:...
- Islands Architecture(孤岛架构)在携程新版首页的实践
-
一、项目背景2022,携程PC版首页终于迎来了首次改版,完成了用户体验与技术栈的全面升级。作为与用户连接的重要入口,旧版PC首页已经陪伴携程走过了22年,承担着重要使命的同时,也遇到了很多问题:维护/...
- HTML中script标签中的那些属性
-
HTML中的<script>标签详解在HTML中,<script>标签用于包含或引用JavaScript代码,是前端开发中不可或缺的一部分。通过合理使用<scrip...
- CSS 中各种居中你真的玩明白了么
-
页面布局中最常见的需求就是元素或者文字居中了,但是根据场景的不同,居中也有简单到复杂各种不同的实现方式,本篇就带大家一起了解下,各种场景下,该如何使用CSS实现居中前言页面布局中最常见的需求就是元...
- CSS样式更改——列表、表格和轮廓
-
上篇文章主要介绍了CSS样式更改篇中的字体设置Font&边框Border设置,这篇文章分享列表、表格和轮廓,一起来看看吧。1.列表List1).列表的类型<ulstyle='list-...
- 一文吃透 CSS Flex 布局
-
原文链接:一文吃透CSSFlex布局教学游戏这里有两个小游戏,可用来练习flex布局。塔防游戏送小青蛙回家Flexbox概述Flexbox布局也叫Flex布局,弹性盒子布局。它决定了...
- css实现多行文本的展开收起
-
背景在我们写需求时可能会遇到类似于这样的多行文本展开与收起的场景:那么,如何通过纯css实现这样的效果呢?实现的难点(1)位于多行文本右下角的展开收起按钮。(2)展开和收起两种状态的切换。(3)文本...
- css 垂直居中的几种实现方式
-
前言设计是带有主观色彩的,同样网页设计中的css一样让人摸不头脑。网上列举的实现方式一大把,或许在这里你都看到过,但既然来到这里我希望这篇能让你看有所收获,毕竟这也是前端面试的基础。实现方式备注:...
- WordPress固定链接设置
-
WordPress设置里的最后一项就是固定链接设置,固定链接设置是决定WordPress文章及静态页面URL的重要步骤,从站点的SEO角度来讲也是。固定链接设置决定网站URL,当页面数少的时候,可以一...
- 面试发愁!吃透 20 道 CSS 核心题,大厂 Offer 轻松拿
-
前端小伙伴们,是不是一想到面试里的CSS布局题就发愁?写代码时布局总是对不齐,面试官追问兼容性就卡壳,想跳槽却总被“多列等高”“响应式布局”这些问题难住——别担心!从今天起,咱们每天拆解一...
- 3种CSS清除浮动的方法
-
今天这篇文章给大家介绍3种CSS清除浮动的方法。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。首先,这里就不讲为什么我们要清楚浮动,反正不清除浮动事多多。下面我就讲3种常用清除浮动的...
- 2025 年 CSS 终于要支持强大的自定义函数了?
-
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!1.什么是CSS自定义属性CSS自...
- css3属性(transform)的一个css3动画小应用
-
闲言碎语不多讲,咱们说说css3的transform属性:先上效果:效果说明:当鼠标移到a标签的时候,从右上角滑出二维码。实现方法:HTML代码如下:需要说明的一点是,a链接的跳转需要用javasc...
- CSS基础知识(七)CSS背景
-
一、CSS背景属性1.背景颜色(background-color)属性值:transparent(透明的)或color(颜色)2.背景图片(background-image)属性值:none(没有)...
- CSS 水平居中方式二
-
<divid="parent"><!--定义子级元素--><divid="child">居中布局</div>...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- maven镜像 (69)
- undefined reference to (60)
- zip格式 (63)
- oracle over (62)
- date_format函数用法 (67)
- 在线代理服务器 (60)
- shell 字符串比较 (74)
- x509证书 (61)
- localhost (65)
- java.awt.headless (66)
- syn_sent (64)
- settings.xml (59)
- 弹出窗口 (56)
- applicationcontextaware (72)
- my.cnf (73)
- httpsession (62)
- pkcs7 (62)
- session cookie (63)
- java 生成uuid (58)
- could not initialize class (58)
- beanpropertyrowmapper (58)
- word空格下划线不显示 (73)
- jar文件 (60)
- jsp内置对象 (58)
- makefile编写规则 (58)