注入屬性與元件


在〈重構控制器〉中,使用了 Spring MVC 很小的功能集合,讓 gossip 專案就執行起來了,就套用一個框架來說,Spring MVC 可以提供這樣的小功能集合,是很好的一個特性,這表示開發者可以在既有的知識背景下,逐步套用 Spring MVC,而不用在一開始就面對過高的學習、修改曲線,日後逐步掌握 Spring MVC 越多,再視需求重構,套用更多的功能特性。

你可能會說,這太浪費框架的功能了,就連 @Value 的值也是寫死的,至少該將相依注入功能加進去吧!其實已經在使用了喔!Spring 的控制器不是沒有繼承任何類別或實作任何介面嗎?那麼方法中的 HttpServletRequestHttpServletResponse 實例是怎麼來的?Spring MVC 會管理相關的 Servlet 物件,若發現控制器的方法上有對應的型態,在呼叫時就會自動注入。

可以在既有的知識背景下套用一個框架是好處,想套用更多的框架功能也得看需求,不必為了要套用框架而一直找出框架的功能來套用,你套用框架越多,你被框架約束的越多,面對過多的框架細節,在未來維護時也是種負擔。

回過頭來,@Value 的值來源,UserServiceAccountDAOJdbcImplMessageDAOJdbcImplDataSource 等元件的管理與注入,如何在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 方法移到了這邊,以便稍後自動注入 AccountDAOJdbcImplMessageDAOJdbcImpl,因為 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 的 DataAccessExceptionMessageDAOJdbcImpl 也可以做相同的標示,這部份就不列出了,可以參考範例專案中的設定。

接著可以來看看控制器的部份了,例如 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 找到以上的範例專案。