0%

所处环境

  • spring boot: 1.5.9.RELEASE
  • spring cloud: Edgware.RELEASE

问题描述

根据netflix hystrix官方的描述,可以通过hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds这个全局配置为所有服务配置默认超时时间。然而在实践中却发现该配置并没有生效(一直是2000ms),但针对各服务进行单独配置却是可以生效的。

问题定位

经google发现,hystrix的timeout机制是通过HystrixTimer来处理的。

HystrixTimer是一个单例,开发人员可以在执行HystrixCommand前通过调用HystrixTimer.getInstance().addTimerListener()方法来添加一个定时的listener,然后在command on completed的时候移除它。相关的代码已经由netflix实现了,可以查看源码AbstractCommand.java

TimerListener listener = new TimerListener() {
    @Override
    public int getIntervalTimeInMilliseconds() {
        return originalCommand.properties.executionTimeoutInMilliseconds().get();
    }
};

final Reference<TimerListener> tl = HystrixTimer.getInstance().addTimerListener(listener);

// set externally so execute/queue can see this
originalCommand.timeoutTimer.set(tl);

/**
* If this subscriber receives values it means the parent succeeded/completed
*/
Subscriber<R> parent = new Subscriber<R>() {
    @Override
    public void onCompleted() {
        if (isNotTimedOut()) {
            // stop timer and pass notification through
            tl.clear();
            child.onCompleted();
        }
    }

    @Override
    public void onError(Throwable e) {
        if (isNotTimedOut()) {
            // stop timer and pass notification through
            tl.clear();
            child.onError(e);
        }
    }
};

从以上代码可以很容易追溯到hystrix超时时间是从originalCommand.properties(HystrixCommandProperties)这个对象中获取的,而在spring cloud提供的AbstractRibbonCommand中存在以下代码

final HystrixCommandProperties.Setter setter = HystrixCommandProperties.Setter()
        .withExecutionIsolationStrategy(zuulProperties.getRibbonIsolationStrategy()).withExecutionTimeoutInMilliseconds(
                RibbonClientConfiguration.DEFAULT_CONNECT_TIMEOUT + RibbonClientConfiguration.DEFAULT_READ_TIMEOUT);

导致originalCommand.properties在构建时可以获取到一个executionTimeoutInMilliseconds的实例级默认值,从而覆盖掉了全局的executionTimeoutInMilliseconds配置,但实例级的配置并不受影响,因为hystrix property优先级为:

  1. Global default from code
  2. Dynamic global default property
  3. Instance default from code
  4. Dynamic instance property

解决方案

  1. 改写HttpClientRibbonCommand的实现
    //todo:: 待补充

  2. 升级spring cloud版本到Edgware.SR1以上
    Spring cloud在Edgware.SR1版本中已经解决了此问题(issue2633),如果有条件可以直接通过升级版本来解决此问题。

参考链接

简介

openfeign

openfeign,简称feign,是netflix开源的技术栈之一。
feign的主旨是使得编写java http客户端更容易。为了贯彻这个理念,feign采用了通过处理注解来自动生成请求的方式(官方称呼为声明式模板化)。因此,基于feign编写的http客户端画风看起来是这样的

interface Bank {
  @RequestLine("POST /account/{id}")
  Account getAccountInfo(@Param("id") String id);
}

然后通过一系列操作可以为Bank接口在运行时自动生成对应的实现。通过这个实现我们就可以在java中像调用一个本地方法一样完成一次http请求,大大减少了编码成本,同时提高了代码可读性。

spring-cloud-openfeign

spring-cloud-feign基于自动配置功能(autoconfiguration),为spring boot应用提供openfiegn的集成:

  • 支持通过JAX-RSSpring MVC annotations来构建feign client
  • 自动使用Spring MVC的HttpMessageConverters来完成序列化反序列化
  • 集成了ribbonhystrix,只要在项目中引入相关的依赖即可立即使用
  • 无需任何配置,开箱即用,秉承了spring-boot一贯的作风。

官方文档参考

常见问题及解决方案

处理响应数据的通用部分

api响应数据格式,一般而言除实际的业务数据外是固定的格式,其中包括了对此次请求的处理信息描述。对于这部分数据,应由feign进行统一处理。

解决方案

feignclient是通过Decoder来对请求响应进行处理的,因此可以使用自定义的Decoder来处理响应数据的通用部分。

例如可以定义一个UnwrapRestfulApiResponseSpringDecoder来对RestfulApiResponse进行自动拆包操作,并通过@FeignClientconfiguration属性配置其使用的Decoder。

@FeignClient(name = AuthCenter.SERVICE_NAME, path = RMIPath.USER, configuration = TenantUserClient.Configuration.class)
public interface TenantUserClient {
    class Configuration {
        @Autowired
        private ObjectFactory<HttpMessageConverters> messageConverters;

        @Bean
        public Decoder feignDecoder() {
            return new ResponseEntityDecoder(new UnwrapRestfulApiResponseSpringDecoder(this.messageConverters));
        }
    }
}

本地调用需要注册到注册中心导致服务不可用的问题

在实际开发过程中,有时会出现需要在本地调用远程服务来进行调试的情况,此时我们需要将本地的应用注册到注册中心。而一旦这样做,会导致该服务存在多个节点(一个远程一个本地),从而导致依赖该服务的应用软负载均衡负载到本地节点时会失败。

解决方案

1. 不通过注册中心获取信息,使用直接指定url的方式调用(✘不推荐)

参考代码

@FeignClient(name = "sysinfo-service" ,url = "http://49.4.7.72", path = "/")
public interface SysinfoClient {
}

