从零搭建一个现代化的验证码登录系统:Spring Boot + 阿里云短信实战教程

从零搭建一个现代化的验证码登录系统:Spring Boot + 阿里云短信实战教程

前言

在当今这个快节奏的数字时代,用户体验已经成为产品成功的关键因素之一。传统的用户名密码登录方式,虽然安全可靠,但对于用户来说,注册流程繁琐、记忆密码困难,往往是用户流失的重灾区。

手机号验证码登录,凭借其"即用即走"的便利性,已经成为主流应用的首选方案。不用注册、不用记密码、输入手机号收个验证码就能登录,体验简直不要太丝滑!

今天,我就带你从零搭建一个完整的验证码登录系统,使用 Spring Boot 3 + 阿里云短信服务,从后端 API 到前端页面,全流程实战!


项目概览

我们要实现的功能包括:

发送短信验证码 - 通过阿里云短信服务发送 6 位验证码

验证码验证 - 验证用户输入的验证码是否正确

用户注册 - 新用户通过验证码完成注册

用户登录 - 已注册用户通过验证码登录

精美界面 - 提供登录、注册、首页三个页面

最终效果预览:

复制代码
用户流程:
首页 → 登录/注册 → 输入手机号 → 收验证码 → 登录成功 → 返回首页

技术栈选择

工欲善其事,必先利其器。我们选择以下技术栈:

技术 版本 用途
Spring Boot 3.2.5 应用框架
Spring Data JPA 3.2.5 数据持久化
H2 Database - 嵌入式数据库(演示用)
Thymeleaf 3.2.5 模板引擎
阿里云 dypnsapi 1.2.3 短信服务 SDK
Java 17 编程语言

为什么选择这些技术?

  1. Spring Boot 3:最新的 Spring 生态,支持 Java 17+,开发效率极高
  2. 阿里云短信:国内最成熟的短信服务之一,稳定性有保障
  3. H2:零配置嵌入式数据库,演示项目的不二之选
  4. Thymeleaf:自然模板,前后端不分离的项目首选

第一步:项目初始化

1.1 创建 Maven 项目

首先,创建一个标准的 Spring Boot Maven 项目。pom.xml 依赖如下:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>aliyun-sms-demo</artifactId>
    <version>1.0.0</version>
    
    <properties>
        <java.version>17</java.version>
    </properties>
    
    <dependencies>
        <!-- Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- 参数校验 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        
        <!-- JPA -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        
        <!-- H2 数据库 -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- 模板引擎 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        
        <!-- 阿里云短信 SDK -->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>dypnsapi20170525</artifactId>
            <version>1.2.3</version>
        </dependency>
        
        <!-- Lombok 减少样板代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

1.2 配置文件

创建 application.yml 配置文件:

yaml 复制代码
server:
  port: 8080

spring:
  application:
    name: aliyun-sms-demo
  datasource:
    url: jdbc:h2:file:./data/smsdb
    driver-class-name: org.h2.Driver
    username: sa
    password: 
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
  thymeleaf:
    cache: false

# 阿里云短信配置(替换为你自己的配置)
aliyun:
  sms:
    access-key-id: 你的AccessKey ID
    access-key-secret: 你的AccessKey Secret
    endpoint: dypnsapi.aliyuncs.com
    sign-name: 你的短信签名
    template-code: 你的模板编码
    code-length: 6
    code-expire-seconds: 300
    send-interval-seconds: 60

注意:这里的阿里云配置需要你去阿里云控制台申请开通短信服务,获取 AccessKey 和模板信息。


第二步:核心代码实现

2.1 配置属性类

使用 Spring Boot 的 @ConfigurationProperties 将配置文件中的配置绑定到 Java 对象:

java 复制代码
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.sms")
public class SmsProperties {
    
    private String accessKeyId;
    private String accessKeySecret;
    private String endpoint;
    private String signName;
    private String templateCode;
    
