基于springboot搭建的web系統(tǒng)架構的方法步驟
從接觸springboot開始,便深深的被它的簡潔性深深的折服了,精簡的配置,方便的集成,使我再也不想用傳統(tǒng)的ssm框架來搭建項目,一大堆的配置文件,維護起來很不方便,集成的時候也要費力不少。從第一次使用springboot開始,一個簡單的main方法,甚至一個配置文件也不需要(當然我是指的沒有任何數(shù)據(jù)交互,沒有任何組件集成的情況),就可以把一個web項目啟動起來,下面總結一下自從使用springboot依賴,慢慢完善的自己的一個web系統(tǒng)的架構,肯定不是最好的,但平時自己用著很舒服。
1. 配置信息放到數(shù)據(jù)庫里邊
個人比較不喜歡配置文件,因此有一個原則,配置文件能不用就不用,配置信息能少些就少些,配置內容能用代碼寫堅決不用xml,因此我第一個想到的就是,能不能把springboot的配置信息寫到數(shù)據(jù)庫里,在springboot啟動的時候自動去加載,而在application.properties里邊只寫一個數(shù)據(jù)源。最終找到了方法:
注意圖中箭頭指向的兩行,構造了一個properties對象,然后將這個對象放到了springboot的啟動對象application中,properties是一個類似map的key-value容器,springboot可以將其中的東西當做成原來application.properties中的內容一樣,因此在properties對象的內容也就相當于寫在了application.properties文件中。知道了這個之后就簡單了,我們將原本需要寫在application.properties中的所有配置信息寫在數(shù)據(jù)庫中,在springboot啟動的時候從數(shù)據(jù)庫中讀取出來放到properties對象中,然后再將這個對象set到application中即可。上圖中PropertyConfig.loadProperties()方法就是進行了這樣的操作,代碼如下:
PropertyConfig.java
public class PropertyConfig { /** * 生成Properties對象 */ public static Properties loadProperties() { Properties properties = new Properties(); loadPropertiesFromDb(properties); return properties; } /** * 從數(shù)據(jù)庫中加載配置信息 */ private static void loadPropertiesFromDb(Properties properties) { InputStream in = PropertyConfig.class.getClassLoader().getResourceAsStream("application.properties"); try { properties.load(in); } catch (Exception e) { e.printStackTrace(); } String profile = properties.getProperty("profile"); String driverClassName = properties.getProperty("spring.datasource.driver-class-name"); String url = properties.getProperty("spring.datasource.url"); String userName = properties.getProperty("spring.datasource.username"); String password = properties.getProperty("spring.datasource.password"); Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; try { Class.forName(driverClassName); String tableName = "t_config_dev"; if ("pro".equals(profile)) { tableName = "t_config_pro"; } String sql = "select * from " + tableName; conn = DriverManager.getConnection(url, userName, password); pstmt = conn.prepareStatement(sql); rs = pstmt.executeQuery(); while (rs.next()) { String key = rs.getString("key"); String value = rs.getString("value"); properties.put(key, value); } } catch (Exception e) { e.printStackTrace(); } finally { try { if (conn != null) { conn.close(); } if (pstmt != null) { pstmt.close(); } if (rs != null) { rs.close(); } } catch (Exception e) { e.printStackTrace(); } } } }
代碼中,首先使用古老的jdbc技術,讀取數(shù)據(jù)庫t_config表,將表中的key-value加載到properties中,代碼中profile是為了區(qū)分開發(fā)環(huán)境和生產環(huán)境,以便于確定從那張表中加載配置文件,數(shù)據(jù)庫中的配置信息如下:
這樣以后,application.properties中就不用再寫很多的配置信息,而且,如果將這些配置信息放到數(shù)據(jù)庫中之后,如果起多個應用可是公用這一張表,這樣也可以做到配置信息的公用的效果,這樣修改以后,配置文件中就只有數(shù)據(jù)源的信息了:
profile代表使用哪個環(huán)境,代碼中可以根據(jù)這個信息來從開發(fā)表中加載配置信息還是從生產表中加載配置信息。
2. 統(tǒng)一返回結果
一般web項目中,大多數(shù)都是接口,以返回json數(shù)據(jù)為主,因此統(tǒng)一一個返回格式很必要。在本示例中,建了一個BaseController,所有的Controller都需要繼承這個類,在這個BaseController中定義了成功的返回和失敗的返回,在其他業(yè)務的Controller中,返回的時候,只需要return super.success(xxx)或者return super.fail(xxx, xxx)即可,例:
說到這里,返回給前臺的狀態(tài)碼,建議也是封裝成一個枚舉類型,不建議直接返回200、400之類的,不方便維護也不方便查詢。那么BaseController里做了什么呢?如下:
定義一個ResultInfo類,該類只有兩個屬性,一個是Integer類型的狀態(tài)碼,一個是泛型,用于成功時返回給前臺的數(shù)據(jù),和失敗時返回給前臺的提示信息。
3. 統(tǒng)一異常捕獲
在上一步中的Controller代碼中看到拋出了一個自定義的異常,在Controller中,屬于最外層的代碼了,這個時候如果有異常就不能直接拋出去了,這里再拋出去就沒有人處理了,服務器只能返回給前臺一個錯誤,用戶體驗不好。因此,建議所有的Controller代碼都用try-catch包裹,捕獲到異常后統(tǒng)一進行處理,然后再給前臺一個合理的提示信息。在上一步中拋出了一個自定義異常:
throw new MyException(ResultEnum.DELETE_ERROR.getCode(), "刪除員工出錯,請聯(lián)系網(wǎng)站管理人員。", e);
該自定義異常有三個屬性,分別是異常狀態(tài)碼,異常提示信息,以及捕獲到的異常對象,接下來定義一個全局的異常捕獲,統(tǒng)一對異常進行處理:
@Slf4j @ResponseBody @ControllerAdvice public class GlobalExceptionHandle { /** * 處理捕獲的異常 */ @ExceptionHandler(value = Exception.class) public Object handleException(Exception e, HttpServletRequest request, HttpServletResponse resp) throws IOException { log.error(AppConst.ERROR_LOG_PREFIX + "請求地址:" + request.getRequestURL().toString()); log.error(AppConst.ERROR_LOG_PREFIX + "請求方法:" + request.getMethod()); log.error(AppConst.ERROR_LOG_PREFIX + "請求者IP:" + request.getRemoteAddr()); log.error(AppConst.ERROR_LOG_PREFIX + "請求參數(shù):" + ParametersUtils.getParameters(request)); if (e instanceof MyException) { MyException myException = (MyException) e; log.error(AppConst.ERROR_LOG_PREFIX + myException.getMsg(), myException.getE()); if (myException.getCode().equals(ResultEnum.SEARCH_PAGE_ERROR.getCode())) { JSONObject result = new JSONObject(); result.put("code", myException.getCode()); result.put("msg", myException.getMsg()); return result; } else if (myException.getCode().equals(ResultEnum.ERROR_PAGE.getCode())) { resp.sendRedirect("/err"); return ""; } else { return new ResultInfo<>(myException.getCode(), myException.getMsg()); } } else if (e instanceof UnauthorizedException) { resp.sendRedirect("/noauth"); return ""; } else { log.error(AppConst.ERROR_LOG_PREFIX + "錯誤信息:", e); } resp.sendRedirect("/err"); return ""; } }
統(tǒng)一捕獲異常之后,可以進行相應的處理,我這里沒有進行特殊的處理,只是進行了一下區(qū)分,獲取數(shù)據(jù)的接口拋出的異常,前臺肯定是使用的ajax請求,因此返回前臺一個json格式的信息,提示出錯誤內容。如果是跳轉頁面拋出的異常,類似404之類的,直接跳轉到自定義的404頁面。補充一點,springboot項目默認是有/error路由的,返回的就是error頁面,所以,如果你在你的項目中定義一個error.html的頁面,如果報404錯誤,會自動跳轉到該頁面。
補充,統(tǒng)一異常處理類中使用了一個注解@Slf4j,該注解是lombok包中的,項目中加入了該依賴后,再也不用寫繁瑣的get、set等代碼,當然類似的像上邊的聲明log對象的代碼也不用寫了:
4. 日志配置文件區(qū)分環(huán)境
本示例使用的是logback日志框架。需要在resources目錄中添加logback.xml配置文件,這是一個比較頭疼的地方,我本來想一個配置文件也沒有的,奈何我也不知道怎么將這個日志的配置文件放到數(shù)據(jù)庫中,所以暫時先這么著了,好在幾乎沒有需要改動它的時候。
我在項目中添加了兩個日志的配置文件,分別是logback-dev.xml和logback-pro.xml可以根據(jù)不同的環(huán)境決定使用哪個配置文件,在數(shù)據(jù)庫配置表中(相當于寫在了application.properties中)添加一條配置logging.config=classpath:logback-dev.xml來區(qū)分使用哪個文件作為日志的配置文件,配置文件內容如下:
logback.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration> <property name="LOG_HOME" value="/Users/oven/log/demo"/> <!-- INFO日志定義 --> <appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender"> <File>${LOG_HOME}/demo.info.log</File> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_HOME}/demo.info.%d{yyyy-MM-dd}.log</FileNamePattern> <maxHistory>180</maxHistory> </rollingPolicy> <encoder> <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</Pattern> <charset>UTF-8</charset> </encoder> </appender> <!-- ERROR日志定義 --> <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender"> <File>${LOG_HOME}/demo.error.log</File> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_HOME}/demo.error.%d{yyyy-MM-dd}.log</FileNamePattern> <maxHistory>180</maxHistory> </rollingPolicy> <encoder> <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</Pattern> <charset>UTF-8</charset> </encoder> </appender> <!-- DEBUG日志定義 --> <appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender"> <File>${LOG_HOME}/demo.debug.log</File> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_HOME}/demo.debug.%d{yyyy-MM-dd}.log</FileNamePattern> <maxHistory>180</maxHistory> </rollingPolicy> <encoder> <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</Pattern> <charset>UTF-8</charset> </encoder> </appender> <!-- 定義控制臺日志信息 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="STDOUT"/> </root> <logger name="com.oven.controller" level="ERROR"> <appender-ref ref="ERROR"/> </logger> <logger name="com.oven.exception" level="ERROR"> <appender-ref ref="ERROR"/> </logger> <logger name="com.oven.mapper" level="DEBUG"> <appender-ref ref="DEBUG"/> </logger> <logger name="com.oven.aop" level="INFO"> <appender-ref ref="INFO"/> </logger> </configuration>
在配置文件中,定義了三個級別的日志,info、debug和error分別輸出到三個文件中,便于查看。在生成日志文件的時候,進行了按照日志進行拆分的配置,每一個級別的日志每一天都會重新生成一個,根據(jù)日期進行命名,超過180天的日志將自動會刪除。當然你還可以按照日志大小進行拆分,我這里沒有進行這項的配置。
5. 全局接口請求記錄
進行全局的接口請求記錄,可以記錄接口的別調用情況,然后進行一些統(tǒng)計和分析,在本示例中,只是將全局的接口調用情況記錄到了info日志中,沒有進行相應的分析操作:
@Slf4j @Aspect @Component public class WebLogAspect { @Pointcut("execution(public * com.oven.controller.*.*(..))") public void webLog() { } @Before("webLog()") public void doBefore() { // 獲取請求 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); @SuppressWarnings("ConstantConditions") HttpServletRequest request = attributes.getRequest(); // 記錄請求內容 log.info(AppConst.INFO_LOG_PREFIX + "請求地址:" + request.getRequestURL().toString()); log.info(AppConst.INFO_LOG_PREFIX + "請求方法:" + request.getMethod()); log.info(AppConst.INFO_LOG_PREFIX + "請求者IP:" + request.getRemoteAddr()); log.info(AppConst.INFO_LOG_PREFIX + "請求參數(shù):" + ParametersUtils.getParameters(request)); } @AfterReturning(returning = "ret", pointcut = "webLog()") public void doAfterReturning(Object ret) { // 請求返回的內容 if (ret instanceof ResultInfo) { log.info(AppConst.INFO_LOG_PREFIX + "返回結果:" + ((ResultInfo) ret).getCode().toString()); } } }
6. 集成shiro實現(xiàn)權限校驗
集成shirl,輕松的實現(xiàn)了權限的管理,如果對shiro不熟悉朋友,還需要先把shiro入門一下才好,shiro的集成一般都需要自定義一個realm,來進行身份認證和授權,因此先來一個自定義realm:
MyShiroRealm.java
public class MyShiroRealm extends AuthorizingRealm { @Resource private MenuService menuService; @Resource private UserService userService; /** * 授權 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); User user = (User) principals.getPrimaryPrincipal(); List<String> permissions = menuService.getAllMenuCodeByUserId(user.getId()); authorizationInfo.addStringPermissions(permissions); return authorizationInfo; } /** * 身份認證 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String userName = String.valueOf(token.getUsername()); // 從數(shù)據(jù)庫獲取對應用戶名的用戶 User user = userService.getByUserName(userName); // 賬號不存在 if (user == null) { throw new UnknownAccountException(ResultEnum.NO_THIS_USER.getValue()); } Md5Hash md5 = new Md5Hash(token.getPassword(), AppConst.MD5_SALT, 2); // 密碼錯誤 if (!md5.toString().equals(user.getPassword())) { throw new IncorrectCredentialsException(ResultEnum.PASSWORD_WRONG.getValue()); } // 賬號鎖定 if (user.getStatus().equals(1)) { throw new LockedAccountException(ResultEnum.USER_DISABLE.getValue()); } ByteSource salt = ByteSource.Util.bytes(AppConst.MD5_SALT); return new SimpleAuthenticationInfo(user, user.getPassword(), salt, getName()); } }
自定義完realm后需要一個配置文件但自定義的realm配置到shiro里:
ShiroConfig.java
@Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/font/**", "anon"); filterChainDefinitionMap.put("/js/**", "anon"); filterChainDefinitionMap.put("/*.js", "anon"); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/doLogin", "anon"); filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setLoginUrl("/login"); shiroFilterFactoryBean.setSuccessUrl("/"); shiroFilterFactoryBean.setUnauthorizedUrl("/noauth"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * 憑證匹配器 */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("MD5"); hashedCredentialsMatcher.setHashIterations(2); return hashedCredentialsMatcher; } @Bean public MyShiroRealm myShiroRealm() { MyShiroRealm myShiroRealm = new MyShiroRealm(); myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myShiroRealm; } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); return securityManager; } /** * 開啟shiro aop注解 */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } @Bean(name = "simpleMappingExceptionResolver") public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() { SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver(); Properties mappings = new Properties(); mappings.setProperty("DatabaseException", "databaseError"); mappings.setProperty("UnauthorizedException", "403"); r.setExceptionMappings(mappings); r.setDefaultErrorView("error"); r.setExceptionAttribute("ex"); return r; } }
身份認證如果簡單的理解的話,你可以理解為登錄的過程。授權就是授予你權利,代表你在這個系統(tǒng)中有權限做什么動作,具體shiro的內容小伙伴們自行去學習吧。
7. 登錄校驗,安全攔截
在集成了shiro之后,登錄操作就需要使用到自定義的realm了,具體的登錄代碼如下:
/** * 登錄操作 * * @param userName 用戶名 * @param pwd 密碼 */ @RequestMapping("/doLogin") @ResponseBody public Object doLogin(String userName, String pwd, HttpServletRequest req) throws MyException { try { Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(userName, pwd); subject.login(token); User userInDb = userService.getByUserName(userName); // 登錄成功后放入application,防止同一個賬戶多人登錄 ServletContext application = req.getServletContext(); @SuppressWarnings("unchecked") Map<String, String> loginedMap = (Map<String, String>) application.getAttribute(AppConst.LOGINEDUSERS); if (loginedMap == null) { loginedMap = new HashMap<>(); application.setAttribute(AppConst.LOGINEDUSERS, loginedMap); } loginedMap.put(userInDb.getUserName(), req.getSession().getId()); // 登錄成功后放入session中 req.getSession().setAttribute(AppConst.CURRENT_USER, userInDb); logService.addLog("登錄系統(tǒng)!", "成功!", userInDb.getId(), userInDb.getNickName(), IPUtils.getClientIPAddr(req)); return super.success("登錄成功!"); } catch (Exception e) { User userInDb = userService.getByUserName(userName); if (e instanceof UnknownAccountException) { logService.addLog("登錄系統(tǒng)!", "失敗[" + ResultEnum.NO_THIS_USER.getValue() + "]", 0, "", IPUtils.getClientIPAddr(req)); return super.fail(ResultEnum.NO_THIS_USER.getCode(), ResultEnum.NO_THIS_USER.getValue()); } else if (e instanceof IncorrectCredentialsException) { logService.addLog("登錄系統(tǒng)!", "失敗[" + ResultEnum.PASSWORD_WRONG.getValue() + "]", userInDb.getId(), userInDb.getNickName(), IPUtils.getClientIPAddr(req)); return super.fail(ResultEnum.PASSWORD_WRONG.getCode(), ResultEnum.PASSWORD_WRONG.getValue()); } else if (e instanceof LockedAccountException) { logService.addLog("登錄系統(tǒng)!", "失敗[" + ResultEnum.USER_DISABLE.getValue() + "]", userInDb.getId(), userInDb.getNickName(), IPUtils.getClientIPAddr(req)); return super.fail(ResultEnum.USER_DISABLE.getCode(), ResultEnum.USER_DISABLE.getValue()); } else { throw new MyException(ResultEnum.UNKNOW_ERROR.getCode(), "登錄操作出錯,請聯(lián)系網(wǎng)站管理人員。", e); } } }
身份認證的操作交給了shiro,利用用戶名和密碼構造一個身份的令牌,調用shiro的login方法,這個時候就會進入自定義reaml的身份認證方法中,也就是上一步中的doGetAuthenticationInfo方法,具體的認證操作看上一步的代碼,無非就是賬號密碼的校驗等。身份認證的時候,通過拋出異常的方式給登錄操作返回信息,從而在登錄方法中判斷身份認證失敗后的信息,從而返回給前臺進行提示。
在身份認證通過后,拿到當前登錄用戶的信息,首先放到session中,便于后續(xù)的使用。其次在放到application對象中,防止同一個賬號的多次登錄。
有了身份任何和授權自然就少不了安全校驗,在本示例中使用了一個攔截器來實現(xiàn)安全校驗的工作:
SecurityInterceptor.java
@Component public class SecurityInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { resp.setContentType("text/plain;charset=UTF-8"); String servletPath = req.getServletPath(); // 放行的請求 if (servletPath.startsWith("/login") || servletPath.startsWith("/doLogin") || servletPath.equals("/err")) { return true; } if (servletPath.startsWith("/error")) { resp.sendRedirect("/err"); return true; } // 獲取當前登錄用戶 User user = (User) req.getSession().getAttribute(AppConst.CURRENT_USER); // 沒有登錄狀態(tài)下訪問系統(tǒng)主頁面,都跳轉到登錄頁,不提示任何信息 if (servletPath.startsWith("/")) { if (user == null) { resp.sendRedirect(getDomain(req) + "/login"); return false; } } // 未登錄或會話超時 if (user == null) { String requestType = req.getHeader("X-Requested-With"); if ("XMLHttpRequest".equals(requestType)) { // ajax請求 ResultInfo<Object> resultInfo = new ResultInfo<>(); resultInfo.setCode(ResultEnum.SESSION_TIMEOUT.getCode()); resultInfo.setData(ResultEnum.SESSION_TIMEOUT.getValue()); resp.getWriter().write(JSONObject.toJSONString(resultInfo)); return false; } String param = URLEncoder.encode(ResultEnum.SESSION_TIMEOUT.getValue(), "UTF-8"); resp.sendRedirect(getDomain(req) + "/login?errorMsg=" + param); return false; } // 檢查是否被其他人擠出去 ServletContext application = req.getServletContext(); @SuppressWarnings("unchecked") Map<String, String> loginedMap = (Map<String, String>) application.getAttribute(AppConst.LOGINEDUSERS); if (loginedMap == null) { // 可能是掉線了 String requestType = req.getHeader("X-Requested-With"); if ("XMLHttpRequest".equals(requestType)) { // ajax請求 ResultInfo<Object> resultInfo = new ResultInfo<>(); resultInfo.setCode(ResultEnum.LOSE_LOGIN.getCode()); resultInfo.setData(ResultEnum.LOSE_LOGIN.getValue()); resp.getWriter().write(JSONObject.toJSONString(resultInfo)); return false; } String param = URLEncoder.encode(ResultEnum.LOSE_LOGIN.getValue(), "UTF-8"); resp.sendRedirect(getDomain(req) + "/login?errorMsg=" + param); return false; } String loginedUserSessionId = loginedMap.get(user.getUserName()); String mySessionId = req.getSession().getId(); if (!mySessionId.equals(loginedUserSessionId)) { String requestType = req.getHeader("X-Requested-With"); if ("XMLHttpRequest".equals(requestType)) { // ajax請求 ResultInfo<Object> resultInfo = new ResultInfo<>(); resultInfo.setCode(ResultEnum.OTHER_LOGINED.getCode()); resultInfo.setData(ResultEnum.OTHER_LOGINED.getValue()); resp.getWriter().write(JSONObject.toJSONString(resultInfo)); return false; } String param = URLEncoder.encode(ResultEnum.OTHER_LOGINED.getValue(), "UTF-8"); resp.sendRedirect(getDomain(req) + "/login?errorMsg=" + param); return false; } return true; } /** * 獲得域名 */ private String getDomain(HttpServletRequest request) { String path = request.getContextPath(); return request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path; } }
在攔截器中,首先對一些不需要校驗的請求進行放行,例如登錄動作、登錄頁面請求以及錯誤頁面等。然后獲取當前登錄的用戶,如果沒有登錄則自動跳轉到登錄頁面。在返回前臺的時候,判斷請求屬于同步請求還是異步請求,如果是同步請求,直接進行頁面的跳轉,跳轉到登錄頁面。如果是異步請求,則返回前臺一個json數(shù)據(jù),提示前臺登錄信息失效。這里補充一點,前臺可以使用ajaxhook進行異步請求的捕獲,相當于一個前端的全局攔截器,攔截所有的異步請求,可以監(jiān)視所有異步請求的返回結果,如果返回的是登錄失效,則進行跳轉到登錄頁面的操作。具體ajaxhook的使用方法請自行學習,本示例中暫時沒有使用。
下面是判斷同一個賬號有沒有多次登錄,具體方法就是使用當前的sessionId,將當前登錄用戶和請求sissionId作為一個key-value放到了application中,如果該用戶的sessionId發(fā)生了變化,說明又有一個人登錄了該賬號,然后就進行相應的提示操作。
8. 配置虛擬路徑
web項目中免不了并上傳的操作,圖片或者文件,如果上傳的是圖片,一般還要進行回顯的操作,我們不想將上傳的文件直接存放在項目的目錄中,而是放在一個自定義的目錄,同時項目還可以訪問:
這樣在進行上傳操作的時候,就可以將上傳的文件放到項目以外的目錄中,然后外部訪問的時候,通過虛擬路徑進行映射訪問。
9. 集成redis緩存
springboot的強悍就是集成一個東西太方便了,如果你不想做任何配置,只需要加入redis的依賴,然后在配置文件(本示例中配置是在數(shù)據(jù)庫中)中添加redis的鏈接信息,就可以在項目中使用redis了。
本示例中使用redis做緩存,首先寫了一個緩存的類,代碼有些長不做展示。然后在service層進行緩存的操作:
代碼中使用了double check的騷操作,防止高并發(fā)下緩存失效的問題(雖然我的示例不可能有高并發(fā),哈哈)。另外就是緩存更新的問題,網(wǎng)上說的有很多,先更新數(shù)據(jù)再更新緩存,先更新緩存再更新數(shù)據(jù)庫等等,具體要看你是做什么,本示例中沒有什么需要特殊注意的地方,因此就先更新數(shù)據(jù)庫,然后再移除緩存:
10. 項目代碼和依賴以及靜態(tài)資源分別打包
之前遇到一個問題,springboot打包之后是一個jar文件,如果將所有依賴也打到這個jar包中的話,那么這個jar包動輒幾十兆,來回傳輸不說,如果想改動其中的一個配置內容,還異常的繁瑣,因此,將項目代碼,就是自己寫的代碼打成一個jar包(一般只有幾百k),然后將所有的依賴打包到一個lib目錄,然后再將所有的配置信息以及靜態(tài)文件打包到resources目錄,這樣,靜態(tài)文件可以直接進行修改,瀏覽器清理緩存刷新即可出現(xiàn)改動效果,而且打包出來的項目代碼也小了很多,至于依賴,一般都是不變的,所以也沒必要每次都打包它。具體操作就是在pom.xml中增加一個插件即可,代碼如下:
代碼太長,不做展示
11. 項目啟動
到現(xiàn)在都沒有貼一個項目的目錄結構,先來一張。目錄中項目跟目錄下的demo.sh就是啟動腳本,當時從網(wǎng)上抄襲改裝過來的,源代碼出自那位大師之手我就不知道了,先行謝過。在部署到服務器的時候,如果服務器上安裝好了jdk、maven、git,每次修改完代碼,直接git pull下來,然后mvn package打包,然后直接./demo.sh start就可以啟動項目,方便快速。慢著,忘記了,如果你提交到github中的application.properties中的數(shù)據(jù)源配置信息是開發(fā)環(huán)境的話,那么你在打包之后,target/resources中的application.properties中的數(shù)據(jù)源需要改成開發(fā)環(huán)境才可以啟動。當然如果你嫌麻煩,可以直接將開發(fā)環(huán)境的數(shù)據(jù)源配置push到github中,安不安全就要你自己考慮了。
12. 總結
示例中可能還有一些細節(jié)沒有說到,總之這個項目是慢慢的添磚添瓦弄出來的,自己在寫很多其他的項目的時候,都是以此項目為模板進行改造出來的,個人感覺很實用很方便,用著也很舒服。github地址:https://github.com/503612012/demo歡迎收藏。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
spring-boot整合ehcache實現(xiàn)緩存機制的方法
spring-boot是一個快速的集成框架,其設計目的是用來簡化新Spring應用的初始搭建以及開發(fā)過程。這篇文章主要介紹了spring-boot整合ehcache實現(xiàn)緩存機制,需要的朋友可以參考下2018-01-01SpringBoot如何使用自定義注解實現(xiàn)接口限流
這篇文章主要介紹了SpringBoot如何使用自定義注解實現(xiàn)接口限流,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-06-06