一步步编写SonarQube Plugin

插件不好写?!

插件确实不好写,因为插件是插入庞大的系统当中工作的,那也就意味着写插件需要具备一定的领域知识,包括系统架构、扩展点、业务共性及差异、API及其业务模型对应、安装和测试。而对于开发者而言,学习这些知识的代价绝对是昂贵的。 在《函数式编程思想》一书中,作者Neal Ford提到开发过程当中的两种抽象方式——composable and contextual abstract. 谈及contextual抽象的时候,他把插件系统列为这一抽象中最经典的例子。

Plugin-based architectures are excellent examples of the contextual abstraction. The plug-in API provides a plethora of data structures and other useful context that developers inherit from or summon via already existing methods. But to use the API, a developer must understand what that context provides, and that understanding is sometimes expensive.

大意是开发者能够借助已存在的方法来使用Plugin API中提供的大量数据结构和有用的上下文信息。但是,理解起这些上下文信息有时是很昂贵的。

基于一个共识:开发者的时间都是宝贵的。知道插件难写之后,我的这篇文章才有价值。

理解领域模型

一说写插件,估计大家都会上官网寻找开发指南或者google大量博客来快速完成开发任务。这里不是说这种方式不好,其实一开始我也是这么做的,但是着手开发以后,很快就遭遇处处掣肘。比如:开发sonar plugin,会用到Profile、Rule、LanguageRepository等概念。单从代码层面上看,我们很难理清这些概念所代表的模型和它们之间的关系。所以需要从用户的视角来感受这些领域知识。

而用户视角大部分情况下就是UI界面。

规则(Rules)

我们先看看Rules导航栏,左边的单选框是这些规则的过滤条件。 说明规则包含或者被包含这些属性之下:

Rules

  • Language:规则对应的某种编程语言。
  • Type:规则的类型,比如:缺陷(Bug)、代码坏味道(Code Smell)、易受攻击(Vulnerability)。
  • Tag:规则设置的标签,易于检索。
  • Repository:承载特定语言下各种规则的容器;通过它可以通过规则的键值(ruleKey)检索。
  • Default Severity:触犯规则的严重程度。
    • Blocker:最高等级,阻碍的
    • Critical:高等级,极为严重的
    • Major:较高等级,主要的;默认级别。
    • Minor:较低等级
    • Info:低等级
  • Status:规则现在的状态,可用、废弃还是实验版(Beta)。
  • Avaiable Since:什么时候开始可用。
  • Template:规则模板:比如某些参数可以运行时传入。
  • Quality Profile:挑选特定语言下各种规则组成的配置;其中可以启用或禁用一部分规则。

质量Profile(Quality Profile)

再看看Quality Profiles导航栏,左侧栏显示的是某种语言包含的所有Profiles.

Profiles

从关系型数据库的角度,Language和Profile是1对多(one-to-many)关系,但是从领域建模的角度,Profile其实和Language是1对1的关系。所以可以是Profile包含Language属性。利用领域建模的思考方式,可以联想到Repository和Rules是1对多的关系,所以Repository包含一个Rules的集合。Repository和Language是1对1的关系,Repository包含Language属性。那么Rules和Profiles的对应关系呢?多对多。但是我们更关心Profile到Rules这一层的关系,所以选择Profile包含一个Rules的集合。

我整理出这样一份对应关系图:

profile
    - language
    - [rules]
respository
    - lanuage
    - [rules]

现在,缺少Profile和Repository的关系。不过既然有了Rule这一层联系,那么就可以这样考虑,Rule和Repository是1对1的关系(为什么呢?因为每个Rule显然只能存在于一个特定的Repository当中)。所以原图可以修改为:

profile
    - language
    - [rules]
      - rule
        - respository
respository
    - language
    - [rules]

好了。梳理完这些领域知识,我们可以开始依照官方的教程Developing a Plugin.

扫描特定领域语言(DSL)的SonarQube插件

SonarQube 5.6现在只支持Java 8、Maven 3.1以上。当然也支持Gradle。

第一步 创建一个Maven工程

这里有两种方式。第一种方式就是从头开始写起,包括创建工程;另一种就是拷贝官方的样例程序。我自然是推荐第二种做法,不过这里我从零开始开发。

