feign的实现原理以及springboot如何使用feign实现服务间调用的

## 1.feign是什么(what)

>feign是java实现的一个声明式的http客户端,

## 2.feign的使用(how)

#### 简单使用

1. maven pom.xml

```

    <dependency>

        <groupId>com.netflix.feign</groupId>

        <artifactId>feign-core</artifactId>

        <version>${feign.version}</version>

    </dependency>

```

2. 定义http调用接口

```

package top.soft1010.feign;

import feign.*;

import java.util.Map;

/**

* Created by zhangjifu on 19/10/25.

*/

public interface TestService {

    //直接url path中添加参数

    @RequestLine("GET /feign/get/{id}")

    String test1(@Param("id") Integer id);

    //url带参数

    @RequestLine("GET /feign/get?id={id}")

    String test2(@Param("id") Integer id);

    //add header 注意id: {id}中间的空格不能省略

    @RequestLine("GET /feign/getHeader")

    @Headers(value = {"id: {id}"})

    String test3(@Param("id") String id);

    //headerMap 添加多个header

    @RequestLine("GET /feign/getHeaders")

    String test4(@HeaderMap Map<String, Object> headers);

    //QueryMap 添加多个查询条件

    @RequestLine("GET /feign/queryMap")

    String test5(@QueryMap Map<String, Object> params);

    //指定json串 注意这里最外层的括号需要转义 很无语是不是

    @RequestLine("POST /feign/json")

    @Body("%7B\"id\":{id},\"name\":\"{name}\"%7D")

    @Headers("Content-Type: application/json")

    String test6(@Param("id") Integer id, @Param("name") String name);

    //这里直接将json字符串作为一个整体

    @RequestLine("POST /feign/json")

    @Body("{jsonBody}")

    @Headers("Content-Type: application/json")

    String test7(@Param("jsonBody") String jsonBody);

}

```

3. 调用方法

```

import com.google.gson.Gson;

import feign.Feign;

import org.junit.Test;

import top.soft1010.feign.JsonPo;

import top.soft1010.feign.TestService;

import java.util.HashMap;

import java.util.Map;

/**

* Created by zhangjifu on 19/10/25.

*/

public class FeignTestServiceTest {

    @Test

    public void test1() {

        TestService testService = Feign.builder().target(TestService.class, "http://127.0.0.1:8080");

        String ret = testService.test1(1);

        System.out.println(ret);

    }

    @Test

    public void test2() {

        TestService testService = Feign.builder().target(TestService.class, "http://127.0.0.1:8080");

        String ret = testService.test2(1);

        System.out.println(ret);

    }

    @Test

    public void test3() {

        TestService testService = Feign.builder().target(TestService.class, "http://127.0.0.1:8080");

        String ret = testService.test3("2");

        System.out.println(ret);

    }

    @Test

    public void test4() {

        TestService testService = Feign.builder().target(TestService.class, "http://127.0.0.1:8080");

        Map<String, Object> headerMap = new HashMap<String, Object>();

        headerMap.put("header1", "header1Value");

        headerMap.put("header2", "header2Value");

        String ret = testService.test4(headerMap);

        System.out.println(ret);

    }

    @Test

    public void test5() {

        TestService testService = Feign.builder().target(TestService.class, "http://127.0.0.1:8080");

        Map<String, Object> queryMap = new HashMap<String, Object>();

        queryMap.put("param1", "params1Value");

        queryMap.put("param2", "params2Value");

        String ret5 = testService.test5(queryMap);

        System.out.println(ret5);

    }

    @Test

    public void test6() {

        TestService testService = Feign.builder().target(TestService.class, "http://127.0.0.1:8080");

        String ret6 = testService.test6(1, "zhang");

        System.out.println(ret6);

    }

    @Test

    public void test7() {

        TestService testService = Feign.builder().target(TestService.class, "http://127.0.0.1:8080");

        JsonPo jsonPo = new JsonPo();

        jsonPo.setName("zhang");

        jsonPo.setId(1);

        String ret7 = testService.test7(new Gson().toJson(jsonPo));

        System.out.println(ret7);

    }

}

```

可以看到使用feign调用http接口相当简单,没有多余的代码

#### feign的丰富功能