    private int codeLength = 6;
    private long codeExpireSeconds = 300;
    private long sendIntervalSeconds = 60;
}

这样,我们就可以在代码中直接注入 SmsProperties 来获取配置,而不是到处写硬编码的配置值。

2.2 用户实体类

定义用户实体,对应数据库中的 users 表:

java 复制代码
@Data
@Entity
@Table(name = "users")
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true, nullable = false)
    private String phone;
    
    @Column(nullable = false)
    private String nickname;
    
    private LocalDateTime createTime;
    private LocalDateTime lastLoginTime;
    
    @PrePersist
    protected void onCreate() {
        createTime = LocalDateTime.now();
    }
}

关键点:

  • @Column(unique = true):确保手机号唯一
  • @PrePersist:在实体持久化前自动设置创建时间
  • LocalDateTime:JDK 8+ 引入的现代日期时间 API

2.3 数据访问层

使用 Spring Data JPA 的 Repository 模式:

java 复制代码
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    Optional<User> findByPhone(String phone);
    
    boolean existsByPhone(String phone);
}

是的,你没看错!只需要定义接口,不需要写实现。Spring Data JPA 会根据方法名自动生成 SQL。

  • findByPhoneSELECT * FROM users WHERE phone = ?
  • existsByPhoneSELECT COUNT(*) FROM users WHERE phone = ?

2.4 阿里云短信服务封装

这是核心中的核心!我们来封装阿里云短信服务的调用:

java 复制代码
@Service
@RequiredArgsConstructor
@Slf4j
public class AliyunSmsService {

    private final SmsProperties smsProperties;
    
    @Data
    @AllArgsConstructor
    public static class SmsResult {
        private boolean success;
        private String message;
        private String bizId;
    }
    
    private Client createClient() throws Exception {
        Config config = new Config()
            .setAccessKeyId(smsProperties.getAccessKeyId())
            .setAccessKeySecret(smsProperties.getAccessKeySecret())
            .setEndpoint(smsProperties.getEndpoint());
        return new Client(config);
    }

    /**
     * 发送短信验证码
     */
    public SmsResult sendSms(String phoneNumber, int expireMinutes) {
        try {
            Client client = createClient();
            
            Map<String, String> templateParam = new HashMap<>();
            templateParam.put("code", "##code##");
            templateParam.put("min", String.valueOf(expireMinutes));
            
            SendSmsVerifyCodeRequest request = new SendSmsVerifyCodeRequest();
            request.setPhoneNumber(phoneNumber);
            request.setSignName(smsProperties.getSignName());
            request.setTemplateCode(smsProperties.getTemplateCode());
            request.setTemplateParam(new Gson().toJson(templateParam));
            
            SendSmsVerifyCodeResponse response = client.sendSmsVerifyCode(request);
            
            if ("OK".equals(response.getBody().getCode())) {
                log.info("短信发送成功,手机号: {}", phoneNumber);
                return new SmsResult(true, "发送成功", 
                    response.getBody().getModel() != null 
                        ? response.getBody().getModel().getBizId() 
                        : null);
            } else {
                return new SmsResult(false, 
                    String.format("错误码: %s, 错误信息: %s", 
                        response.getBody().getCode(), 
                        response.getBody().getMessage()), 
                    null);
            }
        } catch (Exception e) {
            log.error("短信发送异常", e);
            return new SmsResult(false, "发送异常: " + e.getMessage(), null);
        }
    }
    
    /**
     * 验证短信验证码
     */
    public SmsResult verifySms(String phoneNumber, String code) {
        try {
            Client client = createClient();
            
            CheckSmsVerifyCodeRequest request = new CheckSmsVerifyCodeRequest();
            request.setPhoneNumber(phoneNumber);
            request.setVerifyCode(code);
            request.setSchemeName("默认方案");
            
            CheckSmsVerifyCodeResponse response = client.checkSmsVerifyCode(request);
            
            if ("OK".equals(response.getBody().getCode())) {
                String verifyResult = response.getBody().getModel() != null 
                    ? response.getBody().getModel().getVerifyResult() 
                    : null;
                if ("PASS".equals(verifyResult)) {
                    log.info("短信验证成功,手机号: {}", phoneNumber);
                    return new SmsResult(true, "验证成功", null);
                }
            }
            return new SmsResult(false, "验证失败", null);
        } catch (Exception e) {
            log.error("短信验证异常", e);
            return new SmsResult(false, "验证异常: " + e.getMessage(), null);
        }
    }
}