$ mvn archetype:create -DgroupId=com.lambeta -DartifactId=sonar-lambeta -DarchetypeArtifactId=maven-archetype-quickstart

依照官方文档将pom.xml修改如下:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.lambeta</groupId>
    <artifactId>sonar-custom</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>sonar-plugin</packaging>

    <name>sonar-custom</name>
    <url>https://www.lambeta.com</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.sonarsource.sonarqube</groupId>
            <artifactId>sonar-plugin-api</artifactId>
            <!-- minimal version of SonarQube to support. Note that the groupId was "org.codehaus.sonar" before version 5.2 -->
            <version>5.6</version>
            <!-- mandatory scope -->
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>net.sourceforge.pmd</groupId>
            <artifactId>pmd-xml</artifactId>
            <version>5.4.2</version>
        </dependency>
        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
                <artifactId>sonar-packaging-maven-plugin</artifactId>
                <version>1.16</version>
                <extensions>true</extensions>
                <configuration>
                    <pluginClass>com.lambeta.CustomPlugin</pluginClass>
                    <pluginDescription>how to write sonar plugin</pluginDescription>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

注意: pmd-xml、dom4j会在后面的编程当中使用到。

依据标准的代码结构,新建CustomPlugin.java文件。

├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── lambeta
│   │   │           ├── CustomPlugin.java

第二步 识别扩展点

此时,该去查看API Basics了。不过在写代码之前,还得先了解所谓的扩展点(Extension Points)。

Scanner, which runs the source code analysis Compute Engine, which consolidates the output of scanners, for example by computing 2nd-level measures such as ratings aggregating measures (for example number of lines of code of project = sum of lines of code of all files) assigning new issues to developers persisting everything in data stores Web application

翻译如下

  • 扫描器:分析源代码
  • 计算引擎:聚合扫描器的输出。举例:计算第二轮measures,如打分;聚合measures(举例:工程中所有代码的行数 = 所有文件的代码行的综合);给开发者安排新的问题;持久化。
  • Web应用程序。 翻译还不如不翻译!一言不合,去看例子程序...的注释

这三个扩展点,其实对应于API中的三个接口。

扫描器 -> Sensor
计算引擎 -> MeasureComputer
Web应用程序 -> Widget

第三步 定义Sensor(Scanner)

基于扫描DSL源码的需求,我们需要扩展Sensor这个接口。新建CustomSensor.java如下:

public class CustomSensor implements Sensor

    public void describe(SensorDescriptor descriptor) 
    ...
    public void execute(SensorContext context)
    ...

接下来,我们需要定义这门DSL语言的某些属性,以便于识别以及扫描时过滤相关的源文件(通过文件的后缀)。

第四步 定义语言(Language)

新建CustomLanguage如下:

package com.lambeta;
import org.sonar.api.resources.AbstractLanguage;

public class CustomLanguage extends AbstractLanguage {
    public static final String KEY = "custom-key";
    public static final String NAME = "custom-name";

    public CustomLanguage() {
        super(KEY, NAME);
    }

    public String[] getFileSuffixes() {
        return new String[] {"csm.xml"}; //custom这门基于xml的内部DSL的文件后缀
    }
}

我定义了一门基于xml语法的内部DSL,其文件的后缀是csm.xml。比如:right-syntax.csm.xml

Language定义出来了,我们还得定义rule、profile和repository. 回到上文提及的language、rule、profile以及repository的关系图:

profile
    - language
    - [rules]
      - rule
        - respository
respository
    - language
    - [rules]

第五步 定义规则(Rule)

respository
    - language
    - [rules]

我们需要实现接口RulesDefinition

package com.lambeta;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.IOUtils;
import org.sonar.api.server.rule.RulesDefinition;
import org.sonar.api.server.rule.RulesDefinitionXmlLoader;

import java.io.InputStream;

public class CustomRulesDefinition implements RulesDefinition {

    public static final String REPOSITORY_KEY = "custom-repo";
    private final RulesDefinitionXmlLoader xmlLoader;

    public CustomRulesDefinition(RulesDefinitionXmlLoader xmlLoader) {
        this.xmlLoader = xmlLoader;
    }

