NO END FOR LEARNING

Writing blog if you feel tired | 学海无涯 苦写博客

丢掉IDE,回到Java的第一堂课

| Comments

最近,《实现领域驱动设计》的译者,ThoughtWorks的“滕老板”(腾云)给我们上了一堂基础课,就是没有IDE的情况下,去编译和运行用文本编辑器编写的Java代码。

这本应该是基础中的基础,我记得应该是Java第一堂课就应该讲到的东西。但是我们这群用惯了IDE的程序员,在没有了IDE之后,就开始不知所措了。虽然不知道这些知识,对于普通开发似乎是没有影响,但毕竟是Java的重要基础知识,了解这些知识可以帮助你理解IDE帮你做了什么,可以更快速的定位问题。

还是举例子,滕老板的例子:

目录结构

src

–client

—-Client.java

–service

—-Service.java

1
2
3
4
5
6
7
8
package service;

public class Service {
  public String run(){
      String str = new String("as your service");
      return str;
  }
}
1
2
3
4
5
6
7
8
9
10
package client;

import service.*;

public class Client {
  public static void main(String args[]){
      Service service = new Service();
      System.out.println(service.run());
  }
}

ok,代码写完了,你知道该怎么运行吗?

命令行:进入到src目录下

1
2
javac client/Client.java
java client.Client

这样就会输出正确结果:as your service。

再来看一下目录结构

src

–client

—-Client.java

—-Client.class

–service

—-Service.java

—-Service.class

我们知道javac命令编译的是.java文件,而java命令运行是.class文件。

但是这里还是有几个疑问:

1.javac client/Client.java时,它怎么知道去哪里找Service类?

2.对于上一个疑问,你的答案可能是import service.*;告诉它去service包下找,那么第二疑问,为什么是import service.*;而不是import src.service.*?

3.第三个疑问和第一个疑问类似,答案也类似,java client.Client,怎么知道去哪里找service.class文件?

面对这三个问题,答案是,要引入两个概念。

sourcepath和classpath。

classpath大家听得比较多,用来告诉JVM或者Java编译器到哪里去寻找用户定义的类和包。

sourcepath和classpath一样,只不过它是用来告诉编译器到哪里去寻找源文件。

在上面的javac和java命令中,既没有显示的指定sourcepath,也没有显示的指定classpath。但是它们都有默认值,就是将当前目录作为各自的路径。

比如:我现在从当前路径src,进入到它的一个子目录中src/sub。

然后我运行:javac ../client/Client.java命令

它提示我找不到Service类。 那是因为当前的sourcepath是src/sub,在这个路径下没有Service.java。我们改一下,显示的设置它sourcepath。

运行命令:javac -sourcepath ../ ../client/Client.java

编译成功。

那么运行呢?

直接在src/sub路径下,运行:java client.Client。能行吗?肯定不行。

Error: Could not find or load main class client.Client

必须要显示的指定它的classpath为上一级目录:

java -classpath ../ client.Client

ok,成功。

现在再来让我们回到刚才的第二个问题。为什么import的不是src.service.* ,因为我们设置它的sourcepath,它就成为一个相对路径。

这不禁让我想到一个问题。package和import到底有什么用?为什么package名字和目录结构一致?

这不是白痴问题吗?

package是作为命名空间,因为不同开发商,开发出来的类库可能会有相同的类名。为了区分,防止冲突,用不同的包名区分。

import是根据你要使用的类,从classpath下找到你要使用的类,并引入到你的类中。

我做了一个实验:

将Service类的package定义,改为service.something,这样包名和目录结构就不一致了。

1
2
3
4
5
6
7
package service.something;

public class Service {
  public String run(){
      return "as your service";
  }
}

然后还是照常编译。

client/Client.java:7: error: cannot access Service
    Service service = new Service();
    ^
    bad source file: ./service/Service.java
    file does not contain class service.Service
Please remove or make sure it appears in the correct subdirectory of the sourcepath.
1 error