```

import feign.*;

import feign.codec.Decoder;

import feign.codec.EncodeException;

import feign.codec.Encoder;

import org.junit.Test;

import top.soft1010.feign.TestService;

import java.io.IOException;

import java.lang.reflect.Type;

import java.util.ArrayList;

import java.util.List;

/**

* Created by zhangjifu on 19/10/25.

*/

public class ComplexFeignTest {

    @Test

    public void test() {

        /**

        * 包含更多

        * 拦截器 编码 解码 options(超时时间) 重试 日志

        */

        List<RequestInterceptor> requestInterceptors = new ArrayList<RequestInterceptor>();

        requestInterceptors.add(new MyRequestInterceptor());

        TestService testService = Feign.builder().

                requestInterceptors(requestInterceptors).

                decoder(new MyDecoder()).

                encoder(new MyEncoder()).

                options(new Request.Options(10000, 60000)).

                retryer(new MyRetryer(1000, 10000, 3)).

                logger(new MyLogger()).

                logLevel(Logger.Level.BASIC).

                target(TestService.class, "http://127.0.0.1:8080/");

        testService.test1(1);

    }

    static class MyRequestInterceptor implements RequestInterceptor {

        public void apply(RequestTemplate template) {

            System.out.println("MyRequestInterceptor  " + System.currentTimeMillis() + " 发起请求" + template.url());

        }

    }

    static class MyEncoder extends Encoder.Default {

        public void encode(Object o, Type type, RequestTemplate requestTemplate) throws EncodeException {

            System.out.println("MyEncoder " + type.toString() + "  " + o.toString());

        }

    }

    static class MyDecoder extends Decoder.Default {

        public Object decode(Response response, Type type) throws IOException, FeignException {

            if (response.status() == 200) {

                System.out.println("MyDecoder 请求成功");

            }

            return super.decode(response, type);

        }

    }

    static class MyLogger extends Logger {

        @Override

        protected void log(String configKey, String format, Object... args) {

            System.out.println(String.format("MyLogger " + methodTag(configKey) + format + "%n", args));

        }

        @Override

        protected void logRequest(String configKey, Level logLevel, Request request) {

            if (logLevel.equals(Level.BASIC)) {

                super.logRequest(configKey, logLevel, request);

            }

        }

    }

    static class MyRetryer implements Retryer {

        int maxAttempts;

        long period;

        long maxPeriod;

        int attempt;

        long sleptForMillis;

        public MyRetryer(long period, long maxPeriod, int maxAttempts) {

            this.period = period;

            this.maxAttempts = maxAttempts;

            this.maxPeriod = maxPeriod;

        }

        protected long currentTimeMillis() {

            return System.currentTimeMillis();

        }

        public void continueOrPropagate(RetryableException e) {

            if (this.attempt++ >= this.maxAttempts) {

                throw e;

            } else {

                long interval;

                if (e.retryAfter() != null) {

                    interval = e.retryAfter().getTime() - this.currentTimeMillis();

                    if (interval > this.maxPeriod) {

                        interval = this.maxPeriod;

                    }

                    if (interval < 0L) {

                        return;

                    }

                } else {

                    interval = this.nextMaxInterval();

                }

                try {

                    Thread.sleep(interval);

                } catch (InterruptedException var5) {

                    Thread.currentThread().interrupt();

                }

                this.sleptForMillis += interval;

            }

        }

        long nextMaxInterval() {

            long interval = (long) ((double) this.period * Math.pow(1.5D, (double) (this.attempt - 1)));

            return interval > this.maxPeriod ? this.maxPeriod : interval;

        }

        public Retryer clone() {

            System.out.println("MyRetryer clone");

            return new MyRetryer(this.period, this.maxPeriod, this.maxAttempts);

        }

    }

}

```

可以看到feign调用http接口,支持拦截器,编解码,重试等丰富的功能

## 3.feign的实现原理(why)

feign的核心jar包只有一个 **feign-core-8.18.0.jar**

看一下整个jar包下面的类结构,相对来说比较简单

