概要
- 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 のため)
<?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 がデバイス判別する仕組み
次の記事は古いバージョンについて書かれているが、それほど変わっていないのではないかと思われる。
デバイス解決には、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 で属性として追加しているためである。
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);
}