第四行,bad source file: ./service/Service.java表明他在编译Client.java文件时,通过import找到了该文件,说明import是通过文件结构去找到的。

但是他又说file does not contain class service.Service,说明在这个Service.java中找不到对应的Service类。

为什么呢?因为我的包名是service.something,但是他要找的是service。

所以包的作用是和类的名字组成完整的类名,这里的Service类的全名应该是service.something.Service。但是他去找到的是service.Service。所以它找不到。

那是不是将import改为import service.something.*;就可以呢?

不行,前面说了,import是根据目录结构去找到的,根本就没有service/something这个目录结构,所以不可能找到。

client/Client.java:3: error: package service.something does not exist
import service.something.*;
^
client/Client.java:7: error: cannot find symbol
    Service service = new Service();
    ^
    symbol:   class Service
    location: class Client
    client/Client.java:7: error: cannot find symbol
    Service service = new Service();
                          ^
    symbol:   class Service
    location: class Client

你看,第一行:client/Client.java:3: error: package service.something does not exist

回过头来想,这也是为什么,有时候,当同时导入两个不同包中名字相同的类时,其中有一个要用完全限定名。因为类的名字本来就是由包的名字和类的自身名字组成。

总而言之,这堂课的收获还是挺大的,重点要掌握上面提到的两个概念。

课上本来还有一点关于,打jar包和如何引用jar包的内容,以及通过java -d命令,将class文件编译到指定的目录下。

这里我就不多说了,万变不离其宗。

javac -classpath “.:service.jar” client/Client.java //linux下面是冒号,windows下面是分号

java -classpath “.:service.jar” client.Client

只不过要注意一点的是,打jar包时,一定要按照package定义的目录结构去打。因为在将jar包加入到classpath时和将一个目录下的class文件加入是一样的,相对路径。

java -d out client/Client.java,会将class文件编译到out目录下,且内部目录结构与包的结构一致。

Spring Restful Web Service by Example

| Comments

REST是Roy Fielding在他的2000年博士论文《Architectural Styles and the Design of Network-based Software Architectures》中提出的概念,全称是Representational State Transfer。

Roy Fielding是互联网一个极为重要的人物,他是HTTP协议(1.0版和1.1版)的主要设计者,Apache服务器软件的作者之一,Apache基金会的第一任主席。

他在他的论文中提到一点:“网络研究主要关注系统之间通信行为的细节、如何改进特定通信机制的表现,常常忽视了一个事实,那就是改变应用程序的互动风格比改变互动协议,对整体表现有更大的影响。”。

REST是个名字,加上ful之后,变成了形容词“具有REST风格的”。

在看过一些资料之后,我觉的REST越来越火的主要原因之一,就是想在的互联网应用已经不仅仅是内容的资源,还包括计算资源。而资源的消费者也不仅仅是浏览器(人在消费),还有其他应用(应用程序在消费),所以资源的表现形式在变化。

Web Service

Web Service是一种服务导向架构的技术,通过标准的Web协议提供服务,目的是保证不同平台的应用服务可以互操作。而它的技术通常是根据SOAP协议进行传递XML格式消息。而基于Java的主流WEB服务开发框架往往需要通过WSDL生成客户端的源代码。

PS:这里没有任何想要对比基于SOAP和基于REST的Web Service的区别或者优缺点的意思。

Restful Web Service

Restful Web Service又称为Restful Web API。Rest本身其实不是一种技术,而是一种风格,它基于HTTP协议,具有统一的URL资源,通过HTTP协议提供的方法来表示对数据的操作,资源的表现形式多样化,具体表现形式由资源的消费者决定等等。

说了这么多,还是回到标题,如何实现?

如果你的项目采用Spring开发,那么Spring提供了一套基于Spring MVC的Restful方案,可以非常方便的通过Controller提供想要的Web Service,又可以通过RestTemplate实现客户端对Web Service操作。

PS:本例子是针对Spring 3.2以后,3.1版本有所不同。

首先来实现一个基于Controller的Restful Web Service:

首先是Domain,这个很简单一个Car类,包含名字和价格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package me.zeph.spring.restful.demo.model;

public class Car {
  private String name;
  private int price;

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

  public String getName() {
      return name;
  }

  public void setPrice(int price) {
      this.price = price;
  }

  public int getPrice() {
      return price;
  }
}

然后是Controller,按照普通写RequestMapping一样方式,但这里返回的是Domain对象。 这里有两个位置不同,一个是注解@ResponseBody用来说明返回的结果是HttpResponse的body部分。 另一个参数produces用来说明,这里只接受Http请求中head里说明了Accept类型是Application/Json。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package me.zeph.spring.restful.demo.controller;

import me.zeph.spring.restful.demo.model.Car;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import static org.springframework.web.bind.annotation.RequestMethod.GET;

@Controller
@RequestMapping(value = "car", method = GET)
public class CarController {

  public static final String VIEW_NAME = "car";

  @RequestMapping
  public String getCarByView() {
      return VIEW_NAME;
  }

  @RequestMapping(produces = {APPLICATION_JSON_VALUE})
  @ResponseBody
  public Car getCarByJSON() {
      return getCar();
  }

  private Car getCar() {
      Car car = new Car();
      car.setName("BenZ");
      car.setPrice(300000);
      return car;
  }
}

这样基本就算完成了,可能有人会问,为什么上面的getCarByView方法没有写produces。其实,我也看到许多网上例子,将返回HTML格式View的方法添加了produces。比如,添加Text/Http等。

为什么这里不加,原因是,我们知道,该方法是提供给浏览器请求,用于返回基于HTML的页面。而对于不同的浏览器而言,发出的HTTP请求的head的是不一样,以Chrome和IE为例。

Chrome发出的head是:“text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8”