此方案优点是简单易用,但缺点也很明显:

  1. 在应用实际上线时需要将调用方式修改为通过注册中心调用,否则client将无法进行负载均衡,因此需要频繁改动代码
  2. 在特定环境下,即使忘记切换调用方式有时也不影响client的使用,会将问题隐藏,埋下隐患
  3. 并没有与其它服务真正地解耦,一旦依赖的服务故障就会导致本地开发无法进行

由于存在上述缺点,现一般不推荐使用该方案。

2. 使用打桩的方式与远程服务解耦(✔推荐)

大致思路为,通过Spring的@Profile注解,在不同的环境为应用注册不同的client bean(例如在本地环境使用Hard Code bean,而在非本地环境则使用远程调用的client bean)。

参考代码
ClientConfiguration.java

@EnableAuthClient
@EnableFeignClients(basePackages = "com.cheegu.icm.biz.foo.client")
@Configuration
@Profile({SpringProfiles.DEV, SpringProfiles.TEST, SpringProfiles.PROD})
public class ClientConfiguration {
}

MockClientConfiguration.java

@Configuration
@Profile(SpringProfiles.LOCAL)
public class MockClientConfiguration {
    @Bean
    public SysinfoClient sysinfoClient() {
        return new SysinfoClient() {
            @Override
            public UserInfoDto user() {
                return new UserInfoDto();
            }

            @Override
            public TableData<UserRowDto> users() {
                return TableData.empty();
            }
        };
    }

    @Bean
    public TenantUserClient tenantUserClient() {
        return new TenantUserClient() {
            @Override
            public List<ResourceInfoDto> listMyAlResource() {
                return new ArrayList<>();
            }
        };
    }
}

如何在调用时带上请求头

在使用客户端进行调用时,有时会需要带上某些请求头(例如认证token),但又不希望在代码中手动去做这些操作。

解决方案

可以通过实现feign提供的RequestInterceptor接口来对请求进行切面处理。这样每次通过client发起请求时都会由feign interceptor进行拦截,为请求添加headers。

参考代码

@Configuration
public class AppConfiguration implements EnvironmentAware {
    private Environment environment;

    @Bean
    public AuthorizationInfoForwardingInterceptor authorizationInfoForwardingInterceptor() {
        //这个interceptor会为请求自动带上认证token
        return new AuthorizationInfoForwardingInterceptor(environment.getProperty("spring.application.name"));
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }
}

指定了get方法,但feign仍然使用了post方法进行请求

如下代码所示

@FeignClient("microservice-provider-user")
public interface UserFeignClient {
  @RequestMapping(value = "/get", method = RequestMethod.GET)
  public User get0(User user);
}

通过@RequestMapping的method属性指定了请求方法为GET,但实际运行会发现feign仍然使用了POST方法进行了调用。

解决方案

这种写法并不正确,正确写法有二:

1. 通过@RequestParam指定url参数名

@FeignClient(name = "microservice-provider-user")
public interface UserFeignClient {
  @RequestMapping(value = "/get", method = RequestMethod.GET)
  public User get1(@RequestParam("id") Long id, @RequestParam("username") String username);
}

2. 当目标url参数较多时,可使用map来构建

@FeignClient(name = "microservice-provider-user")
public interface UserFeignClient {
  @RequestMapping(value = "/get", method = RequestMethod.GET)
  public User get2(@RequestParam Map<String, Object> map);
}

使用MultipartFile类型作为请求参数

有时对外提供的接口调用可能需要传送文件,此时需要对MultipartFile类型的参数进行处理

解决方案

待补充

项目环境

Spring Boot1.5.4 + Shiro1.4.0 + UrlRewriteFilter4.0.4

问题描述

最近在项目中同时用到ShiroUrlRewriteFilter。由于在web环境下两者都是通过配置filter来实现功能的,因此导致出现了冲突——UrlRewriteFilter的outbound rule无法正常工作。具体表现为:

在urlrewrite.xml中配置了

<urlrewrite>
    <outbound-rule>
        <from>/in-rewrite\?type=(\w+)$</from>
        <to>/in/$1.html</to>
    </outbound-rule>
</urlrewrite>

在java代码中定义action

    @GetMapping("out-rewrite")
    @ResponseBody
    public String out(HttpServletRequest request, HttpServletResponse response) {
        return response.encodeURL("localhost:8080/in-rewrite?type=233");       //will be rewrite to 'localhost:8080/in/233.html'
    }

正常来说,访问这个api应该返回”localhost:8080/in/233.html”才对,但却返回了未转换前的url,即”localhost:8080/in-rewrite?type=233”。

问题分析

在调试源码的时候发现,同时配置了ShiroFilter和UrlRewriteFilter时,response的类型是ShiroHttpServletResponse,此时outbound rule是不起作用的。而把ShiroFilter的配置去掉,只留下UrlRewriteFilter时,response的类型则是UrlRewriteWrappedResponse,而此时outbound rule是起作用的。显然,两者在filter里面都对response进行了重新包装,而ShiroFilter执行在后,导致前者的部分功能失效。

再查看上面两个response包装类的源码:

UrlRewriteWrappedResponse.java

...
    public String encodeURL(String s) {
        RewrittenOutboundUrl rou = this.processPreEncodeURL(s);
        if(rou == null) {
            return super.encodeURL(s);
        } else {
            if(rou.isEncode()) {
                rou.setTarget(super.encodeURL(rou.getTarget()));
            }

            return this.processPostEncodeURL(rou.getTarget()).getTarget();
        }
    }
...

ShiroHttpServletResponse.java

...
    public String encodeURL(String url) {
        String absolute = toAbsolute(url);
        if (isEncodeable(absolute)) {
            // W3c spec clearly said
            if (url.equalsIgnoreCase("")) {
                url = absolute;
            }
            return toEncoded(url, request.getSession().getId());
        } else {
            return url;
        }
    }
...

发现两者均对encodeUrlencodeRedirectUrl等四个方法进行了改写。Shiro的包装类执行在后,导致UrlRewriteWrappedResponse的相关代码没有被执行。

