【问题标题】:JavaFX + Spring (JDBC & @SpringBootApplication & @Autowired & @Transactional)JavaFX + Spring (JDBC & @SpringBootApplication & @Autowired & @Transactional)
【发布时间】:2017-01-09 14:32:56
【问题描述】:

我想通过 Spring JDBC 将 JavaFX 与 DB 访问一起使用。但是我对 Spring 完全陌生,似乎无法完全理解它的功能,尤其是事务处理......

我在我的项目中添加了以下依赖项:

compile 'org.springframework.boot:spring-boot-starter-jdbc'
runtime 'mysql:mysql-connector-java'

...当 GUI 应用程序在 DB 上执行操作时,我想使用 Spring 事务处理机制。据我了解,以下代码应该:

  • 初始化并启动 JavaFX 应用程序 - 创建并显示 GUI 线框
  • 初始化 Spring
  • 配置和注入 JdbcTemplate 依赖项
  • 启动事务处理机制并开始事务
  • 使用 jdbcTemplate 对象在 for loop 的 DB 中创建 5 个条目
  • 模拟错误(通过抛出RuntimeException
  • 恢复对 DB 的操作
  • 退出

所以,总结一下:当 RuntimeException 被抛出到注释为 @Transactional 的方法中时,它应该在应用程序退出之前恢复由该方法创建的所有条目,不是吗?

但是,所有创建的条目都永久保留在数据库中(应用程序退出后我可以在那里看到它们)。所以首先 - 我是否正确理解这些交易应该如何运作?如果是这样,那么如何让它们按我的预期实际工作?

import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.Transactional;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;


@SpringBootApplication
public class SpringTransactional extends Application {
    private Pane viewPane;

    private ConfigurableApplicationContext springContext;

    /** application.properties:
     spring.datasource.driver-class-name = com.mysql.jdbc.Driver
     spring.datasource.url = jdbc:mysql://localhost:3306/db_name?useSSL=false&serverTimezone=UTC
     spring.datasource.username = db_username
     spring.datasource.password = username123
     */
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void init() throws Exception {
        springContext = SpringApplication.run(SpringTransactional.class);
        springContext.getAutowireCapableBeanFactory().autowireBean(this);
    }

    @Override
    public void stop() throws Exception {
        springContext.close();
    }

    @Override
    public void start(Stage primaryStage) {
        viewPane = assembleView(primaryStage);

        try {
            db_transaction_test();
        } catch (RuntimeException e) {
            e.printStackTrace();
        }

        Platform.exit();
    }

    private Pane assembleView(Stage primaryStage) {
        VBox rootPane = new VBox();
        rootPane.setSpacing(10);
        rootPane.setPadding(new Insets(10));
        rootPane.setStyle("-fx-base: #84a7ad;");
        rootPane.getChildren().add(new Label("GUI goes here."));

        primaryStage.setScene(new Scene(rootPane));
        primaryStage.setResizable(false);
        primaryStage.show();

        return rootPane;
    }

    @Transactional
    private void db_transaction_test() {
        for (int i = 0; i < 10; i++) {
            try {
                int entry_name = getEntryId("entry_" + i);
                System.out.println("Created entry id=" + entry_name);
            } catch (DaoException e) {
                e.printStackTrace();
            }

            if (i == 5) {
                throw new RuntimeException("Testing data upload procedure break.");
            }
        }
    }

    /** DB creation and schema:
     CREATE DATABASE db_name;
     CREATE USER db_username;

     USE db_name;
     GRANT ALL ON db_name.* TO db_username;

     SET PASSWORD FOR spz = PASSWORD('username123');
     FLUSH PRIVILEGES;

     CREATE TABLE Entry (
     entry_ID INT NOT NULL AUTO_INCREMENT,
     name   TEXT NOT NULL,

     PRIMARY KEY (entry_ID)
     );
     */
    private int getEntryId(String entryName) throws DaoException {
        List<DbEntry> dbEntries = retrieveEntriesFor(entryName);

        if (dbEntries.size() == 1) {
            return dbEntries.get(0).getEntry_ID();
        } else if (dbEntries.size() == 0) {
            String sqlInsert = "INSERT INTO Entry (name) VALUES (?)";
            jdbcTemplate.update(sqlInsert, entryName);
            dbEntries = retrieveEntriesFor(entryName);
            if (dbEntries.size() == 1) {
                return dbEntries.get(0).getEntry_ID();
            } else {
                throw new DaoException("Invalid results amount received after creating new (" + dbEntries.size() + ") when getting entry for name: " + entryName);
            }
        } else {
            throw new DaoException("Invalid results amount received (" + dbEntries.size() + ") when getting entry for name: " + entryName);
        }
    }

    private List<DbEntry> retrieveEntriesFor(String entryName) {
        return jdbcTemplate.query("SELECT * FROM Entry WHERE name=?;", (ResultSet result, int rowNum) -> unMarshal(result), entryName);
    }

    private DbEntry unMarshal(ResultSet result) throws SQLException {
        DbEntry dbEntry = new DbEntry();
        dbEntry.setEntry_ID(result.getInt("entry_ID"));
        dbEntry.setName(result.getString("name"));
        return dbEntry;
    }

    public class DbEntry {
        private int entry_ID;
        private String name;

        int getEntry_ID() { return entry_ID; }
        void setEntry_ID(int entry_ID) { this.entry_ID = entry_ID; }
        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
    }

    private class DaoException extends Throwable {
        DaoException(String err_msg) { super(err_msg); }
    }
}

【问题讨论】:

    标签: spring javafx spring-boot autowired transactional


    【解决方案1】:

    Spring 中事务的工作方式与 AOP 在 Spring 中的工作方式相同:当您从 Spring 请求具有标记为事务性方法的 bean 时,您实际上会收到该 bean 的代理,其事务性方法的实现“装饰”了您的实现在您的实现类中提供。简而言之,代理类中方法的实现开始一个事务,然后调用实现类中定义的方法,然后提交或回滚事务。

    所以我认为问题是 SpringTransactional 实例不是由 Spring 应用程序上下文创建的,而是由 JavaFX 启动过程创建的(即它是由 JavaFX 框架在您创建时创建的)致电Application.launch())。因此,Spring 无法创建实现事务行为的代理对象。

    尝试将数据库功能分解到一个单独的由 spring 管理的类中,然后将它的一个实例注入到您的应用程序类中。 IE。做类似的事情

    // Note: I'm only familiar with "traditional" Spring, not Spring boot. 
    // Not sure if this annotation is picked up by Spring boot, you may need to 
    // make some changes to the config or something to get this working.
    @Component
    public class DAO {
    
        @Autowired
        private JdbcTemplate jdbcTemplate ;
    
        @Transactional
        private void db_transaction_test() {
            // ...
        }
    
        // ...
    }
    

    然后在您的应用程序类中:

    @SpringBootApplication
    public class SpringTransactional extends Application {
        private Pane viewPane;
    
        private ConfigurableApplicationContext springContext;
    
        @Autowired
        private DAO dao ;
    
        // ...
    
         @Override
        public void start(Stage primaryStage) {
            viewPane = assembleView(primaryStage);
    
            try {
                dao.db_transaction_test();
            } catch (RuntimeException e) {
                e.printStackTrace();
            }
    
            Platform.exit();
        }  
    
        // ...
    }
    

    【讨论】:

    • 您好,感谢您的回复,但正如我上面所说,我对 Spring 完全陌生,并且想要一些解决方案,而无需学习整个“旧”配置方式(有自己的问题),因此 - 作为在这个主题中 - 使用@SpringBootApplication 是这个问题的主要部分之一,也是我真正感兴趣的......
    • @alwi 我认为你完全错过了这里的重点。上面的 // ... 只是应该表明您包含现有代码,即这仍在使用 Spring Boot。您只需要将 DAO 配置为 spring 托管 bean(但是在 Spring Boot 中会发生这种情况)。在我的回答中,我没有说“不要使用 Spring Boot”。关键是@Transactional 仅适用于 spring 管理的 bean:如果您在 Spring 外部创建的对象中的方法上使用它(无论您使用 Spring Boot 还是“经典”Spring 框架)。
    • 我明白你在说什么。很明显 // ... 意味着所有的业务逻辑。在上一条消息中,我指的是您的评论“您可能需要对配置进行一些更改或其他内容才能使其正常工作”以及“我只熟悉 /traditional/ Spring,而不是 Spring boot。”。
    • @alwi “让这个工作”我的意思是“让这个在 Spring Boot 中工作”。很抱歉那里的混乱。我不知道 Spring Boot 是否会仅仅因为类被注释而创建 bean,或者是否需要进一步的配置。作为 Spring Boot 用户,我假设您可以自己弄清楚。
    【解决方案2】:

    经过更多测试,似乎创建单独的 Spring 组件 EntryDao 有效(感谢 James_D),但前提是带有 @Transactional 注释的 db_transaction_test 在该类中 - 下面代码中的选项 A。

    但我真正感兴趣的是选项 B - 当带有 @Transactional 注释的 db_transaction_test 在另一个班级时。这是因为 DAO 类不(也不应该)知道 DB-unrealted 问题是恢复一堆以前的 DB 操作的原因。此信息来自其他“控制器”,其故障不得导致数据完整性问题。因此,在下面的示例中,SpringTransactional 应该是唯一可以抛出此特定RuntimeException("Testing data upload procedure break."); 的示例(作为现实生活中的系统/环境问题的示例)。然而,正如最后的堆栈跟踪所示 - 交易并未在那里初始化。

    那么有没有办法让它按照我的需要使用 Spring @Transactional(又名声明性事务)或仅使用手动(又名程序化)Spring 事务控制?如果这是唯一的方法,那么如何配置DataSourceTransactionManager,同时将@SpringBootApplication 用于“自动配置”,将@Autowired 用于jdbcTemplate 对象?

    主类:

    package tmp;
    
    import javafx.application.Application;
    import javafx.application.Platform;
    import javafx.geometry.Insets;
    import javafx.scene.Scene;
    import javafx.scene.control.Label;
    import javafx.scene.layout.Pane;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.ConfigurableApplicationContext;
    import org.springframework.transaction.annotation.Transactional;
    import tmp.dao.EntryDao;
    
    
    @SpringBootApplication
    public class SpringTransactional extends Application {
        private Pane viewPane;
    
        private ConfigurableApplicationContext springContext;
    
        @Autowired
        private EntryDao dao;
    
        public static void main(String[] args) { launch(args); }
    
        @Override
        public void init() throws Exception {
            springContext = SpringApplication.run(SpringTransactional.class);
            springContext.getAutowireCapableBeanFactory().autowireBean(this);
        }
    
        @Override
        public void stop() throws Exception { springContext.close(); }
    
        @Override
        public void start(Stage primaryStage) {
            viewPane = assembleView(primaryStage);
    
            // OPTION A:
            try {
                dao.db_transaction_test();
            } catch (RuntimeException e) {
                e.printStackTrace();
            }
    
            // OPTION B:
            try {
                db_transaction_test();
            } catch (RuntimeException e) {
                e.printStackTrace();
            }
    
            Platform.exit();
        }
    
        @Transactional
        private void db_transaction_test() {
            for (int i = 0; i < 10; i++) {
                try {
                    int entry_name = dao.getEntryId("entry_" + i);
                    System.out.println("Created entry id=" + entry_name);
                } catch (EntryDao.DaoException e) {
                    e.printStackTrace();
                }
    
                if (i == 5) {
                    throw new RuntimeException("Testing data upload procedure break.");
                }
            }
        }
    
        private Pane assembleView(Stage primaryStage) {
            VBox rootPane = new VBox();
            rootPane.setSpacing(10);
            rootPane.setPadding(new Insets(10));
            rootPane.setStyle("-fx-base: #84a7ad;");
            rootPane.getChildren().add(new Label("GUI goes here."));
    
            primaryStage.setScene(new Scene(rootPane));
            primaryStage.setResizable(false);
            primaryStage.show();
    
            return rootPane;
        }
    }
    

    EntryDao 类:

    package tmp.dao;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Component;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.sql.ResultSet;
    import java.sql.SQLException;
    import java.util.List;
    
    /**
     * DB creation and schema:
     * CREATE DATABASE db_name;
     * CREATE USER db_username;
     * <p>
     * USE db_name;
     * GRANT ALL ON db_name.* TO db_username;
     * <p>
     * SET PASSWORD FOR spz = PASSWORD('username123');
     * FLUSH PRIVILEGES;
     * <p>
     * CREATE TABLE Entry (
     * entry_ID INT NOT NULL AUTO_INCREMENT,
     * name   TEXT NOT NULL,
     * <p>
     * PRIMARY KEY (entry_ID)
     * );
     */
    @Component
    public class EntryDao {
        /**
         * application.properties:
         * spring.datasource.driver-class-name = com.mysql.jdbc.Driver
         * spring.datasource.url = jdbc:mysql://localhost:3306/db_name?useSSL=false&serverTimezone=UTC
         * spring.datasource.username = db_username
         * spring.datasource.password = username123
         */
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Transactional
        public void db_transaction_test() {
            for (int i = 0; i < 10; i++) {
                try {
                    int entry_name = getEntryId("entry_" + i);
                    System.out.println("Created entry id=" + entry_name);
                } catch (EntryDao.DaoException e) {
                    e.printStackTrace();
                }
    
                if (i == 5) {
                    throw new RuntimeException("Testing data upload procedure break.");
                }
            }
        }
    
        public int getEntryId(String entryName) throws DaoException {
            List<DbEntry> dbEntries = retrieveEntriesFor(entryName);
    
            if (dbEntries.size() == 1) {
                return dbEntries.get(0).getEntry_ID();
            } else if (dbEntries.size() == 0) {
                String sqlInsert = "INSERT INTO Entry (name) VALUES (?)";
                jdbcTemplate.update(sqlInsert, entryName);
                dbEntries = retrieveEntriesFor(entryName);
                if (dbEntries.size() == 1) {
                    return dbEntries.get(0).getEntry_ID();
                } else {
                    throw new DaoException("Invalid results amount received after creating new (" + dbEntries.size() + ") when getting entry for name: " + entryName);
                }
            } else {
                throw new DaoException("Invalid results amount received (" + dbEntries.size() + ") when getting entry for name: " + entryName);
            }
        }
    
        private List<DbEntry> retrieveEntriesFor(String entryName) {
            return jdbcTemplate.query("SELECT * FROM Entry WHERE name=?;", (ResultSet result, int rowNum) -> unMarshal(result), entryName);
        }
    
        private DbEntry unMarshal(ResultSet result) throws SQLException {
            DbEntry dbEntry = new DbEntry();
            dbEntry.setEntry_ID(result.getInt("entry_ID"));
            dbEntry.setName(result.getString("name"));
            return dbEntry;
        }
    
        public class DbEntry {
            private int entry_ID;
            private String name;
    
            int getEntry_ID() { return entry_ID; }
            void setEntry_ID(int entry_ID) { this.entry_ID = entry_ID; }
            public String getName() { return name; }
            public void setName(String name) { this.name = name; }
        }
    
        public class DaoException extends Throwable { DaoException(String err_msg) { super(err_msg); } }
    }
    

    堆栈跟踪

      .   ____          _            __ _ _
     /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
    ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
     \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
      '  |____| .__|_| |_|_| |_\__, | / / / /
     =========|_|==============|___/=/_/_/_/
     :: Spring Boot ::        (v1.4.3.RELEASE)
    
    2017-01-10 09:41:48.902  INFO 1860 --- [JavaFX-Launcher] o.s.boot.SpringApplication               : Starting application on alwihasolaptop with PID 1860 (started by alwi in C:\alwi\Workspace_SPZ\GCodeClient)
    2017-01-10 09:41:48.905  INFO 1860 --- [JavaFX-Launcher] o.s.boot.SpringApplication               : No active profile set, falling back to default profiles: default
    2017-01-10 09:41:48.965  INFO 1860 --- [JavaFX-Launcher] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@18660f3: startup date [Tue Jan 10 09:41:48 CET 2017]; root of context hierarchy
    2017-01-10 09:41:49.917  INFO 1860 --- [JavaFX-Launcher] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
    2017-01-10 09:41:49.927  INFO 1860 --- [JavaFX-Launcher] o.s.boot.SpringApplication               : Started application in 1.384 seconds (JVM running for 1.969)
    Created entry id=73
    Created entry id=74
    Created entry id=75
    Created entry id=76
    Created entry id=77
    Created entry id=78
    java.lang.RuntimeException: Testing data upload procedure break.
        at tmp.dao.EntryDao.db_transaction_test(EntryDao.java:53)
        at tmp.dao.EntryDao$$FastClassBySpringCGLIB$$a857b433.invoke(<generated>)
        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:721)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
        at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99)
        at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282)
        at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:656)
        at tmp.dao.EntryDao$$EnhancerBySpringCGLIB$$84e8651e.db_transaction_test(<generated>)
        at tmp.SpringTransactional.start(SpringTransactional.java:45)
        at com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$162(LauncherImpl.java:863)
        at com.sun.javafx.application.PlatformImpl.lambda$runAndWait$175(PlatformImpl.java:326)
        at com.sun.javafx.application.PlatformImpl.lambda$null$173(PlatformImpl.java:295)
        at java.security.AccessController.doPrivileged(Native Method)
        at com.sun.javafx.application.PlatformImpl.lambda$runLater$174(PlatformImpl.java:294)
        at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
        at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
        at com.sun.glass.ui.win.WinApplication.lambda$null$148(WinApplication.java:191)
        at java.lang.Thread.run(Thread.java:745)
    Created entry id=73
    Created entry id=74
    Created entry id=75
    Created entry id=76
    Created entry id=77
    Created entry id=78
    2017-01-10 09:41:50.545  INFO 1860 --- [lication Thread] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@18660f3: startup date [Tue Jan 10 09:41:48 CET 2017]; root of context hierarchy
    java.lang.RuntimeException: Testing data upload procedure break.
        at tmp.SpringTransactional.db_transaction_test(SpringTransactional.java:71)
        at tmp.SpringTransactional.start(SpringTransactional.java:52)
        at com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$162(LauncherImpl.java:863)
        at com.sun.javafx.application.PlatformImpl.lambda$runAndWait$175(PlatformImpl.java:326)
        at com.sun.javafx.application.PlatformImpl.lambda$null$173(PlatformImpl.java:295)
        at java.security.AccessController.doPrivileged(Native Method)
        at com.sun.javafx.application.PlatformImpl.lambda$runLater$174(PlatformImpl.java:294)
        at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
        at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
        at com.sun.glass.ui.win.WinApplication.lambda$null$148(WinApplication.java:191)
        at java.lang.Thread.run(Thread.java:745)
    2017-01-10 09:41:50.546  INFO 1860 --- [lication Thread] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown
    
    Process finished with exit code 0
    

    解决方案:

    目前我发现的最佳解决方案是使用 Spring TransactionTemplate 和额外的回调类:

    package tmp.dao;
    
    public abstract class DbTransactionTask { public abstract void executeTask(); }
    

    SpringTransactionaldb_transaction_test()方法中(注意@Transactional不在):

    private void db_transaction_test() {
        DbTransactionTask dbTask = new DbTransactionTask() {
            @Override
            public void executeTask() {
                for (int i = 0; i < 10; i++) {
                    try {
                        int entry_name = dao.getEntryId("entry_" + i);
                        System.out.println("Created entry id=" + entry_name);
                    } catch (EntryDao.DaoException e) {
                        e.printStackTrace();
                    }
    
                    if (i == 5) {
                        throw new RuntimeException("Testing data upload procedure break.");
                    }
                }
            }
        };
    
        dao.executeTransactionWithoutResult(dbTask);
    }
    

    EntryDao 类需要这个额外的代码:

    @Autowired
    private TransactionTemplate transactionTemplate;
    
    public void executeTransactionWithoutResult(DbTransactionTask dbTask) {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
                dbTask.executeTask();
            }
        });
    }
    

    【讨论】:

    • 所以典型的设置是在业务层方法上标记事务,然后将 DAO 注入业务层,然后由其方法调用它。但是,无论您如何构造它,您的问题的实际答案都是相同的:@Transactional 仅适用于 Spring 管理的 bean。在您的问题中,您试图在 Spring 外部创建的对象上使用它。
    • 那么问题中的springContext.getAutowireCapableBeanFactory().autowireBean(this); 呢?它能够注入@Autowired private JdbcTemplate jdbcTemplate;,尽管正如您所说的那样,它是“在 Spring 外部创建的对象上”...
    • 正确:这基本上告诉应用程序上下文将所有已知的 bean 注入到您传入的对象的注释 @Autowire 的字段中(在这种情况下)。但它不会——事实上不可能——用代理对象替换内存中的那个对象。所以当它注入 bean 时,它不能改变现有对象的行为。因此,您的 db_transaction_test() 方法仍然完全按照您编写的代码执行,不多也不少。它不能用事务行为神奇地装饰属于现有对象的方法的实现。
    • 我喜欢 spring 的一点是,它很容易看出它是如何工作的,因此很容易看到它在做什么。没有“手无寸铁”。所以很明显autowireBean(...) 方法做了什么:获取提供的对象,遍历它的字段和set..() 方法寻找注释,并设置字段或在它有一个匹配的bean 时调用set 方法。这一切都只是普通的标准 Java,尽管它非常有用。但是(显然,当然)您不能更改属于已在内存中的对象的方法的实现。
    猜你喜欢
    • 1970-01-01
    • 2015-08-19
    • 1970-01-01
    • 2013-04-24
    • 2017-12-06
    • 1970-01-01
    • 2019-11-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多