设计要点

  1. 统一返回格式SmsResult 封装了操作结果、消息和业务ID
  2. 异常处理:所有异常都被捕获,转换为友好的错误信息
  3. 日志记录:关键操作都有日志输出,便于排查问题
  4. 方法解耦:发送和验证分开,职责单一

2.5 用户业务服务

处理用户注册、登录的业务逻辑:

java 复制代码
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
    
    private final UserRepository userRepository;
    
    /**
     * 用户注册
     */
    public User register(String phone, String nickname) {
        if (userRepository.existsByPhone(phone)) {
            throw new RuntimeException("该手机号已注册");
        }
        
        User user = new User();
        user.setPhone(phone);
        // 如果没有传昵称,默认使用"用户+手机号后4位"
        user.setNickname(nickname != null ? nickname : "用户" + phone.substring(7));
        
        User saved = userRepository.save(user);
        log.info("用户注册成功,手机号: {}, 昵称: {}", phone, user.getNickname());
        return saved;
    }
    
    /**
     * 用户登录
     */
    public Optional<User> login(String phone) {
        Optional<User> userOpt = userRepository.findByPhone(phone);
        
        if (userOpt.isPresent()) {
            User user = userOpt.get();
            user.setLastLoginTime(LocalDateTime.now());
            userRepository.save(user);
            log.info("用户登录成功,手机号: {}", phone);
            return Optional.of(user);
        }
        
        log.warn("用户不存在,手机号: {}", phone);
        return Optional.empty();
    }
    
    public boolean existsByPhone(String phone) {
        return userRepository.existsByPhone(phone);
    }
}

亮点设计

  • Optional<User>:优雅地处理可能为空的返回值,避免 NullPointerException
  • 登录时更新 lastLoginTime:自动记录用户最后登录时间
  • 默认昵称策略:提升用户体验,不强制用户填写昵称

2.6 API 控制器层

现在,我们需要暴露 RESTful API 给前端调用。

短信服务接口
java 复制代码
@RestController
@RequestMapping("/api/sms")
@Validated
@RequiredArgsConstructor
@Slf4j
public class SmsController {

    private final AliyunSmsService aliyunSmsService;
    private final SmsProperties smsProperties;
    
    private final Map<String, Long> lastSendTime = new ConcurrentHashMap<>();

    /**
     * 发送短信验证码
     */
    @PostMapping("/send")
    public Map<String, Object> sendCode(
            @RequestParam @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") 
            String phone) {
        
        Map<String, Object> result = new HashMap<>();
        
        // 频率限制:防止短信轰炸
        Long lastSend = lastSendTime.get(phone);
        long now = System.currentTimeMillis();
        if (lastSend != null && (now - lastSend) < smsProperties.getSendIntervalSeconds() * 1000) {
            result.put("success", false);
            result.put("message", "发送太频繁,请稍后再试");
            return result;
        }
        
        int expireMinutes = (int) (smsProperties.getCodeExpireSeconds() / 60);
        AliyunSmsService.SmsResult smsResult = aliyunSmsService.sendSms(phone, expireMinutes);
        
        if (smsResult.isSuccess()) {
            lastSendTime.put(phone, now);
            result.put("success", true);
            result.put("message", smsResult.getMessage());
            result.put("bizId", smsResult.getBizId());
            log.info("验证码已发送,手机号: {}", phone);
        } else {
            result.put("success", false);
            result.put("message", smsResult.getMessage());
        }
        
        return result;
    }