解决方案

重排Filter的执行顺序

UrlRewriteWrappedResponse的包装过程执行在后,避免encodeUrl方法被改写。

显然这不是一个好的办法,原因如下:

  1. 导致ShiroHttpServletResponse的方法被改写,可能引出其它未知问题,无异于拆东墙补西墙
  2. 导致ShiroFilter执行在前,可能导致一些访问的url还未被重写,这次访问就被Shiro拦截了

自己实现encodeUrl的逻辑

ShiroHttpServletResponse执行encode前先执行UrlRewriteWrappedResponse的encode代码。

  1. 改写ShiroHttpServletResponse,重写其encode逻辑

     public class CustomShiroHttpServletResponse extends ShiroHttpServletResponse {
         private HttpServletResponse wrapped;
    
         public CustomShiroHttpServletResponse(HttpServletResponse wrapped, ServletContext context, ShiroHttpServletRequest request) {
             super(wrapped, context, request);
             this.wrapped = wrapped;
         }
    
         @Override
         public String encodeRedirectURL(String url) {
             return super.encodeRedirectURL(wrapped.encodeRedirectURL(url));
         }
    
         @Override
         public String encodeRedirectUrl(String s) {
             return super.encodeRedirectUrl(wrapped.encodeRedirectUrl(s));
         }
    
         @Override
         public String encodeURL(String url) {
             return super.encodeURL(wrapped.encodeURL(url));
         }
    
         @Override
         public String encodeUrl(String s) {
             return super.encodeUrl(wrapped.encodeUrl(s));
         }
     }
    
  2. 改写ShiroFilter,使用CustomShiroHttpServletResponse代替ShiroHttpServletResponse

    public class CustomSpringShiroFilter extends AbstractShiroFilter {
     protected CustomSpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
         //这段代码是copy了ShiroFilterFactoryBean$SpringShiroFilter的构造方法中的代码
         super();
         if (webSecurityManager == null) {
             throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
         }
         setSecurityManager(webSecurityManager);
         if (resolver != null) {
             setFilterChainResolver(resolver);
         }
     }
    
     @Override
     protected ServletResponse wrapServletResponse(HttpServletResponse orig, ShiroHttpServletRequest request) {
         //使用CustomShiroHttpServletResponse代替原有的Response Wrapper
         return new CustomShiroHttpServletResponse(orig, getServletContext(), request);
     }
    }
    
  3. 然后改写ShiroFilterFactoryBean的createInstance方法,使用CustomSpringShiroFilter,代替原来的Filter

    public class CustomShiroFilterFactoryBean extends ShiroFilterFactoryBean {
     private static transient final Logger log = LoggerFactory.getLogger(ShiroFilterFactoryBean.class);
    
     @Override
     protected AbstractShiroFilter createInstance() throws Exception {
         //这部分代码与父类相同
         log.debug("Creating Shiro Filter instance.");
    
         SecurityManager securityManager = getSecurityManager();
         if (securityManager == null) {
             String msg = "SecurityManager property must be set.";
             throw new BeanInitializationException(msg);
         }
    
         if (!(securityManager instanceof WebSecurityManager)) {
             String msg = "The security manager does not implement the WebSecurityManager interface.";
             throw new BeanInitializationException(msg);
         }
    
         FilterChainManager manager = createFilterChainManager();
    
         PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
         chainResolver.setFilterChainManager(manager);
    
         //使用CustomSpringShiroFilter代替SpringShiroFilter
         return new CustomSpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
     }
    }
    
  4. 重启项目,访问”/out-rewrite”,结果如下
    outbound rule

问题描述

在Spring Boot环境中使用@MybatisTest注解针对Mapper写单元测试的时候,由于同时引入了通用Mapper,在单元测试中调用通用Mapper的方法进行CRUD时会出现InstantiationException异常

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.builder.BuilderException: Error invoking SqlProvider method (tk.mybatis.mapper.provider.base.BaseSelectProvider.dynamicSQL).  Cause: java.lang.InstantiationException: tk.mybatis.mapper.provider.base.BaseSelectProvider

    at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:77)
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446)
    at com.sun.proxy.$Proxy89.selectOne(Unknown Source)
    at org.mybatis.spring.SqlSessionTemplate.selectOne(SqlSessionTemplate.java:166)
    at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:82)
    at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
    at com.sun.proxy.$Proxy91.selectByPrimaryKey(Unknown Source)
    at com.ikentop.biz.provider.mapper.hh.ImageRecordMapperTest.testSimply(ImageRecordMapperTest.java:33)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:237)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: org.apache.ibatis.builder.BuilderException: Error invoking SqlProvider method (tk.mybatis.mapper.provider.base.BaseSelectProvider.dynamicSQL).  Cause: java.lang.InstantiationException: tk.mybatis.mapper.provider.base.BaseSelectProvider
    at org.apache.ibatis.builder.annotation.ProviderSqlSource.createSqlSource(ProviderSqlSource.java:103)
    at org.apache.ibatis.builder.annotation.ProviderSqlSource.getBoundSql(ProviderSqlSource.java:73)
    at org.apache.ibatis.mapping.MappedStatement.getBoundSql(MappedStatement.java:292)
    at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:81)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:148)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:77)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
    ... 34 more
Caused by: java.lang.InstantiationException: tk.mybatis.mapper.provider.base.BaseSelectProvider
    at java.lang.Class.newInstance(Class.java:427)
    at org.apache.ibatis.builder.annotation.ProviderSqlSource.createSqlSource(ProviderSqlSource.java:85)
    ... 45 more
Caused by: java.lang.NoSuchMethodException: tk.mybatis.mapper.provider.base.BaseSelectProvider.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.newInstance(Class.java:412)
    ... 46 more

单元测试代码如下:

@RunWith(SpringRunner.class)
@MybatisTest
public class ImageRecordMapperTest {
    @Autowired
    private ImageRecordMapper mapper;

    @Test
    public void testSimply() {
        ImageRecord record = mapper.selectByPrimaryKey("1");
        Assert.assertNotNull(record);
        Assert.assertEquals("taccisum", record.getBucket());
        Assert.assertEquals("image1", record.getKey());
        Assert.assertEquals("hash1", record.getHash());
    }
}

分析原因

在github上mapper的issue中有一段作者的回复,说此异常是由于通用方法没有被正确初始化导致。
我这边的情况虽然和这个issue不同,但根本原因是一样的。
查阅Mybatis-Test的文档可知,@MybatisTest注解不同于@SpringBootTest,它只加载保证Mybatis正常运行所需要的configuration。显然通用Mapper作为Mybatis第三方的扩展,并没有被纳入其中,因而默认情况下通用Mapper的starter是不会被加载的,也就导致通用方法不会被初始化。

The @MybatisTest can be used if you want to test MyBatis components(Mapper interface and SqlSession). By default it will configure MyBatis(MyBatis-Spring) components(SqlSessionFactory and SqlSessionTemplate), configure MyBatis mapper interfaces and configure an in-memory embedded database. MyBatis tests are transactional and rollback at the end of each test by default, for more details refer to the relevant section in the Spring Reference Documentation. Also regular @Component beans will not be loaded into the ApplicationContext.
以上是来自mybatis-spring-boot-test-autoconfigure官方文档的描述

解决方案

显然,只要让Mapper的starter在MybatisTest的单元测试启动时得以加载即可解决我们的问题。那么如何做呢?
Spring Boot提供了一个@ImportAutoConfiguration的注解,因此只需要在单元测试的目标类上使用该注解将MapperAutoConfiguration导入即可

@RunWith(SpringRunner.class)
@ImportAutoConfiguration(MapperAutoConfiguration.class)
@MybatisTest
public class ImageRecordMapperTest {
    ……
}

其中MapperAutoConfiguration是通用Mapper的starter的auto-configure类。

发布到Maven仓库

Maven-Publish插件

Gradle将项目发布到Maven仓库需要借助插件,Maven-Publish便是官方推荐的一款插件。
Maven-Publish

发布项目

通过'maven-publish'引入插件
build.gradle

apply plugin: 'maven-publish'

发布到本地

build.gradle

apply plugin: 'java'
apply plugin: 'maven-publish'

group = 'cn.tac.test'
version = '1.0'

publishing {
    publications {
        mavenJava(MavenPublication) {
            from components.java
        }
    }
}
publishing {
    repositories {
        maven {
            url "$buildDir/repo"
        }
    }
}

在以上配置中

  • publishing{}是由插件为项目创建的扩展(PublishingExtension类型),这个扩展提供了两个block:publications{}(MavenPublication类型)和repositories{}(MavenArtifactRepository 类型),分别用于配置要发布的内容目标仓库(均可配置多个)。
  • mavenJava{}被称为发布组件Component,用于配置一项要发布的内容,components.java表示这个组件是通过Java插件添加的java组件,可以简单地理解为就是将当前java项目作为发布内容。
  • maven{}指定了一个要发布的目标仓库,当前指定了当前项目的build目录下的repo目录,即发布到本地

执行publishing -> publish,可以看到项目成功发布到了build/repo目录中。
build/repo

tips

  • 两个publishing{}块的内容也可以合在一起写
  • mavenJava只是一个命名,并无特殊含义,你也可以将其更换为别的名称,但不能与其它发布组件名称重复
  • mavenJava块中还可以配置要发布的artifact的groupidversion,如果不显式指定,则默认采用当前项目的配置

发布源码

在上一步的基础上

  1. 新增一个用于获取源码的task
  2. 配置发布组件,使其发布的同时额外发布一个包含源码的artifact

build.gradle

……
//这个task可以获取到源码
task sourceJar(type: Jar) {
    from sourceSets.main.allJava
}

publishing {
    publications {
        mavenJava(MavenPublication) {
            ……
            //配置额外发布的artifact
            artifact sourceJar {
                //这个字符串会作为artifact文件的后缀
                classifier "sources"
            }
        }
    }
}
……

执行task
sources

发布到远程仓库

只需要修改url指向远程仓库即可。同时由于远程仓库大多需要认证,因此通常需要通过credentials{}指定用户名和密码

build.gradle

……
publishing {
    repositories {
        maven {
            url "http://172.10.10.66:8081/nexus/content/repositories/releases/"
            credentials {
                username = "admin"
                password = "admin123"
            }
        }
    }
}
……

条件发布

这点相信会点groovy的同学都不会觉得难,用if/else就可以完成。例如下面的配置实现根据version后缀来决定是发布到snapshots还是releases仓库中

build.gralde

……
publishing {
    repositories {
        def NEXUS_URL = "http://172.10.10.66:8081/nexus/content/repositories/releases/"
        if (project.version.endsWith("SNAPSHOT")){
            NEXUS_URL = "http://172.10.10.66:8081/nexus/content/repositories/snapshots/"
        }
        maven {
            url NEXUS_URL
            credentials {
                username = "admin"
                password = "admin123"
            }
        }
    }
}
……

发布多项目

父项目的task执行的同时会执行其所有子项目的同一task(最常见的如build任务),因此多项目发布只需要配置好所有子项目的发布配置即可。

build.gradle

subprojects {
    apply plugin: 'java'
    apply plugin: 'maven-publish'

    group "cn.tac.test"
    version "1.0-SNAPSHOT"

    //因为父项目不需要发布,所以只需要配置子项目的发布配置即可
    publishing {
        publications {
            mavenJava(MavenPublication) {
                from components.java
            }
        }
    }
    publishing {
        repositories {
            maven {
                url "$buildDir/repo"
            }
        }
    }
}

