Help us understand the problem. What is going on with this article?

Spring Mobile でスマートフォン・タブレット・パソコンのデバイス種別判定をする

More than 1 year has passed since last update.

概要

  • Spring Boot + Spring Mobile を使って、アクセスしてきた端末がスマートフォン・タブレット・パソコンのどれかデバイス判別する

ソースコード

ソースコード一覧

DeviceInfoController.java と pom.xml の2つのみ。

├── pom.xml
└── src
    └── main
        └── java
            └── com
                └── example
                    └── deviceinfo
                        └── DeviceInfoController.java

Maven のビルド用ファイル pom.xml

  • dependencies に Spring Mobile を導入
  • repositories に Spring Mobile のマイルストーンバージョンが置いてあるリポジトリを追加 (今回使う Spring Mobile のバージョンが 2.0.0.M3 のため)
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.7.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>

  <groupId>com.example</groupId>
  <artifactId>deviceinfo</artifactId>
  <version>0.0.1</version>
  <name>deviceinfo</name>
  <description>Deviceinfo project for Spring Boot</description>

  <properties>
    <java.version>11</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Mobile を導入 -->
    <dependency>
      <groupId>org.springframework.mobile</groupId>
      <artifactId>spring-mobile-device</artifactId>
      <version>2.0.0.M3</version>
    </dependency>
  </dependencies>

  <repositories>
    <!-- Spring Mobile のマイルストーンバージョンが置いてあるリポジトリを追加 -->
    <repository>
      <id>spring-milestone</id>
      <name>Spring Milestone Repository</name>
      <url>http://repo.spring.io/milestone</url>
    </repository>
  </repositories>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>

DeviceInfoController.java

package com.example.deviceinfo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.mobile.device.Device;
import org.springframework.mobile.device.DevicePlatform;
import org.springframework.mobile.device.DeviceResolverHandlerInterceptor;
import org.springframework.mobile.device.DeviceUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.HashMap;
import java.util.Map;

@SpringBootApplication
@Configuration
@RestController
public class DeviceInfoController implements WebMvcConfigurer {

  public static void main(String[] args) {
    SpringApplication.run(DeviceInfoController.class, args);
  }

  // Spring Mobile を使うために必要
  @Bean
  public DeviceResolverHandlerInterceptor deviceResolverHandlerInterceptor() {
    return new DeviceResolverHandlerInterceptor();
  }

  // Spring Mobile を使うために必要
  // WebMvcConfigurer#addInterceptors
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(deviceResolverHandlerInterceptor());
  }

  @RequestMapping("/")
  public Map index(WebRequest req) {

    Map res = new HashMap<String, String>();

    String userAgent = req.getHeader(HttpHeaders.USER_AGENT);
    res.put("useragent", userAgent);

    Device device = DeviceUtils.getCurrentDevice(req);
    if (device == null) {
      res.put("device", "null");
    } else if (device.isMobile()) {
      res.put("device", "mobile");
    } else if (device.isTablet()) {
      res.put("device", "tablet");
    } else if (device.isNormal()) {
      res.put("device", "normal");
    }

    if (device != null) {
      DevicePlatform dp = device.getDevicePlatform();
      switch (dp) {
        case ANDROID:
          res.put("platform", "android");
          break;
        case IOS:
          res.put("platform", "ios");
          break;
        case UNKNOWN:
          res.put("platform", "unknown");
          break;
      }
    }

    return res;
  }
}

ビルドと起動

mvn package で JAR ファイルを生成。

$ mvn package

java コマンドで Spring Boot を起動。

$ java -jar target/deviceinfo-0.0.1.jar

いろいろなユーザーエージェントでアクセスしてみる

Ruby + curl でいろいろなユーザーエージェントを指定してアクセスしてみる。

require 'json'

ualist = []

## Smartphone

# iPhone + iOS + Safari
ualist << 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1'