    /**
     * 验证短信验证码
     */
    @PostMapping("/verify")
    public Map<String, Object> verifyCode(
            @RequestParam @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") 
            String phone,
            @RequestParam String code) {
        
        Map<String, Object> result = new HashMap<>();
        AliyunSmsService.SmsResult smsResult = aliyunSmsService.verifySms(phone, code);
        
        if (smsResult.isSuccess()) {
            lastSendTime.remove(phone);
            result.put("success", true);
            result.put("message", smsResult.getMessage());
            log.info("验证码验证成功,手机号: {}", phone);
        } else {
            result.put("success", false);
            result.put("message", smsResult.getMessage());
        }
        
        return result;
    }
}
认证服务接口
java 复制代码
@RestController
@RequestMapping("/api/auth")
@Validated
@RequiredArgsConstructor
@Slf4j
public class AuthController {

    private final UserService userService;
    private final AliyunSmsService aliyunSmsService;

    /**
     * 检查手机号是否已注册
     */
    @PostMapping("/check-phone")
    public Map<String, Object> checkPhone(
            @RequestParam @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") 
            String phone) {
        
        Map<String, Object> result = new HashMap<>();
        boolean exists = userService.existsByPhone(phone);
        
        result.put("success", true);
        result.put("exists", exists);
        result.put("message", exists ? "该手机号已注册" : "该手机号未注册");
        
        return result;
    }

    /**
     * 用户注册
     */
    @PostMapping("/register")
    public Map<String, Object> register(
            @RequestParam @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") 
            String phone,
            @RequestParam String code,
            @RequestParam(required = false) String nickname) {
        
        Map<String, Object> result = new HashMap<>();
        
        // 1. 验证短信验证码
        AliyunSmsService.SmsResult verifyResult = aliyunSmsService.verifySms(phone, code);
        if (!verifyResult.isSuccess()) {
            result.put("success", false);
            result.put("message", verifyResult.getMessage());
            return result;
        }
        
        // 2. 检查手机号是否已注册
        if (userService.existsByPhone(phone)) {
            result.put("success", false);
            result.put("message", "该手机号已注册");
            return result;
        }
        
        // 3. 执行注册
        try {
            User user = userService.register(phone, nickname);
            result.put("success", true);
            result.put("message", "注册成功");
            result.put("user", Map.of(
                "id", user.getId(),
                "phone", user.getPhone(),
                "nickname", user.getNickname()
            ));
        } catch (Exception e) {
            result.put("success", false);
            result.put("message", "注册失败: " + e.getMessage());
        }
        
        return result;
    }

    /**
     * 用户登录
     */
    @PostMapping("/login")
    public Map<String, Object> login(
            @RequestParam @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") 
            String phone,
            @RequestParam String code) {
        
        Map<String, Object> result = new HashMap<>();
        
        // 1. 验证短信验证码
        AliyunSmsService.SmsResult verifyResult = aliyunSmsService.verifySms(phone, code);
        if (!verifyResult.isSuccess()) {
            result.put("success", false);
            result.put("message", verifyResult.getMessage());
            return result;
        }
        
        // 2. 检查用户是否存在
        if (!userService.existsByPhone(phone)) {
            result.put("success", false);
            result.put("message", "该手机号未注册");
            result.put("needRegister", true);
            return result;
        }
        
        // 3. 执行登录
        User user = userService.login(phone).orElse(null);
        if (user != null) {
            result.put("success", true);
            result.put("message", "登录成功");
            result.put("user", Map.of(
                "id", user.getId(),
                "phone", user.getPhone(),
                "nickname", user.getNickname()
            ));
        } else {
            result.put("success", false);
            result.put("message", "登录失败");
        }
        
        return result;
    }
}