执行task,可以看到每个子项目都发布到了其对应的build/repo中。

tips

  • 如果子模块之间互相有依赖,Gradle发布时会自动解析各模块的先后发布顺序,无需我们自己配置

常见问题

发布后pom.xml中项目的依赖scope为runtime的问题

在发布的时候发现,项目中通过complie依赖的第三方库或其它模块,在发布后由Gradle生成的pom.xml文件中,依赖的scope默认变成了runtime,而非我们期望的compile

<?xml version="1.0" encoding="UTF-8"?>
  ……
  <groupId>cn.tac.test</groupId>
  <artifactId>publishing</artifactId>
  <version>1.0-SNAPSHOT</version>
  <dependencies>
    <dependency>
      <groupId>cn.tac.test</groupId>
      <artifactId>module1</artifactId>
      <version>1.0-SNAPSHOT</version>
      <scope>runtime</scope>
    </dependency>
  </dependencies>
</project>

为了达到期望的效果,只需要在发布配置中通过withXML对pom进行修改即可
build.gradle

publishing {
    publications {
        mavenJava(MavenPublication) {
            from components.java

            pom.withXml {
                asNode().dependencies.'*'.findAll() {
                    it.scope.text() == 'runtime' && project.configurations.compile.allDependencies.find { dep ->
                        dep.name == it.artifactId.text()
                    }
                }.each() {
                    it.scope*.value = 'compile'
                }
            }
        }
    }
}

再次发布

<?xml version="1.0" encoding="UTF-8"?>
  ……
  <groupId>cn.tac.test</groupId>
  <artifactId>publishing</artifactId>
  <version>1.0-SNAPSHOT</version>
  <dependencies>
    <dependency>
      <groupId>cn.tac.test</groupId>
      <artifactId>module1</artifactId>
      <version>1.0-SNAPSHOT</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>
</project>

tips

什么是爬虫

简介

爬虫(Web Crawler),也叫网络蜘蛛(Splider),是一种用来自动浏览www网页的程序。
爬虫一般从一个URL出发,访问所有关联的URL,并从中提取出感兴趣的内容。
爬虫访问网站的过程会消耗目标系统资源,所以有不少网络系统并不默许爬虫工作。因此爬虫要考虑到规划负载,并且要讲『礼貌』(参考robots.txt)。

网络爬虫

应用场景

爬虫主要有以下应用场景

  • 科学研究

    如数据挖掘、机器学习、图片处理等

  • Web安全

    如使用爬虫对网站是否存在某一漏洞进行批量验证、利用

  • 产品研发

    如采集各个商城物品价格,为用户提供市场最低价

  • 舆情监控

    如抓取、分析某社交平台的数据,从而识别出某用户是否为水军

  • 搜索引擎

    爬虫是搜索引擎的核心组成部分

爬虫策略

一个爬虫的实现主要由四大策略组成

  • 选择策略

    指定页面下载的策略,可细分为
    链接跟随限制,指的是爬虫只搜索特定类型(如HTML)的资源,避免发出过多的请求。或者避免请求一些带有”?”的资源,避免从网站下载无限量的URL
    URL规范化,指的是以某种一致的方式修改和标准化URL的过程,避免资源的重复爬取
    路径上移爬取,指爬取每个URL里提示的每个路径,例如对于”http: //www.tac.cn/a/b/c.html",还会爬取"/a/b"、"/a"和"/"路径。
    主题爬取,指有条件地进行爬取(例如只爬取与python相关的页面)。

  • 重新访问策略

    网站是经常动态变化的,需要估算URL的新鲜度过时性,以确保是否需要重新访问。

  • 平衡礼貌策略

    使用爬虫可能导致一个站点瘫痪,因此爬虫需要遵守一些协议来避免这个问题。

  • 并行策略

    并行运行多个进程的爬虫时,为了避免重复下载页面,爬虫系统需要策略来处理爬虫运行时新发现的URL。

简单架构

framework

如图,一个最简单的爬虫架构至少包括爬虫调度端URL管理器网页下载器网页解析器。这几个模块相互作用,从而提取出有价值的数据。其时序图如下

framework

爬虫调度器从URL管理器中获取待爬取的URL,交由下载器进行下载。然后将下载器将下载的页面交由解析器进行解析,解析器又反过来将解析到的URL反馈给URL管理器。如此循环往复,直到没有待抓取的URL,则结束爬取。

——以上图片来自慕课网Python开发简单爬虫

URL管理器

管理待抓取的URL集合和已抓取的URL集合,防止重复抓取、防止循环抓取

实现方式

  • 内存
  • 关系数据库
  • NoSQL

网页下载器

  • urllib2 python官方 *
  • requests 第三方

网页下载分几种场景

  • 直接通过url下载
  • 需要添加请求参数,请求头(伪装浏览器)
  • 需要伪装特殊情景,如cookie、proxy、https、http redirect

页面解析器

从网页中提取有价值数据的工具

python的网页解析器

  • 正则表达式
  • html.parser python官方
  • beautiful soup4 第三方 *
  • lxml

前言

第一篇中我们知道了如何构建一个单项目,但仅仅这样是不够的。实现多项目的构建有利于模块化,如此一来我们便能更好地在一个大型项目中分离我们的关注点。

Getting Started

创建根项目

新建一个文件夹作为根项目目录,执行gradle init

$ mkdir multi_project
$ gradle init
$ ls
build.gradle    gradle          gradlew         gradlew.bat     settings.gradle

这次我们要关注的重点有两个文件

  • build.gradle

    配置一些应用于所有子项目的公共配置

  • settings.gradle

    描述各项目之间的关系

创建子项目

在根目录下分别创建sub-project1和sub-project2目录,然后分别为其创建build.gradle