IE发出的head是:“*/*”

所以如果添加了produces,那么有可能返回结果不是HTML。我就遇到过,添加了Text/Http之后,在IE中返回给我的是JSON数据,而不是HTML页面。

如果不写,那么就是默认的选择,在不满足其他RequestMapping,会去走默认那条Mapping路线。你可以在RestClient中验证,URL是localhost:8080/spring-restful-demo/car,但是head不同,那么它返回给你的view格式就不同了。

其实,走到这一步,已经就完成了。

但是如果你在页面上和RestClient上做一个实验,就会发现它远远不止如此!!!

在浏览器上输入localhost:8080/spring-restful-demo/car.html,会返回给你html页面。

在浏览器上输入localhost:8080/spring-restful-demo/car.json,会返回给你给你json数据。

明明通过浏览器发送请求的Head是Text/Html,为什么会返回给我Json数据。如果通过debug的方式,你会看到确实进入的是getCarByJSON方法。

RestClient去试一把,输入地址,localhost:8080/spring-restful-demo/car.html,设置head为accept=Application/Json,返回给我的是HTMl页面。

默认规约

原来Spring在选择映射的时候,有一定的规约,加入了检查条件,URL后缀,URL参数。并且其默认的检查顺序是后缀,参数,最后才是Accept。

也就是,如果你的URL是以HTML结尾,那么无论你的head是什么,Spring都会认为是返回HTML页面,因为你显示的指明了。

对于那些希望显示指明返回类型的应用,Spring的默认规约已经帮助它们实现。

但是如果,我不希望呢?

无论你的后缀还是参数是什么,我都希望Spring根据我head中的accept值来决定。

Content Negotiation(内容协商)

这里就要引入另外一个东西:Spring MVC Content Negotiation(内容协商)。

它是用来帮助你定义在返回多种表现形式时,应该遵守的规约,默认我们是不需要定义的,那么它就会像上面所提到的去做。但如果你不想要默认的规则,那么你就需要在Spring的Context中定义这个Bean。

这里用我的demo中的例子,简单说明一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/mvc
                    http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
                    http://www.springframework.org/schema/beans
                    http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
                    http://www.springframework.org/schema/context
                    http://www.springframework.org/schema/context/spring-context-3.2.xsd">

    <context:component-scan base-package="me.zeph.spring.restful.demo"/>

    <mvc:annotation-driven content-negotiation-manager="contentNegotiationManager"/>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
        <property name="prefix" value="/WEB-INF/view/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

    <bean id="contentNegotiationManager"
          class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
        <property name="favorPathExtension" value="false" />
        <property name="favorParameter" value="false" />
        <property name="defaultContentType" value="text/html" />
    </bean>

    <bean id="restfulTemplate" class="org.springframework.web.client.RestTemplate"/>

</beans>

在Spring 3.2以后,实现内容协商的bean的名字叫做ContentNegotiationManager,需要通过他的工厂类ContentNegotiationManagerFactoryBean去定义规约。在这里,我修改了三个属性,favorPathExtension,favorParameter和defaultContentType,defaultContentType简单,就是默认的表现内容形式,favorPathExtension表示是否采用路径扩展作为最高优先级的判断,同理favorParameter表示是否采用参数作为第二优先级的判断。这里,我将它们两个都设置为false。即不采用它们作为判断条件。最后,在annotation-driven的标签里,添加属性contentNegotiationManager,说明使用@Controller注解的Controller都采用这种规约。

然后,我们再来是测试,在RestClient上,设置请求Url为localhost:8080/spring-restful-demo/car.html,但是呢,将head设为Accept=Applicaiton/Json。你猜结果怎样?乖乖的返回Json数据。

关于Content Negotiation就说到这里,关于它功能还有很多,以后有机会在补充,那么Web Service就差不多说完了。

RestTemplate实现客户端

在来看看怎么实现Client端的代码。如果不用Spring,可以通过Apache的HttpClient作为客户端,当然写的代码要多一点。

如果使用Spring,可以通过Spring提供的RestTemplate,目的应该和Spring的JDBCTemplate类似,帮你封装了具体操作。

用起来很简单,我写了另一个Controller来在页面显示调用该Web Service的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package me.zeph.spring.restful.demo.controller;

import me.zeph.spring.restful.demo.common.URLs;
import me.zeph.spring.restful.demo.model.Car;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.client.RestTemplate;

@Controller
public class DisplayCarController {

  public static final String VIEW_NAME = "displayCarJson";
  private RestTemplate restTemplate;

  @Autowired
  public DisplayCarController(RestTemplate restTemplate) {
      this.restTemplate = restTemplate;
  }

  @RequestMapping(value = VIEW_NAME)
  public String getCar(Model model) {
      model.addAttribute("car", getCarByJson());
      return VIEW_NAME;
  }

  private Car getCarByJson() {
      return restTemplate.getForObject(URLs.REST_CAR_URL, Car.class);
  }

}

如果你需要特定指定请求的Head。可以使用RestTemplate的exchange方法,不过使用起来要麻烦一些。

1
2
3
4
5
6
7
private Car getCarByJson() {
      HttpHeaders headers = new HttpHeaders();
      headers.add("accept", "application/json");
      HttpEntity httpEntity = new HttpEntity("", headers);
      ResponseEntity<Car> exchange = restTemplate.exchange(URLs.REST_CAR_URL, HttpMethod.GET, httpEntity, Car.class);
      return exchange.getBody();
}

不过第二种方式更能说明是指定了具体head来请求哪种资源。

这里使用Spring来发出请求,都是后端的,如果要用前端请求,固然是通过Ajax,不过那是另外一部分内容。

总而言之,在使用Spring Web Service的这个过程当中还是遇到了不少的坑。

首先,你需要知道有Content Negotiation这个东西,其次,不同的Spring版本,默认的规约不同。Spring 3.2和3.1就是一个分水岭,使用的时候要注意你使用的是哪个版本。

参考资料:

https://spring.io/guides/gs/rest-service/

https://spring.io/blog/2013/05/11/content-negotiation-using-spring-mvc

http://spring.io/blog/2013/06/03/content-negotiation-using-views/