From 16ca97b9cb8223b8aeca9bb608f4e2b21fce796e Mon Sep 17 00:00:00 2001 From: "calvin.xiao" Date: Wed, 11 Nov 2015 00:51:58 +0800 Subject: [PATCH] =?UTF-8?q?Exception=E5=A4=84=E7=90=86=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=EF=BC=8C=E4=BD=BF=E7=94=A8Clock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/boot-api/pom.xml | 12 ++++++ .../examples/bootapi/api/AccountEndPoint.java | 19 +++++---- .../examples/bootapi/api/BookEndpoint.java | 19 ++++++++- .../bootapi/api/support/RestException.java | 15 ------- .../api/support/RestExceptionHandler.java | 22 ---------- .../api/support/ServiceExceptionHandler.java | 42 +++++++++++++++++++ .../examples/bootapi/domain/Message.java | 4 +- .../bootapi/service/AccountService.java | 14 ++----- .../bootapi/service/BookAdminService.java | 14 ++++--- .../bootapi/service/BookBorrowService.java | 41 +++++++++--------- .../bootapi/service/ServiceException.java | 15 +++++++ .../bootapi/service/exception/ErrorCode.java | 17 ++++++++ .../resources/application-prod.properties | 1 + .../src/main/resources/application.properties | 2 + .../bootapi/functional/BookEndpointTest.java | 25 ++++++++--- modules/core/pom.xml | 12 ------ .../springside/modules/mapper/JsonMapper.java | 21 ++-------- 17 files changed, 177 insertions(+), 118 deletions(-) delete mode 100644 examples/boot-api/src/main/java/org/springside/examples/bootapi/api/support/RestException.java delete mode 100644 examples/boot-api/src/main/java/org/springside/examples/bootapi/api/support/RestExceptionHandler.java create mode 100644 examples/boot-api/src/main/java/org/springside/examples/bootapi/api/support/ServiceExceptionHandler.java create mode 100644 examples/boot-api/src/main/java/org/springside/examples/bootapi/service/ServiceException.java create mode 100644 examples/boot-api/src/main/java/org/springside/examples/bootapi/service/exception/ErrorCode.java diff --git a/examples/boot-api/pom.xml b/examples/boot-api/pom.xml index 5804df7c..868fbf1c 100644 --- a/examples/boot-api/pom.xml +++ b/examples/boot-api/pom.xml @@ -22,6 +22,7 @@ 1.4.6 1.10 2.2.0 + 1.3.176 1.7 @@ -124,6 +125,17 @@ org.springframework.boot spring-boot-maven-plugin + + org.flywaydb + flyway-maven-plugin + 3.1 + + org.h2.Driver + jdbc:h2:file:~/.h2/bootapi;AUTO_SERVER=TRUE;DB_CLOSE_DELAY=-1; + sa + + + diff --git a/examples/boot-api/src/main/java/org/springside/examples/bootapi/api/AccountEndPoint.java b/examples/boot-api/src/main/java/org/springside/examples/bootapi/api/AccountEndPoint.java index f026556a..e9640d91 100644 --- a/examples/boot-api/src/main/java/org/springside/examples/bootapi/api/AccountEndPoint.java +++ b/examples/boot-api/src/main/java/org/springside/examples/bootapi/api/AccountEndPoint.java @@ -1,15 +1,18 @@ package org.springside.examples.bootapi.api; +import java.util.Collections; +import java.util.Map; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.springside.examples.bootapi.api.support.RestException; import org.springside.examples.bootapi.service.AccountService; +import org.springside.examples.bootapi.service.ServiceException; +import org.springside.examples.bootapi.service.exception.ErrorCode; import org.springside.modules.web.MediaTypes; // Spring Restful MVC Controller的标识, 直接输出内容,不调用template引擎. @@ -21,14 +24,16 @@ public class AccountEndPoint { @Autowired private AccountService accountServcie; - @RequestMapping(value = "/api/accounts/login", produces = MediaTypes.TEXT_PLAIN_UTF_8) - public String login(@RequestParam("email") String email, @RequestParam("password") String password) { + @RequestMapping(value = "/api/accounts/login", produces = MediaTypes.JSON_UTF_8) + public Map login(@RequestParam("email") String email, @RequestParam("password") String password) { if (StringUtils.isEmpty(email) || StringUtils.isEmpty(password)) { - throw new RestException("User or password empty", HttpStatus.BAD_REQUEST); + throw new ServiceException("User or password empty", ErrorCode.BAD_REQUEST); } - return accountServcie.login(email, password); + String token = accountServcie.login(email, password); + + return Collections.singletonMap("token", token); } @RequestMapping(value = "/api/accounts/logout") @@ -41,7 +46,7 @@ public class AccountEndPoint { @RequestParam("password") String password) { if (StringUtils.isEmpty(email) || StringUtils.isEmpty(name) || StringUtils.isEmpty(password)) { - throw new RestException("User or name or password empty", HttpStatus.BAD_REQUEST); + throw new ServiceException("User or name or password empty", ErrorCode.BAD_REQUEST); } accountServcie.register(email, name, password); diff --git a/examples/boot-api/src/main/java/org/springside/examples/bootapi/api/BookEndpoint.java b/examples/boot-api/src/main/java/org/springside/examples/bootapi/api/BookEndpoint.java index 24c76983..9e34e00c 100644 --- a/examples/boot-api/src/main/java/org/springside/examples/bootapi/api/BookEndpoint.java +++ b/examples/boot-api/src/main/java/org/springside/examples/bootapi/api/BookEndpoint.java @@ -18,6 +18,8 @@ import org.springside.examples.bootapi.dto.BookDto; import org.springside.examples.bootapi.service.AccountService; import org.springside.examples.bootapi.service.BookAdminService; import org.springside.examples.bootapi.service.BookBorrowService; +import org.springside.examples.bootapi.service.ServiceException; +import org.springside.examples.bootapi.service.exception.ErrorCode; import org.springside.modules.mapper.BeanMapper; import org.springside.modules.web.MediaTypes; @@ -53,7 +55,7 @@ public class BookEndpoint { @RequestMapping(value = "/api/books", method = RequestMethod.POST, consumes = MediaTypes.JSON_UTF_8) public void createBook(@RequestBody BookDto bookDto, @RequestParam(value = "token", required = false) String token) { - + checkToken(token); // 使用Header中的Token,查找登录用户 Account currentUser = accountService.getLoginUser(token); @@ -67,6 +69,7 @@ public class BookEndpoint { @RequestMapping(value = "/api/books/{id}/modify", method = RequestMethod.POST, consumes = MediaTypes.JSON_UTF_8) public void modifyBook(@RequestBody BookDto bookDto, @RequestParam(value = "token", required = false) String token) { + checkToken(token); Account currentUser = accountService.getLoginUser(token); Book book = BeanMapper.map(bookDto, Book.class); adminService.modifyBook(book, currentUser.id); @@ -74,6 +77,7 @@ public class BookEndpoint { @RequestMapping(value = "/api/books/{id}/delete") public void deleteBook(@PathVariable("id") Long id, @RequestParam(value = "token", required = false) String token) { + checkToken(token); Account currentUser = accountService.getLoginUser(token); adminService.deleteBook(id, currentUser.id); } @@ -81,6 +85,7 @@ public class BookEndpoint { @RequestMapping(value = "/api/books/{id}/request") public void applyBorrowRequest(@PathVariable("id") Long id, @RequestParam(value = "token", required = false) String token) { + checkToken(token); Account currentUser = accountService.getLoginUser(token); borrowService.applyBorrowRequest(id, currentUser); } @@ -88,6 +93,7 @@ public class BookEndpoint { @RequestMapping(value = "/api/books/{id}/cancel") public void cancelBorrowRequest(@PathVariable("id") Long id, @RequestParam(value = "token", required = false) String token) { + checkToken(token); Account currentUser = accountService.getLoginUser(token); borrowService.cancelBorrowRequest(id, currentUser); } @@ -95,6 +101,7 @@ public class BookEndpoint { @RequestMapping(value = "/api/books/{id}/confirm") public void markBookBorrowed(@PathVariable("id") Long id, @RequestParam(value = "token", required = false) String token) { + checkToken(token); Account currentUser = accountService.getLoginUser(token); borrowService.markBookBorrowed(id, currentUser); } @@ -102,6 +109,7 @@ public class BookEndpoint { @RequestMapping(value = "/api/books/{id}/reject") public void rejectBorrowRequest(@PathVariable("id") Long id, @RequestParam(value = "token", required = false) String token) { + checkToken(token); Account currentUser = accountService.getLoginUser(token); borrowService.rejectBorrowRequest(id, currentUser); } @@ -109,12 +117,14 @@ public class BookEndpoint { @RequestMapping(value = "/api/books/{id}/return") public void markBookReturned(@PathVariable("id") Long id, @RequestParam(value = "token", required = false) String token) { + checkToken(token); Account currentUser = accountService.getLoginUser(token); borrowService.markBookReturned(id, currentUser); } @RequestMapping(value = "/api/mybook", produces = MediaTypes.JSON_UTF_8) public List listMyBook(@RequestParam(value = "token", required = false) String token, Pageable pageable) { + checkToken(token); Account currentUser = accountService.getLoginUser(token); List books = adminService.listMyBook(currentUser.id, pageable); return BeanMapper.mapList(books, BookDto.class); @@ -123,8 +133,15 @@ public class BookEndpoint { @RequestMapping(value = "/api/myborrowedbook", produces = MediaTypes.JSON_UTF_8) public List listMyBorrowedBook(@RequestParam(value = "token", required = false) String token, Pageable pageable) { + checkToken(token); Account currentUser = accountService.getLoginUser(token); List books = borrowService.listMyBorrowedBook(currentUser.id, pageable); return BeanMapper.mapList(books, BookDto.class); } + + private void checkToken(String token) { + if (token == null) { + throw new ServiceException("No token in request", ErrorCode.NO_TOKEN); + } + } } diff --git a/examples/boot-api/src/main/java/org/springside/examples/bootapi/api/support/RestException.java b/examples/boot-api/src/main/java/org/springside/examples/bootapi/api/support/RestException.java deleted file mode 100644 index b32e6eaa..00000000 --- a/examples/boot-api/src/main/java/org/springside/examples/bootapi/api/support/RestException.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.springside.examples.bootapi.api.support; - -import org.springframework.http.HttpStatus; - -public class RestException extends RuntimeException { - - private static final long serialVersionUID = -8634700792767837033L; - - public HttpStatus status; - - public RestException(String message, HttpStatus status) { - super(message); - this.status = status; - } -} diff --git a/examples/boot-api/src/main/java/org/springside/examples/bootapi/api/support/RestExceptionHandler.java b/examples/boot-api/src/main/java/org/springside/examples/bootapi/api/support/RestExceptionHandler.java deleted file mode 100644 index 5f27d46e..00000000 --- a/examples/boot-api/src/main/java/org/springside/examples/bootapi/api/support/RestExceptionHandler.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.springside.examples.bootapi.api.support; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.context.request.WebRequest; -import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; -import org.springside.modules.web.MediaTypes; - -@ControllerAdvice -public class RestExceptionHandler extends ResponseEntityExceptionHandler { - - @ExceptionHandler(value = { RestException.class }) - public final ResponseEntity handleException(RestException ex, WebRequest request) { - logger.error(ex.getMessage(), ex); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.parseMediaType(MediaTypes.TEXT_PLAIN_UTF_8)); - return new ResponseEntity(ex.getMessage(), headers, ex.status); - } -} diff --git a/examples/boot-api/src/main/java/org/springside/examples/bootapi/api/support/ServiceExceptionHandler.java b/examples/boot-api/src/main/java/org/springside/examples/bootapi/api/support/ServiceExceptionHandler.java new file mode 100644 index 00000000..649e2df0 --- /dev/null +++ b/examples/boot-api/src/main/java/org/springside/examples/bootapi/api/support/ServiceExceptionHandler.java @@ -0,0 +1,42 @@ +package org.springside.examples.bootapi.api.support; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import org.springside.examples.bootapi.service.ServiceException; +import org.springside.modules.web.MediaTypes; + +@ControllerAdvice +public class ServiceExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler(value = { ServiceException.class }) + public final ResponseEntity handleException(ServiceException ex, HttpServletRequest request) { + logger.error(ex.getMessage() + ", request from " + request.getRemoteAddr(), ex); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType(MediaTypes.JSON_UTF_8)); + ErrorResult result = new ErrorResult(ex.code.code, ex.getMessage()); + return new ResponseEntity(result, headers, HttpStatus.valueOf(ex.code.httpStatus)); + } + + public static class ErrorResult { + + public int code; + public String message; + + public ErrorResult() { + + } + + public ErrorResult(int code, String message) { + this.code = code; + this.message = message; + } + + } +} diff --git a/examples/boot-api/src/main/java/org/springside/examples/bootapi/domain/Message.java b/examples/boot-api/src/main/java/org/springside/examples/bootapi/domain/Message.java index f8faef6b..ffea8cc6 100644 --- a/examples/boot-api/src/main/java/org/springside/examples/bootapi/domain/Message.java +++ b/examples/boot-api/src/main/java/org/springside/examples/bootapi/domain/Message.java @@ -32,10 +32,10 @@ public class Message { } - public Message(Account receiver, String message) { + public Message(Account receiver, String message, Date receiveDate) { this.receiver = receiver; this.message = message; - this.receiveDate = new Date(); + this.receiveDate = receiveDate; } @Override diff --git a/examples/boot-api/src/main/java/org/springside/examples/bootapi/service/AccountService.java b/examples/boot-api/src/main/java/org/springside/examples/bootapi/service/AccountService.java index 1d052256..81597874 100644 --- a/examples/boot-api/src/main/java/org/springside/examples/bootapi/service/AccountService.java +++ b/examples/boot-api/src/main/java/org/springside/examples/bootapi/service/AccountService.java @@ -9,12 +9,11 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.metrics.CounterService; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springside.examples.bootapi.api.support.RestException; import org.springside.examples.bootapi.domain.Account; import org.springside.examples.bootapi.repository.AccountDao; +import org.springside.examples.bootapi.service.exception.ErrorCode; import org.springside.modules.utils.Digests; import org.springside.modules.utils.Encodes; import org.springside.modules.utils.Ids; @@ -53,11 +52,11 @@ public class AccountService { Account account = accountDao.findByEmail(email); if (account == null) { - throw new RestException("User not exist", HttpStatus.UNAUTHORIZED); + throw new ServiceException("User not exist", ErrorCode.UNAUTHORIZED); } if (!account.hashPassword.equals(hashPassword(password))) { - throw new RestException("Password wrong", HttpStatus.UNAUTHORIZED); + throw new ServiceException("Password wrong", ErrorCode.UNAUTHORIZED); } String token = Ids.uuid2(); @@ -78,15 +77,10 @@ public class AccountService { public Account getLoginUser(String token) { - if (token == null) { - logger.error("no token"); - throw new RestException("User doesn't login", HttpStatus.UNAUTHORIZED); - } - Account account = loginUsers.getIfPresent(token); if (account == null) { - throw new RestException("User doesn't login", HttpStatus.UNAUTHORIZED); + throw new ServiceException("User doesn't login", ErrorCode.UNAUTHORIZED); } return account; diff --git a/examples/boot-api/src/main/java/org/springside/examples/bootapi/service/BookAdminService.java b/examples/boot-api/src/main/java/org/springside/examples/bootapi/service/BookAdminService.java index 4145d49e..35caa072 100644 --- a/examples/boot-api/src/main/java/org/springside/examples/bootapi/service/BookAdminService.java +++ b/examples/boot-api/src/main/java/org/springside/examples/bootapi/service/BookAdminService.java @@ -1,19 +1,18 @@ package org.springside.examples.bootapi.service; -import java.util.Date; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springside.examples.bootapi.api.support.RestException; import org.springside.examples.bootapi.domain.Account; import org.springside.examples.bootapi.domain.Book; import org.springside.examples.bootapi.repository.BookDao; +import org.springside.examples.bootapi.service.exception.ErrorCode; +import org.springside.modules.utils.Clock; // Spring Bean的标识. @Service @@ -24,6 +23,9 @@ public class BookAdminService { @Autowired private BookDao bookDao; + // 可注入的Clock,方便测试时控制日期 + protected Clock clock = Clock.DEFAULT; + @Transactional(readOnly = true) public Iterable findAll(Pageable pageable) { return bookDao.findAll(pageable); @@ -44,7 +46,7 @@ public class BookAdminService { book.owner = owner; book.status = Book.STATUS_IDLE; - book.onboardDate = new Date(); + book.onboardDate = clock.getCurrentDate(); bookDao.save(book); } @@ -52,7 +54,7 @@ public class BookAdminService { @Transactional public void modifyBook(Book book, Long currentAccountId) { if (!currentAccountId.equals(book.owner.id)) { - throw new RestException("User can't modify others book", HttpStatus.FORBIDDEN); + throw new ServiceException("User can't modify others book", ErrorCode.BOOK_OWNERSHIP_WRONG); } Book orginalBook = bookDao.findOne(book.id); @@ -66,7 +68,7 @@ public class BookAdminService { Book book = bookDao.findOne(id); if (!currentAccountId.equals(book.owner.id)) { - throw new RestException("User can't delete others book", HttpStatus.FORBIDDEN); + throw new ServiceException("User can't delete others book", ErrorCode.BOOK_OWNERSHIP_WRONG); } bookDao.delete(id); diff --git a/examples/boot-api/src/main/java/org/springside/examples/bootapi/service/BookBorrowService.java b/examples/boot-api/src/main/java/org/springside/examples/bootapi/service/BookBorrowService.java index 592dce62..bfbccab4 100644 --- a/examples/boot-api/src/main/java/org/springside/examples/bootapi/service/BookBorrowService.java +++ b/examples/boot-api/src/main/java/org/springside/examples/bootapi/service/BookBorrowService.java @@ -1,21 +1,20 @@ package org.springside.examples.bootapi.service; -import java.util.Date; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springside.examples.bootapi.api.support.RestException; import org.springside.examples.bootapi.domain.Account; import org.springside.examples.bootapi.domain.Book; import org.springside.examples.bootapi.domain.Message; import org.springside.examples.bootapi.repository.BookDao; import org.springside.examples.bootapi.repository.MessageDao; +import org.springside.examples.bootapi.service.exception.ErrorCode; +import org.springside.modules.utils.Clock; // Spring Bean的标识. @Service @@ -29,6 +28,9 @@ public class BookBorrowService { @Autowired protected MessageDao messageDao; + // 可注入的Clock,方便测试时控制日期 + protected Clock clock = Clock.DEFAULT; + @Transactional public void applyBorrowRequest(Long id, Account borrower) { Book book = bookDao.findOne(id); @@ -36,12 +38,13 @@ public class BookBorrowService { if (!book.status.equals(Book.STATUS_IDLE)) { logger.error("User request the book not idle, user id:" + borrower.id + ",book id:" + id + ",status:" + book.status); - throw new RestException("The book is not idle", HttpStatus.BAD_REQUEST); + throw new ServiceException("The book is not idle", ErrorCode.BOOK_STATUS_WRONG); } if (borrower.id.equals(book.owner.id)) { logger.error("User borrow the book himself, user id:" + borrower.id + ",book id:" + id); - throw new RestException("User shouldn't borrower the book which is himeself", HttpStatus.BAD_REQUEST); + throw new ServiceException("User shouldn't borrower the book which is himeself", + ErrorCode.BOOK_OWNERSHIP_WRONG); } book.status = Book.STATUS_REQUEST; @@ -49,7 +52,7 @@ public class BookBorrowService { bookDao.save(book); Message message = new Message(book.owner, - String.format("Apply book <%s> request by %s", book.title, borrower.name)); + String.format("Apply book <%s> request by %s", book.title, borrower.name), clock.getCurrentDate()); messageDao.save(message); } @@ -61,13 +64,13 @@ public class BookBorrowService { if (!book.status.equals(Book.STATUS_REQUEST)) { logger.error("User cancel the book not reqesting, user id:" + borrower.id + ",book id:" + id + ",status:" + book.status); - throw new RestException("The book is not requesting", HttpStatus.BAD_REQUEST); + throw new ServiceException("The book is not requesting", ErrorCode.BOOK_STATUS_WRONG); } if (!borrower.id.equals(book.borrower.id)) { logger.error("User cancel the book not request by him, user id:" + borrower.id + ",book id:" + id + ",borrower id" + book.borrower.id); - throw new RestException("User can't cancel other ones request", HttpStatus.FORBIDDEN); + throw new ServiceException("User can't cancel other ones request", ErrorCode.BOOK_OWNERSHIP_WRONG); } book.status = Book.STATUS_IDLE; @@ -75,7 +78,7 @@ public class BookBorrowService { bookDao.save(book); Message message = new Message(book.owner, - String.format("Cancel book <%s> request by %s", book.title, borrower.name)); + String.format("Cancel book <%s> request by %s", book.title, borrower.name), clock.getCurrentDate()); messageDao.save(message); } @@ -87,21 +90,21 @@ public class BookBorrowService { if (!book.status.equals(Book.STATUS_REQUEST)) { logger.error("User confirm the book not reqesting, user id:" + owner.id + ",book id:" + id + ",status:" + book.status); - throw new RestException("The book is not requesting", HttpStatus.BAD_REQUEST); + throw new ServiceException("The book is not requesting", ErrorCode.BOOK_STATUS_WRONG); } if (!owner.id.equals(book.owner.id)) { logger.error("User confirm the book not himself, user id:" + owner.id + ",book id:" + id + ",owner id" + book.owner.id); - throw new RestException("User can't cofirm others book", HttpStatus.FORBIDDEN); + throw new ServiceException("User can't cofirm others book", ErrorCode.BOOK_OWNERSHIP_WRONG); } book.status = Book.STATUS_OUT; - book.borrowDate = new Date(); + book.borrowDate = clock.getCurrentDate(); bookDao.save(book); Message message = new Message(book.borrower, - String.format("Confirm book <%s> request by %s", book.title, owner.name)); + String.format("Confirm book <%s> request by %s", book.title, owner.name), clock.getCurrentDate()); messageDao.save(message); } @@ -112,14 +115,14 @@ public class BookBorrowService { if (!book.status.equals(Book.STATUS_REQUEST)) { logger.error("User reject the book not reqesting, user id:" + owner.id + ",book id:" + id + ",status:" + book.status); - throw new RestException("The book is not requesting", HttpStatus.BAD_REQUEST); + throw new ServiceException("The book is not requesting", ErrorCode.BOOK_STATUS_WRONG); } if (!owner.id.equals(book.owner.id)) { logger.error("User reject the book not himself, user id:" + owner.id + ",book id:" + id + ",owener id" + book.owner.id); - throw new RestException("User can't reject others book", HttpStatus.FORBIDDEN); + throw new ServiceException("User can't reject others book", ErrorCode.BOOK_OWNERSHIP_WRONG); } book.status = Book.STATUS_IDLE; @@ -128,7 +131,7 @@ public class BookBorrowService { bookDao.save(book); Message message = new Message(book.borrower, - String.format("Reject book <%s> request by %s", book.title, owner.name)); + String.format("Reject book <%s> request by %s", book.title, owner.name), clock.getCurrentDate()); messageDao.save(message); } @@ -139,13 +142,13 @@ public class BookBorrowService { if (!book.status.equals(Book.STATUS_OUT)) { logger.error( "User return the book not out, user id:" + owner.id + ",book id:" + id + ",status:" + book.status); - throw new RestException("The book is not borrowing", HttpStatus.BAD_REQUEST); + throw new ServiceException("The book is not borrowing", ErrorCode.BOOK_STATUS_WRONG); } if (!owner.id.equals(book.owner.id)) { logger.error("User return the book not himself, user id:" + owner.id + ",book id:" + id + ",owner id" + book.owner.id); - throw new RestException("User can't make others book returned", HttpStatus.FORBIDDEN); + throw new ServiceException("User can't make others book returned", ErrorCode.BOOK_OWNERSHIP_WRONG); } book.status = Book.STATUS_IDLE; @@ -154,7 +157,7 @@ public class BookBorrowService { bookDao.save(book); Message message = new Message(book.borrower, - String.format("Mark book <%s> returned by %s", book.title, owner.name)); + String.format("Mark book <%s> returned by %s", book.title, owner.name), clock.getCurrentDate()); messageDao.save(message); } diff --git a/examples/boot-api/src/main/java/org/springside/examples/bootapi/service/ServiceException.java b/examples/boot-api/src/main/java/org/springside/examples/bootapi/service/ServiceException.java new file mode 100644 index 00000000..7ec09ab5 --- /dev/null +++ b/examples/boot-api/src/main/java/org/springside/examples/bootapi/service/ServiceException.java @@ -0,0 +1,15 @@ +package org.springside.examples.bootapi.service; + +import org.springside.examples.bootapi.service.exception.ErrorCode; + +public class ServiceException extends RuntimeException { + + private static final long serialVersionUID = -8634700792767837033L; + + public ErrorCode code; + + public ServiceException(String message, ErrorCode code) { + super(message); + this.code = code; + } +} diff --git a/examples/boot-api/src/main/java/org/springside/examples/bootapi/service/exception/ErrorCode.java b/examples/boot-api/src/main/java/org/springside/examples/bootapi/service/exception/ErrorCode.java new file mode 100644 index 00000000..43b1ffa1 --- /dev/null +++ b/examples/boot-api/src/main/java/org/springside/examples/bootapi/service/exception/ErrorCode.java @@ -0,0 +1,17 @@ +package org.springside.examples.bootapi.service.exception; + +public enum ErrorCode { + + BAD_REQUEST(1000, 400), UNAUTHORIZED(1001, 401), FORBIDDEN(1002, 403), + + BOOK_STATUS_WRONG(1100, 400), BOOK_OWNERSHIP_WRONG(1101, 403), NO_TOKEN(1102, 401); + + public int code; + public int httpStatus; + + ErrorCode(int code, int httpStatus) { + this.code = code; + this.httpStatus = httpStatus; + } + +} diff --git a/examples/boot-api/src/main/resources/application-prod.properties b/examples/boot-api/src/main/resources/application-prod.properties index 9211cb6d..9e533586 100644 --- a/examples/boot-api/src/main/resources/application-prod.properties +++ b/examples/boot-api/src/main/resources/application-prod.properties @@ -8,6 +8,7 @@ spring.datasource.password= #disable automatic initialize for embedded H2 spring.jpa.hibernate.ddl-auto=none spring.datasource.initialize=false +flyway.enabled=true #connection pool settings spring.datasource.initial-size=10 diff --git a/examples/boot-api/src/main/resources/application.properties b/examples/boot-api/src/main/resources/application.properties index 5d89df35..fc2194f8 100644 --- a/examples/boot-api/src/main/resources/application.properties +++ b/examples/boot-api/src/main/resources/application.properties @@ -9,6 +9,8 @@ app.loginTimeoutSecs=600 spring.datasource.sqlScriptEncoding=UTF-8 spring.jpa.hibernate.ddl-auto=validate spring.jpa.showsql=false +spring.datasource.initialize=true +flyway.enabled=false # other settings spring.main.show-banner=false diff --git a/examples/boot-api/src/test/java/org/springside/examples/bootapi/functional/BookEndpointTest.java b/examples/boot-api/src/test/java/org/springside/examples/bootapi/functional/BookEndpointTest.java index ad77cf9b..68b1e409 100644 --- a/examples/boot-api/src/test/java/org/springside/examples/bootapi/functional/BookEndpointTest.java +++ b/examples/boot-api/src/test/java/org/springside/examples/bootapi/functional/BookEndpointTest.java @@ -32,9 +32,12 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.web.client.RestTemplate; import org.springside.examples.bootapi.BootApiApplication; +import org.springside.examples.bootapi.api.support.ServiceExceptionHandler.ErrorResult; import org.springside.examples.bootapi.domain.Book; import org.springside.examples.bootapi.dto.BookDto; import org.springside.examples.bootapi.repository.BookDao; +import org.springside.examples.bootapi.service.exception.ErrorCode; +import org.springside.modules.mapper.JsonMapper; import org.springside.modules.test.data.RandomData; import com.google.common.collect.Maps; @@ -53,6 +56,8 @@ public class BookEndpointTest { private BookDao bookDao; private RestTemplate restTemplate; + private JsonMapper jsonMapper = new JsonMapper(); + private String resourceUrl; private String loginUrl; private String logoutUrl; @@ -101,6 +106,8 @@ public class BookEndpointTest { // 未设置token ResponseEntity response = restTemplate.getForEntity(resourceUrl + "/{id}/request", String.class, 1L); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + ErrorResult errorResult = jsonMapper.fromJson(response.getBody(), ErrorResult.class); + assertThat(errorResult.code).isEqualTo(ErrorCode.NO_TOKEN.code); Book book = bookDao.findOne(1L); assertThat(book.borrower).isNull(); @@ -108,6 +115,8 @@ public class BookEndpointTest { // 设置错误token response = restTemplate.getForEntity(resourceUrl + "/{id}/request?token={token}", String.class, 1L, "abc"); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + errorResult = jsonMapper.fromJson(response.getBody(), ErrorResult.class); + assertThat(errorResult.code).isEqualTo(ErrorCode.UNAUTHORIZED.code); book = bookDao.findOne(1L); assertThat(book.borrower).isNull(); @@ -116,7 +125,9 @@ public class BookEndpointTest { String token = login("calvin.xiao@vipshop.com"); response = restTemplate.getForEntity(resourceUrl + "/{id}/request?token={token}", String.class, 1L, token); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + errorResult = jsonMapper.fromJson(response.getBody(), ErrorResult.class); + assertThat(errorResult.code).isEqualTo(ErrorCode.BOOK_OWNERSHIP_WRONG.code); book = bookDao.findOne(1L); assertThat(book.borrower).isNull(); @@ -131,6 +142,8 @@ public class BookEndpointTest { response = restTemplate.getForEntity(resourceUrl + "/{id}/request?token={token}", String.class, 3L, token); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + errorResult = jsonMapper.fromJson(response.getBody(), ErrorResult.class); + assertThat(errorResult.code).isEqualTo(ErrorCode.BOOK_STATUS_WRONG.code); // 回退操作 response = restTemplate.getForEntity(resourceUrl + "/{id}/cancel?token={token}", String.class, 3L, token); @@ -145,8 +158,8 @@ public class BookEndpointTest { // 发起请求 String token = login("david02.wang@vipshop.com"); - ResponseEntity response = restTemplate.getForEntity(resourceUrl + "/{id}/request?token={token}", String.class, - 1L, token); + ResponseEntity response = restTemplate.getForEntity(resourceUrl + "/{id}/request?token={token}", + String.class, 1L, token); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); logout(token); @@ -167,10 +180,10 @@ public class BookEndpointTest { map.put("email", user); map.put("password", "springside"); - ResponseEntity response = restTemplate.getForEntity(loginUrl + "?email={email}&password={password}", - String.class, map); + ResponseEntity response = restTemplate.getForEntity(loginUrl + "?email={email}&password={password}", + Map.class, map); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - return response.getBody(); + return (String) response.getBody().get("token"); } public void logout(String token) { diff --git a/modules/core/pom.xml b/modules/core/pom.xml index a5099530..ce476ede 100644 --- a/modules/core/pom.xml +++ b/modules/core/pom.xml @@ -34,24 +34,12 @@ true - - commons-io - commons-io - ${commons-io.version} - true - com.fasterxml.jackson.core jackson-databind ${jackson.version} true - - com.fasterxml.jackson.module - jackson-module-jaxb-annotations - ${jackson.version} - true - ma.glasnost.orika orika-core diff --git a/modules/core/src/main/java/org/springside/modules/mapper/JsonMapper.java b/modules/core/src/main/java/org/springside/modules/mapper/JsonMapper.java index b962588e..dde74ea5 100644 --- a/modules/core/src/main/java/org/springside/modules/mapper/JsonMapper.java +++ b/modules/core/src/main/java/org/springside/modules/mapper/JsonMapper.java @@ -20,7 +20,6 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.util.JSONPObject; -import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule; /** * 简单封装Jackson,实现JSON String<->Java Object的Mapper. @@ -64,9 +63,7 @@ public class JsonMapper { } /** - * Object可以是POJO,也可以是Collection或数组。 - * 如果对象为Null, 返回"null". - * 如果集合为空集合, 返回"[]". + * Object可以是POJO,也可以是Collection或数组。 如果对象为Null, 返回"null". 如果集合为空集合, 返回"[]". */ public String toJson(Object object) { @@ -81,8 +78,7 @@ public class JsonMapper { /** * 反序列化POJO或简单Collection如List. * - * 如果JSON字符串为Null或"null"字符串, 返回Null. - * 如果JSON字符串为"[]", 返回空集合. + * 如果JSON字符串为Null或"null"字符串, 返回Null. 如果JSON字符串为"[]", 返回空集合. * * 如需反序列化复杂Collection如List, 请使用fromJson(String, JavaType) * @@ -154,24 +150,13 @@ public class JsonMapper { } /** - * 設定是否使用Enum的toString函數來讀寫Enum, - * 為False時時使用Enum的name()函數來讀寫Enum, 默認為False. - * 注意本函數一定要在Mapper創建後, 所有的讀寫動作之前調用. + * 設定是否使用Enum的toString函數來讀寫Enum, 為False時時使用Enum的name()函數來讀寫Enum, 默認為False. 注意本函數一定要在Mapper創建後, 所有的讀寫動作之前調用. */ public void enableEnumUseToString() { mapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); mapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING); } - /** - * 支持使用Jaxb的Annotation,使得POJO上的annotation不用与Jackson耦合。 - * 默认会先查找jaxb的annotation,如果找不到再找jackson的。 - */ - public void enableJaxbAnnotation() { - JaxbAnnotationModule module = new JaxbAnnotationModule(); - mapper.registerModule(module); - } - /** * 取出Mapper做进一步的设置或使用其他序列化API. */