    public void define(Context context) {

        final InputStream stream = getClass().getResourceAsStream("/rules.xml");
        final NewRepository repository = context.createRepository(REPOSITORY_KEY, CustomLanguage.KEY);

        try {
            if (stream != null) {
                xmlLoader.load(repository, stream, Charsets.UTF_8);
            }
            repository.done();
        } finally {
            IOUtils.closeQuietly(stream);
        }
    }
}

我们通过context新建出一个repository。respository需要一个唯一key作为其标识(可以通过setName方法设置名称)以及一个language key来关联(从UI上可以看出来)。然后,通过DI进来的RulesDefinitionXmlLoaderrules.xml中定义的rules加载进repository中。最后,调用reposiotory.done()宣告加载完成。

定义的rules.xml内容如下:

<?xml version="1.0" encoding="UTF-8" ?>
<rules>
    <rule>
        <key>ComponentsMustNotBeFollowedByComponentsRule</key>

        <name>Components标签后不能跟随Components标签规则</name>
        <description>
            <![CDATA[
                Components标签后不能跟随Components标签
            ]]>
        </description>
        <severity>MINOR</severity>
        <cardinality>SINGLE</cardinality>
        <status>READY</status>
        <tag>custom</tag>
        <example>
            <![CDATA[
                <components>
                <!-- Error, components must be here! -->
                    <components/>
                </components>
            ]]>
        </example>
    </rule>
</rules>

包含了rule的key和其他相关的属性。它们最终显示在UI上,会是这样:

Rule

第六步 定义Profile

profile
    - language
    - [rules]
      - rule
        - respository

我们需要实现接口ProfileDefinition.

package com.lambeta;
import org.apache.commons.io.IOUtils;
import org.sonar.api.profiles.ProfileDefinition;
import org.sonar.api.profiles.RulesProfile;
import org.sonar.api.profiles.XMLProfileParser;
import org.sonar.api.utils.ValidationMessages;

import java.io.InputStreamReader;

public class CustomProfileDefinition extends ProfileDefinition {
    private final XMLProfileParser xmlProfileParser;

    public CustomProfileDefinition(XMLProfileParser xmlProfileParser) {
        this.xmlProfileParser = xmlProfileParser;
    }

    @Override
    public RulesProfile createProfile(ValidationMessages validation) {
        final InputStreamReader reader = new InputStreamReader(getClass().getResourceAsStream("/profile.xml"));

        try {
            return xmlProfileParser.parse(reader, validation);
        } finally {
            IOUtils.closeQuietly(reader);
        }
    }
}

使用DI注入的XMLProfileParser解析profile.xml文件,并生成RulesProfile对象。我们来看看profile.xml的内容:

<?xml version="1.0" encoding="utf-8" ?>
<profile>
    <language>custom-key</language>
    <name>Custom Quality</name>
    <rules>
        <rule>
            <repositoryKey>custom-repo</repositoryKey>
            <key>ComponentsMustNotBeFollowedByComponentsRule</key>
            <priority>MAJOR</priority>
        </rule>
    </rules>
</profile>

这里定义一个名为Custom Quality的profile,它关联CustomLanguage的键值:custom-key. 同时包含了多条rules,每条rule拥有自己的标识key以及其所在的repository(事实上,profile会在repository中通过ruleKey来查找rule)。

写到这里,一个DSL的SonarQube Plugin已经几近完善。但是,我们还缺少至关重要的一环——规则的执行!

第七步 运行PMD扫描代码

PMD简介

我们需要一个静态扫描工具来扫描源代码,发现这些代码存在的缺陷和坏味道。PMD就是这么一款好用的工具。

PMD is a source code analyzer. It finds common programming flaws like unused variables, empty catch blocks, unnecessary object creation, and so forth. It supports Java, JavaScript, PLSQL, Apache Velocity, XML, XSL.

翻译: PMD是一款源码分析工具。它会发现编程中的普遍缺陷,如未使用的变量、空的catch块、不必要的对象创建等等。它支持分析Java、Javascript、PLSQL、Apache Velocity、XML、XSL语言。

前面提到我定义的是一门基于XML的DSL,那么理所当然,可以借助PMD,扩展XML的扫描规则来满足自己的需求。

