Angularの開発サーバー(http://localhost:4200)からApache-Tomcat(Spring Boot, Java)(http://localhost)へのアクセス

2019年12月28日(土)

環境

目次

  1. Cross-Origin Resource Sharing (CORS)
    1. 課題
    2. Cross-Origin Resource Sharingについての参考
    3. パッケージ構成についての参考
  2. コントローラークラスに "@CrossOrigin" アノテーションを付加する
    1. プロジェクト構成
    2. Javaプロジェクトを作成する
    3. WARをTomcatにデプロイする
    4. Angularプロジェクトを作成する
    5. 動作確認

ディレクトリ構成

/
+-home
|   +-mizuki
|       +-.nvm
|       |   +-versions
|       |       +-node
|       |           +-v12.14.0
|       |           +-v13.5.0
|       |
|       +-opt
|       |   +-eclipse-jee -> /home/mizuki/opt/eclipse-jee-201912
|       |   |
|       |   |-eclipse-jee-201912
|       |
|       +-workspace
|       |   +-angular
|       |   |   +-cors
|       |   |
|       |   +-java
|       |   |   +-CORSwithController
|       |   |
|       |   +-node_modules
|       |   |   +-@angular
|       |   |   +-@angular-devkit
|       |   |
|       |   |-package-lock.json
|       |
|       +-www
|           +-html
|               +-static
|                   |-index.html
|
+-media
|   +-sf_sharedfolder
|
+-opt
    |-apache-tomcat -> /opt/apache-tomcat-9.0.29
    |
    +-apache-tomcat-9.0.29
    |   +-bin
    |   |   |-startup.sh
    |   |   |-shutdown.sh
    |   |   |-setenv.sh
    |   |
    |   +-conf
    |       |-tomcat-users.xml
    |
    |-java -> /opt/oracle-jdk-11.0.5
    |
    +-oracle-jdk-11.0.5
        +-bin
            |-java
          

1. Cross-Origin Resource Sharing (CORS)

1-1. 課題

同一ホスト上であってもポートが異なる場合はブラウザによってCross-Origin Resource Sharing (CORS)のチェックが行われるのでHTTP応答ヘッダーで対応する必要がある。

作業はApache-Tomcat(Spring Boot, Java)側だけでよい。

1-2. Cross-Origin Resource Sharingについての参考

Enabling Cross Origin Requests for a RESTful Web Service
https://spring.io/guides/gs/rest-service-cors/
  Enabling CORS
  https://spring.io/guides/gs/rest-service-cors/#_enabling_cors
    Controller method CORS configuration
      So that the RESTful web service will include CORS access control headers in its response,
      you just have to add a @CrossOrigin annotation to the handler method:

      it is also possible to add this annotation at controller class level as well,
      in order to enable CORS on all handler methods of this class.

      "@CrossOrigin" アノテーションをコントローラークラスまたは個々のメソッドに追加する。
      デフォルトでは全て許可となる。

    Global CORS configuration
      Applicationクラスに設定用の定型メソッドを記述して "@Bean" アノテーションを追加する。
          

1-3. パッケージ構成についての参考

コントローラークラスを "@SpringBootApplication" アノテーションを付加したクラスとは平行の別のパッケージに置く場合は追加の記述が必要になる。公式の推奨パッケージ構成は "@SpringBootApplication" アノテーションを付加したクラスを最上位パッケージに置くこと。

2. コントローラークラスに "@CrossOrigin" アノテーションを付加する

2-1. プロジェクト構成

Javaプロジェクト

CORSwithController
  +-src
  |   +-main
  |       +-java
  |           +-com.example.demo
  |               |-CorSwithControllerApplication.java
  |               |-Greeting.java
  |               |-GreetingController.java
  |               |-ServletInitializer.java
  |
  +-target
      |-CORSwithController-0.0.1-SNAPSHOT.war
          

Angularプロジェクト

cors
  +-src
      +-app
          +-class
          |   |-greeting.ts
          |
          +-component
          |   +-greeting
          |       |-greeting.component.css
          |       |-greeting.component.html
          |       |-greeting.component.ts
          |
          +-service
          |   |-greeting.service.ts
          |
          |-app.component.html
          |-app.module.ts
          

2-2. Javaプロジェクトを作成する

EclipseでSpring Bootのプロジェクトを作成する。

Fileメニュー > New > Other...
  Spring Boot
    Spring Starter Project
          
New Spring Starter Project

  Service URL: https://start.spring.io
  Name: CORSwithController
  [チェックする] Use default location
  Location: /home/mizuki/workspace/java/CORSwithController
  Type: Maven              Packaging: War (変更)
  Java Version: 11 (変更)  Language: Java
  Group: com.example
  Artifact: CORSwithController
  Version: 0.0.1-SNAPSHOT
  Description: Demo project for Spring Boot
  Package: com.example.demo
  Working sets
    [チェックしない] Add project to working sets
          
New Spring Starter Project Dependencies

  Spring Boot Version: 2.2.2
  Avaliable:
    Web
      Spring Web
          

プロジェクトに必要なファイルが全てダウンロードされるまで時間が掛かる。Project Explorerにプロジェクトが表示されてもステータスバーのプログレスバーが消えるまで待つ。

コードを追加する。

Greeting.java

package com.example.demo;

public class Greeting {
  private final long id;
  private final String content;

  public Greeting() {
    this.id = -1;
    this.content = "";
  }

  public Greeting(long id, String content) {
    this.id = id;
    this.content = content;
  }

  public long getId() {
    return this.id;
  }

  public String getContent() {
    return this.content;
  }
}
        

GreetingController.java

package com.example.demo;

import java.util.concurrent.atomic.AtomicLong;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@CrossOrigin
@RestController
public class GreetingController {
  private static final String template = "Hello, %s!";
  private final AtomicLong counter = new AtomicLong();

  @GetMapping("/greeting")
  public Greeting greeting(@RequestParam(required=false, defaultValue="World") String name) {
    System.out.println("==== in greeting ====");
    return new Greeting(counter.incrementAndGet(), String.format(template, name));
  }
}
        

参考

2-3. WARをTomcatにデプロイする

前回のWARを削除する。

pom.xmlを右クリック
  Run As
    Maven clean
          

WARを作成する。targetフォルダ配下に出力される。

pom.xmlを右クリック
  Run As
    Maven install
          

Tomcatを起動する。

$ sudo systemctl stop httpd
$ sudo systemctl stop tomcat
$ sudo systemctl start tomcat
$ sudo systemctl start httpd
        

ブラウザで http://localhost を開く。Manager App を tomcat/tomcat で開く。

配備 > WARファイルの配備でtargetフォルダ配下のwarファイルを選択して配備ボタンを押す。

2-4. Angularプロジェクトを作成する

$ pwd
/home/mizuki/workspace/angular

$ npx ng new cors
        

Visual Studio Codeを起動してFile > Open Folder...からcorsディレクトリを開く。

Java側と受け渡しするクラスを追加する。

$ pwd
/home/mizuki/workspace/angular/cors

$ npx ng generate class class/greeting
        

src/app/class/greeting.ts

export class Greeting {
  id: number;
  content: string;
}
        

Java側と通信するサービスを追加する。

$ pwd
/home/mizuki/workspace/angular/cors

$ npx ng generate service service/greeting
        

src/app/service/greeting.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
          
import { Greeting } from 'src/app/class/greeting';

@Injectable({
  providedIn: 'root'
})
export class GreetingService {

  private url: string = 'http://localhost/CORSwithController-0.0.1-SNAPSHOT/greeting'

  constructor(private http: HttpClient) { }

  getGreeting(): Observable<Greeting> {
    return this.http.get<Greeting>(this.url);
  }
}
        

コンポーネントを追加する。

$ pwd
/home/mizuki/workspace/angular/cors

$ npx ng generate component component/greeting
        

src/app/component/greeting/greeting.component.ts

import { Component, OnInit } from '@angular/core';

import { Greeting } from 'src/app/class/greeting';
import { GreetingService } from 'src/app/service/greeting.service';

@Component({
  selector: 'app-greeting',
  templateUrl: './greeting.component.html',
  styleUrls: ['./greeting.component.css']
})
export class GreetingComponent implements OnInit {
  title: string = 'Greeting';
  greeting: Greeting;

  constructor(private greetingService: GreetingService) { }

  ngOnInit() {
    console.log('GreetingComponent.ngOnInit');
    this.getGreeting();
  }

  getGreeting() {
    this.greetingService.getGreeting().subscribe(
      (value: Greeting) => {
        console.log('GreetingComponent.getGreeting.next');
        this.greeting = value;
      },
      (error: any) => {
        console.log('GreetingComponent.getGreeting.error');
        for (let property in error) {
          this.errorList.push(error[property]);
        }
      },
      () => {
        console.log('GreetingComponent.getGreeting.complete');
        console.log('this.greeting.content[' + this.greeting.content + ']');
      }
    );
  }
}
        

Observable.subscribe()にnext(),error(),complete()の関数を渡す場合は次のようにオブジェクトリテラルの形では渡さない。こうすると関数内でのコンポーネント変数の変更がhtml側へ反映されない。

this.greetingService.getGreeting().subscribe({
  next(value: Greeting) {
    console.log('GreetingComponent.getGreeting.next');
    this.greeting = value; /* これが反映されない。 */
  },
  error(err: any) {
    console.log('GreetingComponent.getGreeting.error');
    for (let property in err) {
      this.errorList.push(err[property]);
    }
  },
  complete() {
    console.log('GreetingComponent.getGreeting.complete');
    console.log('this.greeting.content[' + this.greeting.content + ']');
  }
});
        

src/app/component/greeting/greeting.component.html

<p>{{title}}</p>
<p *ngIf="greeting">{{greeting.content}}</p>
<ul *ngIf="!greeting">
  <li>失敗</li>
  <li *ngFor="let message of errorList; let i=index">{{message}}</li>
</ul>
        

src/app/app.component.html

<app-greeting></app-greeting>
        

src/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { GreetingComponent } from './component/greeting/greeting.component';

@NgModule({
  declarations: [
    AppComponent,
    GreetingComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
        

参考

2-5. 動作確認

CORSのエラーが出る場合はブラウザのF12でデベロッパーツールを表示するとConsoleに次のエラーが出ている。

Access to XMLHttpRequest at 'http://localhost/CORSwithController-0.0.1-SNAPSHOT/greeting'
from origin 'http://localhost:4200' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
          

CORSのエラーが出ない場合はブラウザのF12でデベロッパーツールを表示するとNetwork > greeting > Headers > Response Headersに次の項目が含まれている。

Access-Control-Allow-Origin: *