关键设计

  1. 参数校验 :使用 @Pattern 注解在 Controller 层校验手机号格式
  2. 频率限制 :使用 ConcurrentHashMap 记录发送时间,防止短信轰炸
  3. 统一格式 :所有 API 返回统一的 Map 结构,前端处理更方便
  4. 业务流程:注册/登录都遵循"验证验证码 → 检查状态 → 执行业务"的流程

2.7 页面控制器

提供页面路由,使用 Thymeleaf 渲染页面:

java 复制代码
@Controller
public class PageController {
    
    @GetMapping("/")
    public String index() {
        return "index";
    }
    
    @GetMapping("/login")
    public String login() {
        return "login";
    }
    
    @GetMapping("/register")
    public String register() {
        return "register";
    }
}

第三步:前端页面实现

后端 API 准备好了,现在来写前端页面。我们会用原生 HTML + CSS + JS,不依赖任何前端框架,保持轻量。

3.1 登录页面

登录页面的核心逻辑:

javascript 复制代码
// 发送验证码
sendCodeBtn.addEventListener('click', async () => {
    const phone = phoneInput.value.trim();
    
    if (!/^1[3-9]\d{9}$/.test(phone)) {
        showMessage('请输入正确的手机号', 'error');
        return;
    }
    
    sendCodeBtn.disabled = true;
    sendCodeBtn.textContent = '发送中...';
    
    try {
        const response = await fetch('/api/sms/send?phone=' + phone, {
            method: 'POST'
        });
        const result = await response.json();
        
        if (result.success) {
            showMessage('验证码已发送', 'success');
            startCountdown(); // 60秒倒计时
        } else {
            showMessage(result.message, 'error');
            sendCodeBtn.disabled = false;
            sendCodeBtn.textContent = '获取验证码';
        }
    } catch (error) {
        showMessage('发送失败,请稍后重试', 'error');
        sendCodeBtn.disabled = false;
        sendCodeBtn.textContent = '获取验证码';
    }
});

// 提交登录
loginForm.addEventListener('submit', async (e) => {
    e.preventDefault();
    
    const params = new URLSearchParams();
    params.append('phone', phoneInput.value.trim());
    params.append('code', codeInput.value.trim());
    
    const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: params
    });
    const result = await response.json();
    
    if (result.success) {
        showMessage('登录成功!欢迎回来,' + result.user.nickname, 'success');
        localStorage.setItem('user', JSON.stringify(result.user));
        setTimeout(() => {
            window.location.href = '/';
        }, 1000);
    } else {
        showMessage(result.message, 'error');
    }
});

3.2 倒计时功能

这是一个经典的 UX 优化:

javascript 复制代码
let countdown = 0;
let countdownTimer = null;

function startCountdown() {
    countdown = 60;
    sendCodeBtn.disabled = true;
    sendCodeBtn.textContent = countdown + 's 后重试';
    sendCodeBtn.classList.add('sending');
    
    countdownTimer = setInterval(() => {
        countdown--;
        if (countdown <= 0) {
            clearInterval(countdownTimer);
            sendCodeBtn.disabled = false;
            sendCodeBtn.textContent = '获取验证码';
            sendCodeBtn.classList.remove('sending');
        } else {
            sendCodeBtn.textContent = countdown + 's 后重试';
        }
    }, 1000);
}

3.3 页面样式

使用渐变背景和圆角卡片,营造现代化感:

css 复制代码
body {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
}

.container {
    background: white;
    border-radius: 20px;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
    padding: 40px;
    width: 100%;
    max-width: 400px;
}

.btn-submit {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    border: none;
    border-radius: 10px;
    padding: 14px;
    font-weight: 600;
    transition: all 0.3s;
}

.btn-submit:hover {
    transform: translateY(-2px);
    box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}

第四步:运行测试

4.1 启动项目

bash 复制代码
mvn spring-boot:run

4.2 访问地址

页面 地址
首页 http://localhost:8080/
登录页 http://localhost:8080/login
注册页 http://localhost:8080/register
H2 控制台 http://localhost:8080/h2-console