$ mkdir sub-project1 sub-project2
$ touch sub-project1/build.gradle sub-project2/build.gradle

然后在根项目的settings.gradle中添加下面内容以关联子项目

include 'sub-project1'
include 'sub-project2'

tips

  • 一定要注意子项目只需为其创建build.gradle即可,而非使用gradle init指令初始化,这点很重要
  • 子项目的build.gradle只用于配置该项目特有的一些配置项,公共的配置通过根项目的build.gradle配置

公共配置

在根项目的build.gradle中加入以下内容

allprojects{
    group = 'cn.tac'
    version = '0.1'
    repositories{
        maven {
            url 'http://maven.aliyun.com/nexus/content/groups/public/'
        }
    }
}

subprojects {
    apply plugin: 'java'
    sourceCompatibility = 1.8
    dependencies {
        testCompile 'junit:junit:4.12'
    }
}
  • allprojects{} 为所有项目添加配置项,所以group、version、repositories都放在了这个block下
  • subprojects{} 仅仅为当前项目的子项目添加配置项(不包括当前项目本身),所以根项目不需要的内容(如依赖)放在了这个block下

编写代码

分别为子项目创建src目录

$ mkdir -p sub-project1/src/test/java/cn/tac/gradle sub-project2/src/test/java/cn/tac/gradle

并创建单元测试

package cn.tac.gradle;

import org.junit.Assert;
import org.junit.Test;

public class SubProject1Test {
    @Test
    public void testSimply() {
        System.out.println("hello, i'm sub project1");
    }
}
package cn.tac.gradle;

import org.junit.Assert;
import org.junit.Test;

public class SubProject2Test {
    @Test
    public void testSimply() {
        System.out.println("hello, i'm sub project2");
    }
}

执行构建

完成了上述步骤之后,不出意外此时的目录结构应该如下

$ tree .
.
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
├── sub-project1
│   ├── build.gradle
│   └── src
│       └── test
│           └── java
│               └── cn
│                   └── tac
│                       └── gradle
│                           └── SubProject1Test.java
└── sub-project2
    ├── build.gradle
    └── src
        └── test
            └── java
                └── cn
                    └── tac
                        └── gradle
                            └── SubProject2Test.java

接下来我们切换到根目录下,执行

$ sh gradlew build

再查看子项目的目录,发现分别多了一个build目录,可见在为根项目执行构建任务时,其include的所有子项目也分别进行了构建。以下分别是两个子项目的build reports
sub project 1
sub project 2

依赖其它项目

dependencies{}中添加依赖即可,例如sub-project2要依赖sub-project1,可以在sub-project2的build.gradle中添加

dependencies {
  compile project(':sub-project1')
}

build-scan(构建审视)插件

介绍

A build scan is a shareable and centralized record of a build that provides insights into what happened and why. By applying the build scan plugin to your project, you can create a build scan in the Gradle Cloud for free.
Creating Build Scans

大概的意思是build scan能为你提供构建过程中发生的what and why信息,在你构建的时候,插件会抓取数据提交到Gradle Cloud,同时返回一个包含构建信息的链接。

工作流程
overview

配置

配置方式很简单,只需要在build.gradle中加入

plugins {
    id 'com.gradle.build-scan' version '1.9' 
}

添加了插件后,可以通过buildScan块来配置插件,其中有两个license相关的属性是必需要配置

buildScan {
    licenseAgreementUrl = 'https://gradle.com/terms-of-service'
    licenseAgree = 'yes'
}

import changes(如果没有勾选auto-import),可以看到Gradle Project面板上多了个task

tip

  • 如果添加新的plugin,应该确保build-scan总是在第一个位置,否则其之前的插件虽然仍然正常工作,但是无法到抓取相关的构建信息

使用

先执行build -> build,再执行这个build scan -> buildScanPublishPrevious,不出意外可以看到terminal中返回了一个链接

9:50:39 PM: Executing external task 'buildScanPublishPrevious'...
:buildScanPublishPrevious

Publishing build scan...
https://gradle.com/s/af72je4qbzhme

BUILD SUCCESSFUL

Total time: 1.283 secs
9:50:41 PM: External task execution finished 'buildScanPublishPrevious'.

打开链接后可以看到这样一个界面

build scan

接下来在任一个单元测试内加入一行让构建失败

        Assert.fail();

再执行一次构建,打开链接查看
failed build scan

可以看到有以下几大优点:

  • 信息展示非常全面丰富、直观
  • 良好的分类、折叠,让用户自己选择展开感兴趣的内容,非常友好
  • 网页形式,非常易于分享(这一点有点类似AngularJS的错误信息,不过没考察是谁借鉴谁的,或许两者都不是原创?)。

相比之下,这里不得不提到一直被人吐槽的Maven的构建信息,真心是非常不友好😒

tip

  • 如果构建时出现”There is no previous build data available to publish.”,可能是没有先执行任一task。

Application插件

介绍

Application插件可以让你轻松地在本地开发环境下执行JVM应用,同时还可以帮助你将应用打包成一个包含了各类操作系统对应启动脚本的tar and/or zip文件。

The Application Plugin

配置

build.gradle

apply plugin: 'application'
mainClassName = "cn.tac.test.gradle.Application"    //指定程序入口类
//applicationDefaultJvmArgs = ["-Dgreeting.language=en"]      //应用程序启动时的jvm参数

添加了Application插件后,项目会多出以下几个task

  • run
  • startScripts
  • installDist
  • distZip
  • distTar

具体可以通过tasks任务查看

tips

  • 按照官方的说法,Application插件已经隐式地包括了Java插件和Distribution插件,因此如果你原来引入了这两个插件,现在可以去掉了

使用

以我的main函数为例(注意要跟mainClassName属性指定的类一致)

package cn.tac.test.gradle;

import java.util.Arrays;

