Implementing IP‑Based City Detection and Visit Counting in a Spring Backend
This article explains how to obtain a user's city from their IP address in a Java Spring backend, using the QQWry IP database, custom utility classes, and a login interceptor to count visits per city, with complete code examples and deployment considerations.
Significance
In e‑commerce, news, and social sites, understanding the geographic distribution of users is essential for cost‑effective operations, especially when different cities require different handling strategies.
Methods to Obtain City from IP
Public APIs from providers like Taobao or Sina, which may change URLs and have rate limits.
Open‑source pure IP database (e.g., QQWry) that can be downloaded and updated independently.
Implementation Idea
First retrieve the client’s IP address from the HttpServletRequest, then use the QQWry library to map the IP to a city, and finally count visits per city.
IPUtil – Getting Client IP
public class IPUtil {
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}IPZone – Parsing QQWry Data
public IPZone findIP(final String ip) {
final long ipNum = toNumericIP(ip);
final QIndex idx = searchIndex(ipNum);
if (idx == null) {
return new IPZone(ip);
}
return readIP(ip, idx);
}Login Interceptor
The interceptor extracts the IP, resolves the city via QQWry, and updates an in‑memory map of visit counts (in production a Redis store would be used).
@Slf4j
public class MyLoginInterceptor implements HandlerInterceptor {
private static final String LOGIN_PATH = "/user/login";
private static Map
visitCount;
private static final QQWry qqWry;
static {
visitCount = new HashMap<>(31);
qqWry = new QQWry();
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("【MyLoginInterceptor】called: {}", request.getRequestURI());
if (request.getRequestURI().equals(LOGIN_PATH)) {
String ipAddress = IPUtil.getIpAddress(request);
String province = qqWry.findIP(ipAddress).getMainInfo();
if (visitCount.containsKey(province)) {
visitCount.put(province, new AtomicInteger(visitCount.get(province).incrementAndGet()));
} else {
visitCount.put(province, new AtomicInteger());
}
}
return true;
}
// postHandle and afterCompletion omitted for brevity
}WebMvc Configuration
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyLoginInterceptor());
}
}Login Controller Example
@RestController("user")
public class LoginController {
@GetMapping("login")
public String login() {
// login logic
return "success";
}
}In production, the visit counts should be stored in a distributed cache such as Redis, and a separate REST endpoint can expose the per‑city statistics for front‑end visualization.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.