# iPhone + iOS + Chrome
ualist << 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/69.0.3497.91 Mobile/15E148 Safari/605.1'

# Galaxy A30 SCV43 + Android + Browser
ualist << 'Mozilla/5.0 (Linux; Android 9; SCV43 Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/9.0 Chrome/67.0.3396.87 Mobile Safari/537.36'

# Xperia XZ3 SOV39 + Chrome
ualist << 'Mozilla/5.0 (Linux; Android 9; SOV39 Build/52.0.C.1.119) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.91 Mobile Safari/537.36'

## Tablet

# iPad mini + iOS + Safari
ualist << 'Mozilla/5.0 (iPad; CPU OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1'

# Qua tab QZ10 + Android + Chrome
ualist << 'Mozilla/5.0 (Linux; Android 8.1.0; KYT33 Build/3.020VE.0072.a) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.126 Safari/537.36'

## Desktop

# macOS + Safari
ualist << 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.2 Safari/605.1.15'

# Windows 10 + Microsoft Edge
ualist << 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362'

ualist.each do |ua|
  json = `#{"curl --silent --user-agent '#{ua}' http://localhost:8080/"}`
  puts JSON.pretty_generate(JSON.parse(json))
  puts
end

実行結果。
ユーザーエージェントを解析してしっかりと判別できているのがわかる。

{
  "useragent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1",
  "device": "mobile",
  "platform": "ios"
}

{
  "useragent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/69.0.3497.91 Mobile/15E148 Safari/605.1",
  "device": "mobile",
  "platform": "ios"
}

{
  "useragent": "Mozilla/5.0 (Linux; Android 9; SCV43 Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/9.0 Chrome/67.0.3396.87 Mobile Safari/537.36",
  "device": "mobile",
  "platform": "android"
}

{
  "useragent": "Mozilla/5.0 (Linux; Android 9; SOV39 Build/52.0.C.1.119) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.91 Mobile Safari/537.36",
  "device": "mobile",
  "platform": "android"
}

{
  "useragent": "Mozilla/5.0 (iPad; CPU OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1",
  "device": "tablet",
  "platform": "ios"
}

{
  "useragent": "Mozilla/5.0 (Linux; Android 8.1.0; KYT33 Build/3.020VE.0072.a) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.126 Safari/537.36",
  "device": "tablet",
  "platform": "android"
}

{
  "useragent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.2 Safari/605.1.15",
  "device": "normal",
  "platform": "unknown"
}

{
  "useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362",
  "device": "normal",
  "platform": "unknown"
}

ハンドラ・メソッドの引数に Device を指定できるようにする

ハンドラ・メソッド (@RequestMapping アノテーションが付加されたメソッド) の引数に Device を指定できるように DeviceInfoController.java を修正する。

package com.example.deviceinfo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.mobile.device.Device;
import org.springframework.mobile.device.DeviceHandlerMethodArgumentResolver;
import org.springframework.mobile.device.DevicePlatform;
import org.springframework.mobile.device.DeviceResolverHandlerInterceptor;
import org.springframework.mobile.device.DeviceUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@SpringBootApplication
@Configuration
@RestController
public class DeviceInfoController implements WebMvcConfigurer {

  public static void main(String[] args) {
    SpringApplication.run(DeviceInfoController.class, args);
  }

  // Spring Mobile を使うために必要
  @Bean
  public DeviceResolverHandlerInterceptor deviceResolverHandlerInterceptor() {
    return new DeviceResolverHandlerInterceptor();
  }