4.3 测试流程

  1. 访问 /register → 输入手机号 → 点击"获取验证码"
  2. 收到短信验证码 → 输入验证码和昵称 → 点击"注册"
  3. 注册成功后跳转到 /login → 用同样的手机号登录
  4. 登录成功后跳转到首页,显示用户信息

系统架构总结

让我们用一张图来回顾整个系统的架构:

复制代码
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│   前端页面   │───▶│ Controller  │───▶│   Service   │
│ index.html  │    │ SmsController│   │ AliyunSms   │
│ login.html  │    │ AuthController│  │ UserService │
│register.html│    └─────────────┘    └──────┬──────┘
└─────────────┘                             │
                                            ▼
                                   ┌─────────────────┐
                                   │    Repository   │
                                   │  UserRepository │
                                   └────────┬────────┘
                                            │
                                            ▼
                                   ┌─────────────────┐
                                   │   H2 Database   │
                                   │     users表      │
                                   └─────────────────┘

安全与优化建议

生产环境必做

  1. 替换数据库:H2 → MySQL/PostgreSQL
  2. 密钥管理:使用环境变量或配置中心管理阿里云密钥
  3. 分布式限流:接入 Redis,避免单实例内存限流的问题
  4. HTTPS:生产环境必须启用 SSL
  5. 日志脱敏:手机号、验证码等敏感信息在日志中需要脱敏

可选优化

  1. 图形验证码:在发送短信前增加图形验证码,防止机器刷短信
  2. 登录态管理:接入 Spring Security + JWT,实现真正的会话管理
  3. 限流策略升级:支持按 IP 限流、单日发送次数限制等
  4. 监控告警:接入 Prometheus,监控短信发送成功率

完整 API 列表

方法 路径 功能
POST /api/sms/send 发送短信验证码
POST /api/sms/verify 验证短信验证码
POST /api/auth/check-phone 检查手机号是否已注册
POST /api/auth/register 用户注册
POST /api/auth/login 用户登录

写在最后

通过本篇教程,我们从零搭建了一个完整的验证码登录系统。回顾一下我们做了什么:

  1. ✅ 项目初始化和依赖配置
  2. ✅ 阿里云短信服务集成
  3. ✅ 用户实体和数据访问层
  4. ✅ 业务服务层实现
  5. ✅ RESTful API 设计
  6. ✅ 前端页面开发
  7. ✅ 完整的用户登录/注册流程

这个系统的核心价值在于:

  • 用户体验:无需密码,一键登录
  • 代码质量:分层清晰,职责单一
  • 可扩展性:易于替换短信服务商、数据库等
  • 安全性:内置频率限制、参数校验等安全机制

参考资源

如果这篇文章对你有帮助,欢迎点赞、收藏、关注!有任何问题欢迎在评论区留言交流。

相关推荐
未若君雅裁1 小时前
工厂模式详解:简单工厂、工厂方法与抽象工厂
java·开发语言
夏天的味道٥1 小时前
Spring-AI 多模型接入实战:本地 deepseek + 阿里云百炼 + 硅基流动
人工智能·spring·阿里云
不会写DN1 小时前
通过php 中的Route:: 的写法了解什么是静态类调用
android·java·php
小刘|1 小时前
SpringAIAlibaba快速接入阿里云百炼
java·spring boot·spring·maven
阿酷tony1 小时前
阿里云播放器API和酷播云播放器PI
阿里云·云计算·酷播云播放器
我命由我123451 小时前
由 ImageView 获取到的 Drawable 对象,它的 intrinsicWidth、intrinsicWidth 与实际图片的尺寸
java·开发语言·java-ee·android studio·android jetpack·android-studio·android runtime
Han.miracle1 小时前
Jackson 工具类详解:ObjectMapper 配置、泛型擦除、TypeReference 与 JavaType
java·spring boot·spring
guslegend1 小时前
Java 创建对象有几种方式
java·开发语言
暗暗别做白日梦1 小时前
延时消息的几种实现方式及优缺点
java