PMD在命令行中执行的方式如下:

pmd -d src/ -f xml -R myrule.xml -r dest/report.xml
  • -d 代表要扫描的源码目录
  • -f 代表报告输出的格式
  • -R 代表采用哪些规则来扫描源代码
  • -r 代表报告的输出路径

注意:这里PMD的规则和SonarQube中的规则其实没有太大关系,属于两种事物。不过,为方便后续提取PMD输出的报告,需要将PMD规则的名字和Sonar规则的键值保持一致。

我们定义PMD需要使用到的规则集custom-pmd-rules.xml

<?xml version="1.0"?>
<ruleset name="ExamplePmdRuleset"
         xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">

    <description>
        Example set of configured PMD rules
    </description>

    <rule name="ComponentsMustNotBeFollowedByComponentsRule"
          message="Components tags followed by components tag found!"
          language="xml"
          class="net.sourceforge.pmd.lang.rule.XPathRule">

        <description>
            Tag components must not be followed by components tag.
        </description>

        <priority>1</priority>

        <properties>
            <property name="xpath">
                <value>//components/components</value>
            </property>
        </properties>

        <example>
            <![CDATA[
                <components>
                    <components>
                </components>
            ]]>
        </example>
    </rule>
</ruleset>

这里的类net.sourceforge.pmd.lang.rule.XPathRule来自于我们先前在pom.xml中声明的pmd-xml这个依赖包。它可以让我们通过设置xpath这一属性的值来构建各种不同规则。扫描中XML文件一旦匹配这些xpath规则,就会输出错误报告。

ComponentsMustNotBeFollowedByComponentsRule这个自定义的规则为例。顾名思义,Components元素下不能再跟着Components元素。它在PMD扫描过程中如果被匹配上,会输出这样的报告:

<?xml version="1.0" encoding="UTF-8"?>
<pmd version="5.4.2" timestamp="2016-06-23T23:06:04.120">
    <file name="/Users/qianyan/github/sonar/sonar-custom/src/test/resources/wrong-syntax-but-not-csm.xml">
        <violation beginline="4"
                   endline="4"
                   begincolumn="5"
                   endcolumn="17"
                   rule="ComponentsMustNotBeFollowedByComponentsRule"
                   ruleset="ExamplePmdRuleset"
                   priority="1">
            Components tags followed by components tag found!
        </violation>
    </file>
    <file name="/Users/qianyan/github/sonar/sonar-custom/src/test/resources/wrong-syntax.csm.xml">
        <violation beginline="4"
                   endline="4"
                   begincolumn="5"
                   endcolumn="17"
                   rule="ComponentsMustNotBeFollowedByComponentsRule"
                   ruleset="ExamplePmdRuleset"
                   priority="1">
            Components tags followed by components tag found!
        </violation>
    </file>
</pmd>

PMD报告转化为Sonar的Issue

由于PMD是由Java编写的,所以我们可以在代码中调用PMD这个类net.sourceforge.pmd.PMD根据我们写好的PMD规则,来扫描Sonar指定的目录及其文件。最后,将PMD输出的XML格式的报告转化成Sonar能够理解的Issue。

代码如下:

public void execute(SensorContext context) {
        File reportFile = new File(context.fileSystem().workDir(), "report.xml"); // 1
        runPMD(context, reportFile); // 2
        convertToIssues(context, doc(reportFile)); // 3
}
  1. 指定PMD输出文件的路径;
  2. 运行PMD,输出XML格式的报告到1指定的文件当中;
  3. 解析报告,并转化为Issue。

下面我们一步步来解释对应的代码:

  • runPMD
    private void runPMD(SensorContext context, File reportFile) {
        final String dir = context.settings().getString("sonar.sources");
        final File file = new File(dir);
        String[] pmdArgs = {
                "-f", "xml",
                "-R", "custom-pmd-rules.xml",
                "-d", dir,
                "-r", reportFile.getAbsolutePath(),
                "-e", context.settings().getString("sonar.sourceEncoding"),
                "-language", "xml",
                "-version", "1.0"
        };
        final ClassLoader loader = Thread.currentThread().getContextClassLoader();
        try {
            Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
            PMD.run(pmdArgs);
        } finally {
            Thread.currentThread().setContextClassLoader(loader);
        }
    }