public class Application {
    public static void main(String[] args) {
        if (args.length > 0) {
             System.out.println("hello, it's you args: " + Arrays.toString(args));
        } else {
             System.out.println("hello, you do not input any args");
        }
    }
}

执行应用

$ sh gradlew run

> Task :run
hello, you do not input any args

如果要传入参数,可以配置一下run任务
build.gradle

run {
    if(project.hasProperty("myArgs")){
      args myArgs
    }
}

上面配置的意思是,如果当前项目的project对象包含有myArgs属性,那么在执行main函数时就将这个属性作为参数传递,之后我们可以这样执行

$ sh gradlew run -PmyArgs="123","abc","qaz"

> Task :run
hello, it s you args: [123,abc,qaz]

其中-PmyArgs分为两部分

  • -P,命令行option。作用是指定一个属性的值run,不能省去
  • myArgs,我们刚刚在run任务中自定义的属性,通过-P指定

打包

执行以下脚本可以进行打包

$ sh gradlew distTar distZip

打包好的内容在/build/distributions中,分别多了一个tar文件和一个zip文件,解压后查看目录结构如下

$ tree .
.
├── bin
│   ├── gradle_cli
│   └── gradle_cli.bat
└── lib
    └── gradle_cli-1.0.jar

tips

  • 当然你也可以通过build任务来打包,build任务会自动将distTardistZip任务包括进去

前言

虽然一直以来都用Maven作为java项目的构建工具,但早就听说过Gradle大名,于是今天终于抽出时间来了解一下这款号称结合了Ant和Maven优点的构建工具。
虽然Gradle支持多种语言,但这个系列的文章主要以Java项目构建为主。由于本人不是做Android开发,所以这个系列的文章可能会更偏向于Java Web开发视角。

学习资源推荐

官方Documentation 内容非常全面,缺点是全英文(对于英语差的同学简直是噩梦)
《跟我学Gradle》 Gralde中文用户组编写的中文系列教程,缺点是还不够完善,有些章节还没有内容
《Gradle In Action》中译版 书我没完整看过,只是查资料时读过几个章节,感觉内容还不错

概貌了解

同类工具比对

Ant
Ant是第一个“现代”构建工具,于2000年发布基于过程式编程的idea,具备插件功能及通过网络进行依赖管理的功能(结合Apache Ivy)。不足之处是采用XML作为脚本编写格式,不符合过程化编程的初衷。

Maven
Maven出现的目的是解决Ant带来的一些问题,发布于2004年。Maven依靠约定并提供现成的可调用的目标,首创了从网络下载依赖的功能。依然采用XML作为配置文件(因此同样有跟Ant一样难以定制化构建过程的缺点)。另外Maven虽然聚焦于依赖管理,但并不能很好地处理相同库文件不同版本之间的冲突(不如Ivy)。

Gradle
Gradle结合了两者的优点,并在此基础上做了许多改进。
Gradle使用基于Groovy的DSL编写构建脚本,可以更细致地控制编译打包过程(这也是为什么Android Studio默认采用Gradle作为构建工具的原因)。
Gradle对多模块项目有很好的支持。
Gradle支持多语言,包括java、groovy、scala、c++等。
Gradle使用Apache Ivy处理依赖,因此依赖管理方面优于Maven。同时Gradle可以使用多种类型的远程仓库,如Maven仓库、Ivy仓库。

关于DSL

DSL是Domain Specific Language的缩写,即领域特定语言。同字面上的意思,就是专用于处理某一领域问题的特定语言,例如用于web页面开发的HTML语言、用于GNU Emacs的Emacs Lisp等,甚至有一些简单的DSL只用于某个单应用程序(也称为Mini-Languages)。

由此可见,Gradle使用的DSL应该是一种专用于项目构建的语言。

更多内容

Getting Started - IntelliJ IDEA

初次接触为了能够快速看到效果,所以直接使用ide来入门。不过为了对Gradle有更深入的了解,往后的练习项目将全部使用命令行构建。

创建项目

  1. New -> Project -> Gradle,新建一个Gradle项目(就像Maven一样,IDEA内置了Gradle,所以不需要我们手动去安装了)
  2. 填写GroupId、ArtifactId、Version(这些跟Maven是一样的)
  3. 这里勾选上Create directories for empty content roots automatically选项,让IDEA帮我们创建好目录结构
  4. Finish,初次构建可能会花费较长的时间(跟Maven一样,要从网络下载一些东西,比如项目模板),构建好后的目录结构如下
    project structure

来看下各folder&file的含义:

  • .gradle

    Gradle相关的支持文件,一般不用关心

  • gradle
    • wrapper

      The wrapper is a small script and supporting jar and properties file that allows a user to execute Gradle tasks even if they don’t already have Gradle installed. Generating a wrapper also ensures that the user will use the same version of Gradle as the person who created the project.
      Creating New Gradle Project

      大意为,wrapper里面是一些简单的脚本、使用户能在没有安装Gradle的情况下也能执行Gradle任务的supporting jarproperties文件等,同时wrapper还能确保用户执行Gradle任务时使用的版本与项目创建者使用的Gradle版本相同。总之是一个开发人员基本不需要关心的目录。
  • src

    源码目录,采用了与Maven相同的结构

  • build.gradle

    Gradle的构建配置文件(build file),需要我们编写内容(类似Maven的pom.xml)。
    按照官方的描述,每个build.gradle都配置了一个org.gradle.api.Project类的实例,并且这个实例会有许多内建的方法和属性(稍后CLI项目中可以看到gradlew properties列出了一堆project的属性)。
    build.gradle的DSL参考

  • gradlew/gradlew.bat

    分别用于类unix系统和windows系统下的wrapper脚本,之后可以看到,创建了wrapper后我们所有的指令都通过wrapper脚本来执行

  • settings.gradle

    与多模块项目配置有关的文件,用于描述项目模块之间的关系

