Обновление / уведомление другого пользователя в Spring Web - PullRequest
0 голосов
/ 27 октября 2018

У меня есть проблема с дизайном / реализацией, которую я просто не могу обдумать.В настоящее время я работаю над текстовой игрой с несколькими игроками.Я вроде понимаю, как это работает для Player-to-Server, я имел в виду, что Server видит каждого игрока как одного и того же.

Я использую spring-boot 2, spring-web, thymeleaf, hibernate.

Я реализовал пользовательский UserDetails, который возвращается после входа пользователя в систему.

@Entity
@Table(name = "USER")
public class User implements Serializable {

    @Id
    private long userId;

    @Column(unique = true, nullable = false)
    private String userName;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "playerStatsId")
    private PlayerStats stats;
}

public class CurrentUserDetailsService implements UserDetailsService {

    @Override
    public CurrentUser loadUserByUsername(String userName) {

        User user = this.accountRepository.findByUserName(userName)
                .orElseThrow(() -> 
                    new UsernameNotFoundException("User details not found with the provided username: " + userName));

        return new CurrentUser(user);
    }
}

public class CurrentUser implements UserDetails {

    private static final long serialVersionUID = 1L;
    private User user = new User();

    public CurrentUser(User user) {
        this.user = user;
    }

    public PlayerStats getPlayerStats() {
        return this.user.getStats();
    }

    // removed the rest for brevity 
}

Следовательно, в моем контроллере я могу сделать это, чтобы получить CurrentUser.* Обратите внимание, что каждый пользователь также является игроком.

@GetMapping("/attackpage")
public String viewAttackPage(@AuthenticationPrincipal CurrentUser currentUser) {

    // return the page view for list of attacks

    return "someview";
}

Значение currentUser будет отображаться для текущего пользователя (скажем, 1 или 2, 3 и т. Д.).Это прекрасно работает для большинства вещей, происходящих с самим собой, таких как покупка некоторых вещей, обновление профиля и так далее.Но чего я не могу получить или не знаю, как этого добиться, это , когда 2 игрока взаимодействуют .

Например, Игрок 1 атакует Игрока 2. Если я Игрок 1, я должен нажать «Атака» в представлении, выбрать Игрока 2 и отправить команду.Следовательно, в контроллере это будет что-то вроде этого.

@GetMapping("/attack")
public String launchAttack(@AuthenticationPrincipal CurrentUser currentUser, @RequestParam("playername") String player2) {

    updatePlayerState(player2);

    return "someview";
}

public void updatePlayerState(String player) {

    User user = getUserByPlayername(player);
    // perform some update to player state (say health, etc)
    // update back to db?
}

Вот что действительно меня запутало.Как было показано ранее, когда каждый пользователь / игрок входит в систему, набор текущего состояния пользователя (игрока) будет извлечен из БД и сохранен «в памяти».Следовательно, когда Игрок 1 атакует Игрока 2,

  1. Как «уведомить» или обновить Игрока 2, что статистика изменилась, и, таким образом, Игрок 2 должен вытянуть обновленную статистику из БД в память,

  2. Как решить возможную проблему параллелизма здесь?Например, здоровье игрока 2 составляет 50 в БД.Затем игрок 2 выполняет некоторое действие (скажем, приобретает зелье здоровья + 30), которое затем обновляет базу данных (здоровье до 80).Однако непосредственно перед обновлением БД Player 1 уже запустил атаку и извлекает из БД состояние Player 2, где он вернет 50, поскольку БД еще не обновлен.Так что теперь любые изменения, сделанные в getUserByPlayername() и обновлении БД, будут неверными, и все состояние проигрывателя будет «не синхронизировано».Я надеюсь, что здесь есть смысл.

Я понимаю, что в спящем режиме @Version для оптимистической блокировки, но я не уверен, применимо ли это в этом случае.И будет ли Spring-Session полезен в таком случае?

Не следует ли хранить какие-либо данные в памяти при входе пользователя?Должен ли я всегда получать данные из БД только при выполнении какого-либо действия?Вроде когда viewProfile, то я вытаскиваю из accountRepository.или когда viewStats тогда я вытащил из statsRepository и так далее.

Направь меня в правильном направлении.Был бы признателен за любой конкретный пример рода, или какое-то видео / статьи.Если потребуется какая-либо дополнительная информация, дайте мне знать, и я постараюсь объяснить мой случай лучше.

Спасибо.

Ответы [ 2 ]

0 голосов
/ 06 ноября 2018