![image](https://user-gold-cdn.xitu.io/2019/10/30/16e1a5cbcf350d76?w=710&h=1140&f=jpeg&s=112320)

#### 通过源码解析实现原理

``` 

    Feign.builder()

    .requestInterceptors(requestInterceptors)

    .decoder(new MyDecoder())

    .encoder(new MyEncoder())

    .options(new Request.Options(10000, 60000))

    .retryer(new MyRetryer(1000, 10000, 3))

    .logger(new MyLogger())

    .logLevel(Logger.Level.BASIC)

    .target(TestService.class, "http://127.0.0.1:8080/");   

```

1. 通过**构造器模式**构造Feign.Builder对象。

2. target()方法最后调用了ReflectiveFeign.newInstance()方法

3. 核心代码 通过**动态代理**将feign模板化的接口实例化

```

    InvocationHandler handler = factory.create(target, methodToHandler);

    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);

```

4. 根据动态代理的原理,**InvocationHandler**这个接口的实现既是具体调用逻辑所在

```

static class FeignInvocationHandler implements InvocationHandler {

    private final Target target;

    private final Map<Method, MethodHandler> dispatch;

    FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {

      this.target = checkNotNull(target, "target");

      this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);

    }

    @Override

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

      if ("equals".equals(method.getName())) {

        try {

          Object

              otherHandler =

              args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;

          return equals(otherHandler);

        } catch (IllegalArgumentException e) {

          return false;

        }

      } else if ("hashCode".equals(method.getName())) {

        return hashCode();

      } else if ("toString".equals(method.getName())) {

        return toString();

      }

      //看看这个dispatct.get()是如何实现的?

      return dispatch.get(method).invoke(args);

    }

```

```

@Override

  public Object invoke(Object[] argv) throws Throwable {

    RequestTemplate template = buildTemplateFromArgs.create(argv);

    Retryer retryer = this.retryer.clone();

    while (true) {

      try {

        return executeAndDecode(template);

      } catch (RetryableException e) {

        retryer.continueOrPropagate(e);

        if (logLevel != Logger.Level.NONE) {

          logger.logRetry(metadata.configKey(), logLevel);

        }

        continue;

      }

    }

  }

```

最终调用了Client类中的默认实现

![image](https://user-gold-cdn.xitu.io/2019/10/30/16e1a54c334e7c77?w=1542&h=1134&f=jpeg&s=213169)

**结论:feign最核心的逻辑还是利用了放射&动态代理设计模式最终还是调用了java API实现http接口的调用。**

## 4.feign与其他http客户端对比

直接通过代码比较一下几个http调用

#### java原生调用http接口

```

public static String sendGet(String url, String param) {

        String result = "";

        BufferedReader in = null;

        try {

            String urlNameString = url + "?" + param;

            URL realUrl = new URL(urlNameString);

            // 打开和URL之间的连接

            URLConnection connection = realUrl.openConnection();

            // 设置通用的请求属性

            connection.setRequestProperty("accept", "*/*");

            connection.setRequestProperty("connection", "Keep-Alive");

            connection.setRequestProperty("user-agent",

                    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");

            // 建立实际的连接

            connection.connect();

            // 获取所有响应头字段

            Map<String, List<String>> map = connection.getHeaderFields();

            // 遍历所有的响应头字段

            for (String key : map.keySet()) {

                System.out.println(key + "--->" + map.get(key));

            }

            // 定义 BufferedReader输入流来读取URL的响应

            in = new BufferedReader(new InputStreamReader(

                    connection.getInputStream()));

            String line;

            while ((line = in.readLine()) != null) {

                result += line;

            }

        } catch (Exception e) {

            System.out.println("发送GET请求出现异常!" + e);

            e.printStackTrace();

        }

        // 使用finally块来关闭输入流

        finally {

            try {

                if (in != null) {

                    in.close();

                }

            } catch (Exception e2) {

                e2.printStackTrace();

            }

        }

        return result;

    }

```

#### httpclient

```

CloseableHttpClient closeableHttpClient = HttpClients.createDefault();

        URI uri = new URIBuilder().

                setScheme("https").

                setHost("www.google.com").

                setPath("/search").

                setParameter("q", "张").

                setParameter("btnG","Google Search").

                setParameter("oq","").

                setParameter("aq","f").

                build();

        CloseableHttpResponse closeableHttpResponse = null;

        try {

            HttpGet httpGet = new HttpGet(uri);

            System.out.println(httpGet.getURI().toString());

            closeableHttpResponse = closeableHttpClient.execute(httpGet);

            System.out.println(closeableHttpResponse.getStatusLine().getStatusCode());

        } catch (IOException e) {

            e.printStackTrace();

        } finally {

            if (closeableHttpResponse != null) {

                try {

                    closeableHttpClient.close();

                } catch (IOException e) {

                    e.printStackTrace();

                }

            }

        }

```

#### okhttp

```

        String url = "http://127.0.0.1:8080/feign/get/1";

        OkHttpClient client = new OkHttpClient();

        Request request = new Request.Builder()

                .url(url)

                .addHeader("1", "2")

                .build();

        String ret = "";

        try (Response response = client.newCall(request).execute()) {

            ret = response.body().string();

        }

        System.out.println(ret);

```

不同的http调用的对比

|原生API|httpClient|okhttp|feign|

|---|---|---|---|

|代码量最多|对比代码量比原生好很多,支持所有http调用|相对httpclient代码量更少|代码量最少,书写最简单,但是仅支持最常用的调用|

## 5.feign在spring boot中的应用及如何运行的

#### feign在spring boot中的应用

1. 直接写对应的模板化接口,使用注解添加对应url以及参数等信息

```

package top.soft1010.feign.client;

import org.springframework.cloud.netflix.feign.FeignClient;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestParam;

/**

* Created by zhangjifu on 19/10/28.

*/

@FeignClient(name = "test-feign-client", url = "http://127.0.0.1:8080/")

public interface FeignConsumer {


    @RequestMapping(value = "/feign/get/{id}")

    String test1(@PathVariable(value = "id") Integer id);

    @RequestMapping(value = "/feign/get/")

    String test2(@RequestParam(value = "id") Integer id);

}

```

2. 直接注入接口bean就可以想使用本地方法一样使用了。

```

import org.junit.Test;

import org.junit.runner.RunWith;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import top.soft1010.feign.client.FeignConsumer;

import top.soft1010.feign.client.TestAppApplication;

/**

* Created by zhangjifu on 19/10/28.

*/

@RunWith(SpringJUnit4ClassRunner.class)

@SpringBootTest(classes = TestAppApplication.class)

public class SpringFeignTest {

    @Autowired

    FeignConsumer feignConsumer;

    @Test

    public void test() {

        System.out.println(feignConsumer.test1(1));

    }

    @Test

    public void test2() {

        System.out.println(feignConsumer.test2(1));

    }

}

```

#### spring boot(cloud)是如何集成feign,并且像使用本地方法一下实现远程调用的。

下面跟着源码走进feign

1. spring boot启动 SpringApplication.run()

```

package top.soft1010.feign.client;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

//import org.springframework.cloud.netflix.feign.EnableFeignClients;

import org.springframework.cloud.netflix.feign.EnableFeignClients;

import org.springframework.context.ApplicationContext;

import org.springframework.context.annotation.ComponentScan;

import java.util.Arrays;

/**

* Created by zhangjifu on 19/10/25.

*/

@SpringBootApplication

@EnableFeignClients

@ComponentScan(basePackages = "top.soft1010")

public class TestAppApplication {

    final static Logger logger = LoggerFactory.getLogger(TestAppApplication.class);

    public static void main(String[] args) {

        // 启动Sprign Boot

        ApplicationContext ctx = SpringApplication.run(TestAppApplication.class, args);

        String[] beanNames = ctx.getBeanDefinitionNames();

        Arrays.sort(beanNames);

        for (String beanName : beanNames) {

            logger.info(beanName);

        }

    }

}

```

2. 启动过程中核心方法 refresh()方法

-  refresh()方法在启动过程中会执行两次,第一次是environmentPrepared的时候执行,我们关注的是第二次执行。

-  其中invokeBeanFactoryPostProcessors(beanFactory)这个方法注册BeanDefinition到beanFactory

-  finishBeanFactoryInitialization(beanFactory)初始化bean实例,但是这里不一定会实例化,一般都是懒加载

```

public void refresh() throws BeansException, IllegalStateException {

synchronized (this.startupShutdownMonitor) {

// Prepare this context for refreshing.

prepareRefresh();

// Tell the subclass to refresh the internal bean factory.

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();

}

}

}

```

3. 如何注册BeanDefinition到beanFactory

一直debug进去发现与这样一段代码,简单点说就是ImportBeanDefinitionRegistrar的实现类会产生对应的BeanDefinition到beanFactory

```

                else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {

// Candidate class is an ImportBeanDefinitionRegistrar ->

// delegate to it to register additional bean definitions

Class<?> candidateClass = candidate.loadClass();

ImportBeanDefinitionRegistrar registrar =

BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);

ParserStrategyUtils.invokeAwareMethods(

registrar, this.environment, this.resourceLoader, this.registry);

configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());

}

```

FeignClientsRegistrar就是实现了这个接口

```

private void registerFeignClient(BeanDefinitionRegistry registry,

AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {

String className = annotationMetadata.getClassName();

//这里, FeignClientFactoryBean.class 声明的@FeignClient的接口最后就是实例化成这个类的对象

BeanDefinitionBuilder definition = BeanDefinitionBuilder

.genericBeanDefinition(FeignClientFactoryBean.class);

validate(attributes);

definition.addPropertyValue("url", getUrl(attributes));

definition.addPropertyValue("path", getPath(attributes));

String name = getName(attributes);

definition.addPropertyValue("name", name);

definition.addPropertyValue("type", className);

definition.addPropertyValue("decode404", attributes.get("decode404"));

definition.addPropertyValue("fallback", attributes.get("fallback"));

definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));

definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);

String alias = name + "FeignClient";

AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();

boolean primary = (Boolean)attributes.get("primary"); // has a default, won't be null

beanDefinition.setPrimary(primary);

String qualifier = getQualifier(attributes);

if (StringUtils.hasText(qualifier)) {

alias = qualifier;

}

BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,

new String[] { alias });

BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);

}

```

![image](https://user-gold-cdn.xitu.io/2019/10/30/16e1a54c33a60d09?w=1876&h=1232&f=jpeg&s=689073)

![image](https://user-gold-cdn.xitu.io/2019/10/30/16e1a54c33cb7ea1?w=1666&h=940&f=jpeg&s=234671)

4. bean Initialization

FeignClientFactoryBean实现了FactoryBean接口,这个接口实现方法getObject()是返回spring bean调用的方法

```

public Object getObject() throws Exception {

FeignContext context = applicationContext.getBean(FeignContext.class);

Feign.Builder builder = feign(context);

if (!StringUtils.hasText(this.url)) {

String url;

if (!this.name.startsWith("http")) {

url = "http://" + this.name;

}

else {

url = this.name;

}

url += cleanPath();

return loadBalance(builder, context, new HardCodedTarget<>(this.type,

this.name, url));

}

if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {

this.url = "http://" + this.url;

}

String url = this.url + cleanPath();

Client client = getOptional(context, Client.class);

if (client != null) {

if (client instanceof LoadBalancerFeignClient) {

// not lod balancing because we have a url,

// but ribbon is on the classpath, so unwrap

client = ((LoadBalancerFeignClient)client).getDelegate();

}

builder.client(client);

}

Targeter targeter = get(context, Targeter.class);

return targeter.target(this, builder, context, new HardCodedTarget<>(

this.type, this.name, url));

}

```

可以看到这个实现是不是很熟悉,就是通过动态代理代理调用具体方法的过程。

```

Targeter targeter = get(context, Targeter.class);

return targeter.target(this, builder, context, new HardCodedTarget<>(

this.type, this.name, url));

```

## 6.延伸知识点

#### 反射

[参考](https://juejin.im/post/5d9ecb9c5188256eee163fb3)

#### 动态代理

- 代理类在程序运行时创建

- 使用步骤

>步骤1:通过实现InvocationHandler接口创建自己的调用处理器;

步骤2:通过为Proxy类指定ClassLoader对象和一组interface来创建动态代理类;

步骤3:通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;

步骤4:通过构造函数创建动态代理对象,构造时调用处理器对象作为参数被传入。

还有更简便的动态代理实现方法,Proxy的静态方法newProxyInstance已经为我们封装了步骤2到步骤4的过程。

## 7.思考

**mybatis也是通过写一个mapper接口&对应的xml配置就可以直接使用bean调用具体方法,这个实现是否相同???**

- MapperScannerRegistrar类 实现ImportBeanDefinitionRegistrar接口

- MapperFactoryBean 实现FactoryBean接口

```

@Override

  public T getObject() throws Exception {

    return getSqlSession().getMapper(this.mapperInterface);

  }

```

## 8.遗留问题

feign整合httpclient 或者 okhttp ?

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,358评论 0 17
  • 自己备忘,随便写 android网络框架源码解析及对比 android常用网络框架对比 Volley: 特点 基于...
    幻海流心阅读 1,438评论 0 4
  • 设计模式分类 总体来说设计模式分为三大类:创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原...
    lifeline丿毅阅读 1,197评论 0 2
  • 对于开发人员来说,设计模式有时候就是一道坎,但是设计模式又非常有用,过了这道坎,它可以让你水平提高一个档次。而在a...
    WANKUN阅读 254评论 0 2
  • 主要积累一些开发中比较 常用的工具类,部分借鉴于网络,主要来源于平时开发因需求而生的小工具类 13、ArithUt...
    大鸭梨leepear阅读 665评论 0 1