我们通过PMD这个类运行pmdArgs。这里值得注意的是自SonarQube 5.6之后,我们可以通过context.settings()来获取工程的配置了,而不像以前那样依赖注入Settings对象了。

至于Thread.currentThread().setContextClassLoader(getClass().getClassLoader());这步操作和Sonar使用独立的classLoader加载自己的类有关。

  • convertToIssues
private void convertToIssues(SensorContext context, Document doc) {
    final Element root = doc.getRootElement();
    final List<Element> files = root.elements("file");
    for (Element file : files) {

        final List<Element> violations = file.elements("violation");
        final String filePath = file.attributeValue("name");
        final FileSystem fs = context.fileSystem();
        final InputFile inputFile = fs.inputFile(fs.predicates().hasAbsolutePath(filePath));
        if (inputFile == null) {
            LOG.info("fs predicates that there is no {}", filePath);
            continue;
        }
        for (Element violation : violations) {
            final String rule = violation.attributeValue("rule");
            final int beginLine = Integer.parseInt(violation.attributeValue("beginline"));
            final int endLine = Integer.parseInt(violation.attributeValue("endline"));
            final int beginColumn = Integer.parseInt(violation.attributeValue("begincolumn"));
            final int endColumn = Integer.parseInt(violation.attributeValue("endcolumn"));
            final NewIssue newIssue = context.newIssue()
                    .forRule(RuleKey.of(CustomRulesDefinition.REPOSITORY_KEY, rule));
            final NewIssueLocation newIssueLocation = newIssue
                    .newLocation()
                    .on(inputFile)
                    .at(inputFile.newRange(beginLine, beginColumn, endLine, endColumn))
                    .message(violation.getText());
            newIssue.at(newIssueLocation).save();
        }
    }
}

这里主要是对PMD生成XML报告的解析和转换。比较需要关注是这块代码:

final InputFile inputFile = fs.inputFile(fs.predicates().hasAbsolutePath(filePath));
if (inputFile == null) {
    LOG.info("fs predicates that there is no {}", filePath);
    continue;
}

InputFile这是Sonar定义的合法的待扫描文件。举个例子:我们定义了一门基于XML的DSL,其文件的后缀是csm.xml,那么合法的待扫描文件就只能是这个后缀的文件了。像上述PMD输出的那份报告中出现的

<file name="/Users/qianyan/github/sonar/sonar-custom/src/test/resources/wrong-syntax-but-not-csm.xml">

就是不合法的。这个文件是以xml作为后缀的,PMD肯定可以扫描它,但是对于Sonar而言,它并不是InputFile(如果不作处理,就会返回null),所以我们需要在转换为Issue之前剔除掉。

最后,不要忘记保存,newIssue.at(newIssueLocation).save();

Issue呈现在UI上,是这样的:

Issue

第八步 注册所有组件

现在所有的组件已经就绪,是时候将这些组件注册进插件当中了。还记得第一步我们创建的CustomPlugin.java? 所有上述组件,包括Language、Rules、Profiles以及Sensor都得在这个类中进行注册。代码如下:

package com.lambeta;

import org.sonar.api.Plugin;

public class CustomPlugin implements Plugin {
    public void define(Context context) {
        context.addExtension(CustomLanguage.class)
                .addExtension(CustomRulesDefinition.class)
                .addExtension(CustomProfileDefinition.class)
                .addExtension(CustomSensor.class);
    }
}

到此,这个插件算是写完了。那么接下来的问题就是如何运行它?

使用插件扫描工程

下载sonarqube docker镜像

最易于调试的地方莫过于本地了。如果机器是Mac,建议使用Kitematic这个Docker的客户端下载sonarqube的官方镜像,同时将映射的Port定在9000端口上,启动该镜像的容器实例。

sonarqube docker

构建和Copy插件包

在插件的工程根目录下,运行

mvn clean package

然后执行

cp target/sonar-custom-1.0-SNAPSHOT.jar /Users/your-name/Documents/Kitematic/sonarqube/opt/sonarqube/extensions/plugins

