在〈重構控制器〉中,使用了 Spring MVC 很小的功能集合,讓 gossip 專案就執行起來了,就套用一個框架來說,Spring MVC 可以提供這樣的小功能集合,是很好的一個特性,這表示開發者可以在既有的知識背景下,逐步套用 Spring MVC,而不用在一開始就面對過高的學習、修改曲線,日後逐步掌握 Spring MVC 越多,再視需求重構,套用更多的功能特性。
你可能會說,這太浪費框架的功能了,就連 @Value
的值也是寫死的,至少該將相依注入功能加進去吧!其實已經在使用了喔!Spring 的控制器不是沒有繼承任何類別或實作任何介面嗎?那麼方法中的 HttpServletRequest
、HttpServletResponse
實例是怎麼來的?Spring MVC 會管理相關的 Servlet 物件,若發現控制器的方法上有對應的型態,在呼叫時就會自動注入。
可以在既有的知識背景下套用一個框架是好處,想套用更多的框架功能也得看需求,不必為了要套用框架而一直找出框架的功能來套用,你套用框架越多,你被框架約束的越多,面對過多的框架細節,在未來維護時也是種負擔。
回過頭來,@Value
的值來源,UserService
、AccountDAOJdbcImpl
、MessageDAOJdbcImpl
、DataSource
等元件的管理與注入,如何在Spring MVC 中實作呢?基本上與先前文件中談的方式相同,例如,可以在 WebConfig
中加入:
package cc.openhome.web;
...略
@Configuration
@EnableWebMvc
@PropertySource("classpath:path.properties")
@ComponentScan("cc.openhome.controller")
public class WebConfig implements WebMvcConfigurer {
...
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
path.properties 可以放在 WEB-INF/classes 下,由於是 Eclipse 專案,在專案中存放在 src 下就可以了:
path.url.member=/gossip/member
path.url.index=/gossip
path.view.register_success=/WEB-INF/jsp/register_success.jsp
path.view.register_form=/WEB-INF/jsp/register.jsp
path.view.verify=/WEB-INF/jsp/verify.jsp
path.view.forgot=/WEB-INF/jsp/forgot.jsp
path.view.reset_password_form=/WEB-INF/jsp/reset_password.jsp
path.view.reset_password_success=/WEB-INF/jsp/reset_success.jsp
path.view.index=/WEB-INF/jsp/index.jsp
path.view.user=/WEB-INF/jsp/user.jsp
path.view.member=/WEB-INF/jsp/member.jsp
這麼一來,稍後 @Value
就可以搭配 Place Holder 來取得對應的屬性值;在這之前,先來處理 DataSource
等元件的注入,首先定義 RootConfig
:
package cc.openhome.web;
...略
@Configuration
@PropertySource("classpath:mail.properties")
@ComponentScan("cc.openhome.model")
public class RootConfig {
@Bean
public DataSource dataSource() {
try {
Context initContext = new InitialContext();
Context envContext = (Context) initContext.lookup("java:/comp/env");
return (DataSource) envContext.lookup("jdbc/gossip");
} catch (NamingException e) {
throw new RuntimeException(e);
}
}
@Bean
public static PropertySourcesPlaceholderConfigurer
propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
原先在 GossipInitializer
中的 dataSource
方法移到了這邊,以便稍後自動注入 AccountDAOJdbcImpl
、MessageDAOJdbcImpl
,因為 RootConfig
不是個 ServletContextListener
,無法取得 ServletContext
初始參數,因此將 web.xml 中的郵件設定資訊,移至 mail.properties:
mail.user=yourname@gmail.com
mail.password=yourpassword
接著可以來標註 GmailService
:
package cc.openhome.model;
...略
import org.springframework.stereotype.Service;
@Service
public class GmailService implements EmailService {
private final Properties props = new Properties();
private final String mailUser;
private final String mailPassword;
public GmailService(
@Value("${mail.user}") String mailUser,
@Value("${mail.password}") String mailPassword) {
props.put("mail.smtp.host", "smtp.gmail.com");
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.port", 587);
this.mailUser = mailUser;
this.mailPassword = mailPassword;
}
...略
}
基本上在 GmailService
標註 @Component
也可以,@Component
是通用的註解,表示 Spring 管理的元件,而 @Service
涵義更為清楚,表示設計分層中的服務層元件;在上面也使用 @Value
搭配 Place Holder 取得了屬性值。
接著來標註 UserService
:
package cc.openhome.model;
...略
@Service
public class UserService {
private final AccountDAO acctDAO;
private final MessageDAO messageDAO;
@Autowired
public UserService(AccountDAO acctDAO, MessageDAO messageDAO) {
this.acctDAO = acctDAO;
this.messageDAO = messageDAO;
}
...略
}
然後是 AccountDAOJdbcImpl
:
package cc.openhome.model;
...略
import org.springframework.stereotype.Repository;
@Repository
public class AccountDAOJdbcImpl implements AccountDAO {
private DataSource dataSource;
@Autowired
public AccountDAOJdbcImpl(DataSource dataSource) {
this.dataSource = dataSource;
}
...略
}
基本上在 AccountDAOJdbcImpl
標註 @Component
也可以,然而 @Repository
涵義更為清楚,表示設計分層中的存儲層元件,若方法拋出 SQLException
,會轉為 Spring 的 DataAccessException
,MessageDAOJdbcImpl
也可以做相同的標示,這部份就不列出了,可以參考範例專案中的設定。
接著可以來看看控制器的部份了,例如 AccessController
:
package cc.openhome.controller;
...略
@Controller
public class AccessController {
@Value("${path.url.member}")
private String REDIRECT_MEMBER_PATH;
@Value("${path.url.index}")
private String REDIRECT_INDEX_PATH;
@Value("${path.view.index}")
private String INDEX_PATH;
@Autowired
private UserService userService;
@PostMapping("login")
public void login(
HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
Optional<String> optionalPasswd = userService.encryptedPassword(username, password);
try {
request.login(username, optionalPasswd.get());
request.getSession().setAttribute("login", username);
response.sendRedirect(REDIRECT_MEMBER_PATH);
} catch(NoSuchElementException | ServletException e) {
request.setAttribute("errors", Arrays.asList("登入失敗"));
List<Message> newest = userService.newestMessages(10);
request.setAttribute("newest", newest);
request.getRequestDispatcher(INDEX_PATH)
.forward(request, response);
}
}
@GetMapping("logout")
public void logout(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.logout();
response.sendRedirect(REDIRECT_INDEX_PATH);
}
}
主要的修改為 @Value
改從 Place Holder 獲得值,而 UserService
改為自動綁定,因此原先控制器中從 ServletContext
取得 UserService
的程式碼,也就是底下這行都可以刪掉了:
UserService userService = (UserService) request.getServletContext().getAttribute("userService");
其他的控制器都可以做相同修改,AccountController
中需要注入 EmailService
實例,別忘了加進去,並刪掉從 ServletContext
取得 MailService
的程式碼。
現在 GossipInitializer
已經用不到了,而 web.xml 中有關郵件設定的資訊也可以刪除,重新部署應用程式,一切就可以正常運作。
你可以在 gossip 找到以上的範例專案。