Hello World

创建好项目后可以看到,已经有许多配置好的东西了,如junit依赖、Gradle wrapper等,所以现在已我们直接可以直接写单元测试

public class GettingStarted {
    @Test
    public void testSimply() {
        System.out.println("hello gradle");
    }
}

执行可以看到控制台输出

hello gradle

Getting Started - CLI

使用ide写个Gradle的Hello World确实非常简单,但使用CLI来搭建项目,能让我们对Gradle有更加深入的了解。
接下来我们将尝试用CLI写一个Hello World。

安装Gradle

由于我们这次用的是CLI,所以必须手动安装Gradle。
以我用的MacOS为例,打开terminal,run

$ brew install gradle

安装前必须确保安装了jdk1.7以上版本(我用的Gradle 4.1版本的要求),其它系统用户可以参考Installation

然后run,若已成功安装可以看到

$ gradle --version
------------------------------------------------------------
Gradle 4.1
------------------------------------------------------------

Build time:   2017-08-07 14:38:48 UTC
Revision:     941559e020f6c357ebb08d5c67acdb858a3defc2

Groovy:       2.4.11
Ant:          Apache Ant(TM) version 1.9.6 compiled on June 29 2015
JVM:          1.8.0_121 (Oracle Corporation 25.121-b13)
OS:           Mac OS X 10.12.4 x86_64

创建工程

创建一个空文件夹作为工程目录,同时创建一个build.gradle空文件

$ mkdir gradle_cli
$ cd gradle_cli
$ touch build.gradle

然后执行以下命令生成Gradle Wrapper

$ gradle wrapper

可以看到当前目录的变化

$ tree .
.
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
└── gradlew.bat

现在可以通过wrapper脚本来执行各项任务,这样可以确保在更换了环境后依然能使用创建项目时使用的Gradle版本进行构建。

查看properties信息(以下输出做了大量删减)

$ sh gradlew properties
> Task :properties

------------------------------------------------------------
Root project
------------------------------------------------------------

allprojects: [root project 'gradle_cli']
ant: org.gradle.api.internal.project.DefaultAntBuilder@70e298ee
……
buildDir: /Users/tac/Documents/studyspace/src/java/gradle_cli/build
buildFile: /Users/tac/Documents/studyspace/src/java/gradle_cli/build.gradle
buildScriptSource: org.gradle.groovy.scripts.UriScriptSource@460e8e7a
……
depth: 0
description: null
displayName: root project 'gradle_cli'
……
gradle: build 'gradle_cli'
group:
……
plugins: [org.gradle.api.plugins.HelpTasksPlugin@23fb712b]
……
project: root project 'gradle_cli'
……
projectDir: /Users/tac/Documents/studyspace/src/java/gradle_cli
……
repositories: repository container
resources: org.gradle.api.internal.resources.DefaultResourceHandler@7cad9556
rootDir: /Users/tac/Documents/studyspace/src/java/gradle_cli
rootProject: root project 'gradle_cli'
……
state: project state 'EXECUTED'
status: release
subprojects: []
tasks: task set
version: unspecified

可以看到其中大多数属性都已经有了默认的值,这也恰好验证了Gradle约定优于配置的原则。
如果我们需要修改一些属性值,可以通过写build.gradle文件来进行配置

description = 'A Gradle build project for CLI'
version = '1.0'
group = 'cn.tac.test'

再次查看properties可以看到属性已经更改了

$ sh gradlew properties | grep -E "group|description|version"
description: A Gradle build project for CLI
group: cn.tac.test
version: 1.0

tip

  • 你也可以先生成Wrapper再创建build.gradle,并不会有影响
  • 除了手动创建之外,还可以通过gradle init指令来初始化项目。初始化的内容包括执行gradle wrapper,以及自动生成build.gradle和settings.gradle,并且生成的文件里面已经有了一些自动生成的配置(默认是注释状态,即未启用)。

配置环境

由于我们是手动创建的空build.gradle,要构建java项目,我们还需要做一些简单的配置。
在build.gradle加入目标工程语言(上面提过,Gradle是支持多语言的)及版本、依赖及下载依赖的仓库的配置

apply plugin: 'java'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

tips

  • 可以通过dependencies任务查看当前项目的依赖信息。

Hello World

配置好环境后,我们创建源码目录及单元测试类

$ mkdir -p src/main/java src/main/resources src/test/java src/test/resources
$ cd src/test/java
$ mkdir -p cn/tac/test
$ cd cn/tac/test
$ touch HelloWorld.java

然后执行构建,如果不知道有哪些tasks可以执行,可以通过以下命令来查看

$ sh gradlew tasks

> Task :tasks

------------------------------------------------------------
All tasks runnable from root project - A Gradle build project for CLI
------------------------------------------------------------

Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
……
Build Setup tasks
-----------------
init - Initializes a new Gradle build.
wrapper - Generates Gradle wrapper files.
Documentation tasks
-------------------
javadoc - Generates Javadoc API documentation for the main source code.
Help tasks
----------
……
Verification tasks
------------------
check - Runs all checks.
test - Runs the unit tests.

Rules
-----
……
$ sh gradlew build

完了可以看见项目根目录下多了一个build目录,里面的内容就是执行构建的产物。与Maven不同的是,有个reports目录是Gradle生成的HTML格式的构建报告,可以通过浏览器打开查看
build reports

tip

  • 有没有发现sh gradlew tasks出来的列表有点像IDEA Gradle Project面板上的tasks节点?😃
  • 在项目根目录下使用gradle跟gradlew执行task的效果基本是一样的,区别在于gradle会使用本地安装的Gradle版本进行构建,而gradlew会使用创建项目时使用的gradle版本进行构建,如果本地没有搜索到这个版本,则会自动下载