Я не весенний пользователь, но думаю, что проблема скорее в концептуальной, чем в технической.Я постараюсь дать ответ, который использует общий подход, при написании примеров в стиле JavaEE, чтобы они были понятными и, надеюсь, переносимыми к весне.

Прежде всего: каждая отдельная сущность DETACHEDЭто устаревшие данные.А устаревшие данные не являются «доверяемыми».

Итак:

  1. каждый метод, который изменяет состояние объекта, должен повторно извлекать объект из БД внутри транзакции:

    updatePlayerState() должен быть методом границы транзакции (или вызываться внутри tx), а getUserByPlayername(player) должен извлекать целевой объект из БД.

    JPAговоря: em.merge() запрещено (без надлежащей блокировки, т. е. @Version).

    если вы (или пружина) уже делаете это, добавить нечего.
    WRT «проблема с потерянным обновлением», которую выупомяните в своем 2. помните, что это касается стороны сервера приложений (JPA / Hibernate), но та же проблема может присутствовать на стороне БД, которая должна быть правильно настроена, по крайней мере, для повторяемого чтения изоляция.Взгляните на MySQL не соответствует Repeatable Read действительно , если вы его используете.

вы должны обрабатывать поля контроллера, которые ссылаются на устаревших игроков / пользователей / объекты.У вас есть, по крайней мере, два варианта.

  1. повторная выборка для каждого запроса : предположим, что Player1 атаковал Player2 и уменьшил HP Player2 на 30. Когда Player2 переходит кпредставление, которое показывает его HP, контроллер позади этого представления должен был повторно извлечь объект Player2 / User2 перед рендерингом представления.

    Другими словами, все ваши объекты представления (отсоединенные) должны быть, вроде, в области запроса.

    т.е. вы можете использовать @WebListener для перезагрузки вашего Игрока / Пользователя:

    @WebListener
    public class CurrentUserListener implements ServletRequestListener {
    
        @Override
        public void requestInitialized(ServletRequestEvent sre) {
            CurrentUser currentUser = getCurrentUser();
            currentUser.reload();
        }
    
        @Override
        public void requestDestroyed(ServletRequestEvent sre) {
            // nothing to do
        }
    
        public CurrentUser getCurrentUser() {
            // return the CurrentUser
        }
    }
    

    или бин в области запроса (или любой другой-эквивалентный пружине):

    @RequestScoped
    public class RefresherBean {
        @Inject
        private CurrentUser currentUser;
    
        @PostConstruct
        public void init()
        {
            currentUser.reload();
        }
    }
    
  2. уведомить другие экземпляры контроллера : если обновление прошло успешно, уведомление должно быть отправлено на другие контроллеры.

    т.е. с использованием CDI @Observe (если у вас есть CDI):

    public class CurrentUser implements UserDetails {
    
        private static final long serialVersionUID = 1L;
        private User user = new User();
    
        public CurrentUser(User user) {
            this.user = user;
        }
    
        public PlayerStats getPlayerStats() {
            return this.user.getStats();
        }
    
        public void onUpdate(@Observes(during = TransactionPhase.AFTER_SUCCESS) User user) {
            if(this.user.getId() == user.getId()) {
                this.user = user;
            }
        }
    
        // removed the rest for brevity 
    }
    

    Обратите внимание, что CurrentUser должен быть управляемым сервером объектом.

0 голосов
/ 31 октября 2018

Я думаю, что вы не должны обновлять currentUser в своих методах контроллера и не должны полагаться на данные в этом объекте для представления текущего состояния игрока.Возможно, есть способы заставить это работать, но вам нужно возиться с обновлением контекста безопасности.

Я также рекомендую вам искать пользователей по id вместо userName, поэтому напишуостальная часть этого ответа с таким подходом.Если вы настаиваете на том, чтобы найти Пользователей по имени пользователя, настройте их там, где это необходимо.

Таким образом, для простоты я буду ссылаться на accountRepository в контроллере, а затем, когда вам понадобится получить или обновитьсостояние игрока, используйте

User user = accountRepository.findById(currentUser.getId())

Да, @Version и оптимистическая блокировка поможет с проблемами параллелизма, которые вас беспокоят.Вы можете перезагрузить Entity из базы данных и повторить операцию, если вы поймали @OptimisticLockException.Или, возможно, вы захотите ответить игроку 1 примерно так: «Игрок 2 только что купил зелье исцеления, и теперь у него 80 очков здоровья, вы все еще хотите атаковать?»

...