如果plugins目录不存在,可以手动创建。执行完命令之后,重启容器。

安装Maven的sonar插件

<!-- settings.xml -->
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
    https://maven.apache.org/xsd/settings-1.0.0.xsd">
    <localRepository/>
    <interactiveMode/>
    <usePluginRegistry/>
    <offline/>
    <pluginGroups>
        <pluginGroup>org.sonarsource.scanner.maven</pluginGroup>
    </pluginGroups>
    <profiles>
        <profile>
            <id>sonar</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <!-- Optional URL to server. Default value is http://localhost:9000 -->
                <sonar.host.url>
                  http://192.168.99.100:9000
                </sonar.host.url>
            </properties>
        </profile>
     </profiles>
    <servers/>
    <mirrors/>
    <proxies/>
    <activeProfiles/>
</settings>

将这个settings.xml的文件放到~/.m2下。

运行Maven sonar:sonar

mvn sonar:sonar -Dsonar.sources=src/test/resources/ -Dsonar.language=custom-key -X

src/test/resources目录展开如下:

src/test/resources
├── right-syntax.csm.xml
├── wrong-syntax-but-not-csm.xml
└── wrong-syntax.csm.xml

然后,根据输出提示,访问 http://192.168.99.100:9000/dashboard/index/com.lambeta:sonar-custom

总结

Sonar Plugin

Plugin implements details

--更新 2017.04.07--

  1. 官网样例链接

[1] 官方教程 [2] 博客 [3] 官方样例 [4] 本文样例

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏WindCoder

微信小程序踩坑记-Java基于SSM下的post请求

最近在持续踩微信小程序的坑,canvas和WebSocket的暂时还没找到相关的解决方案,暂时先将post请求无法获取data参数的坑填上。直接附上解决方案,已...

8861
来自专栏coolblog.xyz技术专栏

Spring IOC 容器源码分析系列文章导读

Spring 是一个轻量级的企业级应用开发框架,于 2004 年由 Rod Johnson 发布了 1.0 版本。经过十几年的迭代,现在的 Spring 框架已...

1403
来自专栏老码农专栏

TodoBackend展示应用以及ActFramework的实现

1285
来自专栏desperate633

深入理解Spring框架的作用(Spring in action 学习笔记)激发POJO的潜能依赖注入应用切面使用模板消除样板式代码

纵览Spring , 读者会发现Spring 可以做非常多的事情。 但归根结底, 支撑Spring的仅仅是少许的基本理念, 所有的理念都可以追溯到Spring最...

2243
来自专栏ImportSource

JDK10要来了:下一代 Java 有哪些新特性?

JDK 10 目前正在Rampdown Phase One,开发正在努力的修复着bug。 排期 2017/12/14 Rampdown Phase One ...

5868
来自专栏张戈的专栏

移动搜索SEO:网站移动适配之Meta标注、移动跳转终结篇

这些天,在给博客的标签页(tag)添加跳转和 META 动态申明时,居然让我醍醐灌顶,发现之前的动态适配的做法是多么的苦逼和小白! 总结前,先来回顾下小白张戈在...

4976
来自专栏小灰灰

Java & PhantomJs 实现html输出图片

Java & PhantomJs 实现html输出图片 借助phantomJs来实现将html网页输出为图片 I. 背景 如何在小程序里面生成一张图,分享到朋...

6898
来自专栏吉浦迅科技

DAY71:阅读Device-side Launch from PTX

我们正带领大家开始阅读英文的《CUDA C Programming Guide》,今天是第71天,我们正在讲解CUDA 动态并行,希望在接下来的30天里,您可以...

1182
来自专栏coolblog.xyz技术专栏

Spring IOC 容器源码分析系列文章导读

Spring 是一个轻量级的企业级应用开发框架,于 2004 年由 Rod Johnson 发布了 1.0 版本。经过十几年的迭代,现在的 Spring 框架已...

28610
来自专栏Java架构师历程

solr

Solr它是一种开放源码的、基于 Lucene Java 的搜索服务器,易于加入到 Web 应用程序中。Solr 提供了层面搜索(就是统计)、命中醒目显示并且支...

3212

扫码关注云+社区

领取腾讯云代金券