  // Spring Mobile を使うために必要
  // WebMvcConfigurer#addInterceptors
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(deviceResolverHandlerInterceptor());
  }

  // @RequestMapping が付加されたメソッド (ハンドラ・メソッド) の引数に
  // Spring Mobile の Device を指定できるようにするために必要
  @Bean
  public DeviceHandlerMethodArgumentResolver deviceHandlerMethodArgumentResolver() {
    return new DeviceHandlerMethodArgumentResolver();
  }

  // @RequestMapping が付加されたメソッド (ハンドラ・メソッド) の引数に
  // Spring Mobile の Device を指定できるようにするために必要
  // WebMvcConfigurer#addArgumentResolvers
  @Override
  public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
    argumentResolvers.add(deviceHandlerMethodArgumentResolver());
  }

  @RequestMapping("/")
  public Map index(WebRequest req, Device device) {

    Map res = new HashMap<String, String>();

    String userAgent = req.getHeader(HttpHeaders.USER_AGENT);
    res.put("useragent", userAgent);

    // ハンドラ・メソッドの引数に Device を指定できるようになったのでこの処理は不要
    //Device device = DeviceUtils.getCurrentDevice(req);

    if (device == null) {
      res.put("device", "null");
    } else if (device.isMobile()) {
      res.put("device", "mobile");
    } else if (device.isTablet()) {
      res.put("device", "tablet");
    } else if (device.isNormal()) {
      res.put("device", "normal");
    }

    if (device != null) {
      DevicePlatform dp = device.getDevicePlatform();
      switch (dp) {
        case ANDROID:
          res.put("platform", "android");
          break;
        case IOS:
          res.put("platform", "ios");
          break;
        case UNKNOWN:
          res.put("platform", "unknown");
          break;
      }
    }

    return res;
  }
}

Spring Mobile がデバイス判別する仕組み

次の記事は古いバージョンについて書かれているが、それほど変わっていないのではないかと思われる。

Spring Mobile 1.0がリリース

デバイス解決には、LiteDeviceResolverが既定で使われる。これは WordPress Mobile Packの検知アルゴリズム を基にしている。DeviceResolverHandlerInterceptorのコンストラクタ引数を注入することにより、別のDeviceResolver実装 をプラグインできる。WURFLのようなより洗練されたデバイスのリゾルバは、画面サイズ、製造元、モデル、または優先マークアップなどの特定のデバイスの機能を識別することができる。

LiteDeviceResolver.java のソースコードを見るとユーザーエージェント文字列からデバイスを判別している様子がわかる。

spring-mobile/LiteDeviceResolver.java at v2.0.0.M3 · spring-projects/spring-mobile · GitHub

// Android special case
if (userAgent.contains("android")) {
  return resolveWithPlatform(DeviceType.MOBILE, DevicePlatform.ANDROID);
}
// Apple special case
if (userAgent.contains("iphone") || userAgent.contains("ipod") || userAgent.contains("ipad")) {
  return resolveWithPlatform(DeviceType.MOBILE, DevicePlatform.IOS);
}

Spring Mobile を使うためには WebMvcConfigurer#addInterceptors を実装して、そのメソッド内で DeviceResolverHandlerInterceptor オブジェクトを InterceptorRegistry オブジェクトに追加する必要がある。

これは、ハンドラメソッドでの処理の前に DeviceResolverHandlerInterceptor オブジェクトが Device オブジェクトを生成して、HttpServletRequest#setAttribute で属性として追加しているためである。

spring-mobile/DeviceResolverHandlerInterceptor.java at v2.0.0.M3 · spring-projects/spring-mobile · GitHub

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  Device device = deviceResolver.resolveDevice(request);
  request.setAttribute(DeviceUtils.CURRENT_DEVICE_ATTRIBUTE, device);
  return true;
}

spring-mobile/DeviceUtils.java at v2.0.0.M3 · spring-projects/spring-mobile · GitHub

DeviceUtils.getCurrentDevice ではセットされた属性を返しているだけとなる。

public static Device getCurrentDevice(HttpServletRequest request) {
  return (Device) request.getAttribute(CURRENT_DEVICE_ATTRIBUTE);
}

参考資料

niwasawa
迷子になりがちな地図・位置情報系プログラマ。
http://niwasawa.hatenablog.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした