基于springboot搭建的web系統(tǒng)架構(gòu)的方法步驟
從接觸springboot開始,便深深的被它的簡潔性深深的折服了,精簡的配置,方便的集成,使我再也不想用傳統(tǒng)的ssm框架來搭建項(xiàng)目,一大堆的配置文件,維護(hù)起來很不方便,集成的時候也要費(fèi)力不少。從第一次使用springboot開始,一個簡單的main方法,甚至一個配置文件也不需要(當(dāng)然我是指的沒有任何數(shù)據(jù)交互,沒有任何組件集成的情況),就可以把一個web項(xiàng)目啟動起來,下面總結(jié)一下自從使用springboot依賴,慢慢完善的自己的一個web系統(tǒng)的架構(gòu),肯定不是最好的,但平時自己用著很舒服。
1. 配置信息放到數(shù)據(jù)庫里邊
個人比較不喜歡配置文件,因此有一個原則,配置文件能不用就不用,配置信息能少些就少些,配置內(nèi)容能用代碼寫堅(jiān)決不用xml,因此我第一個想到的就是,能不能把springboot的配置信息寫到數(shù)據(jù)庫里,在springboot啟動的時候自動去加載,而在application.properties里邊只寫一個數(shù)據(jù)源。最終找到了方法:

注意圖中箭頭指向的兩行,構(gòu)造了一個properties對象,然后將這個對象放到了springboot的啟動對象application中,properties是一個類似map的key-value容器,springboot可以將其中的東西當(dāng)做成原來application.properties中的內(nèi)容一樣,因此在properties對象的內(nèi)容也就相當(dāng)于寫在了application.properties文件中。知道了這個之后就簡單了,我們將原本需要寫在application.properties中的所有配置信息寫在數(shù)據(jù)庫中,在springboot啟動的時候從數(shù)據(jù)庫中讀取出來放到properties對象中,然后再將這個對象set到application中即可。上圖中PropertyConfig.loadProperties()方法就是進(jìn)行了這樣的操作,代碼如下:
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ù),讀取數(shù)據(jù)庫t_config表,將表中的key-value加載到properties中,代碼中profile是為了區(qū)分開發(fā)環(huán)境和生產(chǎn)環(huán)境,以便于確定從那張表中加載配置文件,數(shù)據(jù)庫中的配置信息如下:

這樣以后,application.properties中就不用再寫很多的配置信息,而且,如果將這些配置信息放到數(shù)據(jù)庫中之后,如果起多個應(yīng)用可是公用這一張表,這樣也可以做到配置信息的公用的效果,這樣修改以后,配置文件中就只有數(shù)據(jù)源的信息了:

profile代表使用哪個環(huán)境,代碼中可以根據(jù)這個信息來從開發(fā)表中加載配置信息還是從生產(chǎn)表中加載配置信息。
2. 統(tǒng)一返回結(jié)果
一般web項(xiàng)目中,大多數(shù)都是接口,以返回json數(shù)據(jù)為主,因此統(tǒng)一一個返回格式很必要。在本示例中,建了一個BaseController,所有的Controller都需要繼承這個類,在這個BaseController中定義了成功的返回和失敗的返回,在其他業(yè)務(wù)的Controller中,返回的時候,只需要return super.success(xxx)或者return super.fail(xxx, xxx)即可,例:

說到這里,返回給前臺的狀態(tài)碼,建議也是封裝成一個枚舉類型,不建議直接返回200、400之類的,不方便維護(hù)也不方便查詢。那么BaseController里做了什么呢?如下:

定義一個ResultInfo類,該類只有兩個屬性,一個是Integer類型的狀態(tài)碼,一個是泛型,用于成功時返回給前臺的數(shù)據(jù),和失敗時返回給前臺的提示信息。
3. 統(tǒng)一異常捕獲
在上一步中的Controller代碼中看到拋出了一個自定義的異常,在Controller中,屬于最外層的代碼了,這個時候如果有異常就不能直接拋出去了,這里再拋出去就沒有人處理了,服務(wù)器只能返回給前臺一個錯誤,用戶體驗(yàn)不好。因此,建議所有的Controller代碼都用try-catch包裹,捕獲到異常后統(tǒng)一進(jìn)行處理,然后再給前臺一個合理的提示信息。在上一步中拋出了一個自定義異常:
throw new MyException(ResultEnum.DELETE_ERROR.getCode(), "刪除員工出錯,請聯(lián)系網(wǎng)站管理人員。", e);
該自定義異常有三個屬性,分別是異常狀態(tài)碼,異常提示信息,以及捕獲到的異常對象,接下來定義一個全局的異常捕獲,統(tǒng)一對異常進(jìn)行處理:
@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)一捕獲異常之后,可以進(jìn)行相應(yīng)的處理,我這里沒有進(jìn)行特殊的處理,只是進(jìn)行了一下區(qū)分,獲取數(shù)據(jù)的接口拋出的異常,前臺肯定是使用的ajax請求,因此返回前臺一個json格式的信息,提示出錯誤內(nèi)容。如果是跳轉(zhuǎn)頁面拋出的異常,類似404之類的,直接跳轉(zhuǎn)到自定義的404頁面。補(bǔ)充一點(diǎn),springboot項(xiàng)目默認(rèn)是有/error路由的,返回的就是error頁面,所以,如果你在你的項(xiàng)目中定義一個error.html的頁面,如果報(bào)404錯誤,會自動跳轉(zhuǎn)到該頁面。
補(bǔ)充,統(tǒng)一異常處理類中使用了一個注解@Slf4j,該注解是lombok包中的,項(xiàng)目中加入了該依賴后,再也不用寫繁瑣的get、set等代碼,當(dāng)然類似的像上邊的聲明log對象的代碼也不用寫了:

4. 日志配置文件區(qū)分環(huán)境
本示例使用的是logback日志框架。需要在resources目錄中添加logback.xml配置文件,這是一個比較頭疼的地方,我本來想一個配置文件也沒有的,奈何我也不知道怎么將這個日志的配置文件放到數(shù)據(jù)庫中,所以暫時先這么著了,好在幾乎沒有需要改動它的時候。
我在項(xiàng)目中添加了兩個日志的配置文件,分別是logback-dev.xml和logback-pro.xml可以根據(jù)不同的環(huán)境決定使用哪個配置文件,在數(shù)據(jù)庫配置表中(相當(dāng)于寫在了application.properties中)添加一條配置logging.config=classpath:logback-dev.xml來區(qū)分使用哪個文件作為日志的配置文件,配置文件內(nèi)容如下:
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ìn)行了按照日志進(jìn)行拆分的配置,每一個級別的日志每一天都會重新生成一個,根據(jù)日期進(jìn)行命名,超過180天的日志將自動會刪除。當(dāng)然你還可以按照日志大小進(jìn)行拆分,我這里沒有進(jìn)行這項(xiàng)的配置。
5. 全局接口請求記錄
進(jìn)行全局的接口請求記錄,可以記錄接口的別調(diào)用情況,然后進(jìn)行一些統(tǒng)計(jì)和分析,在本示例中,只是將全局的接口調(diào)用情況記錄到了info日志中,沒有進(jìn)行相應(yīng)的分析操作:
@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();
// 記錄請求內(nèi)容
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) {
// 請求返回的內(nèi)容
if (ret instanceof ResultInfo) {
log.info(AppConst.INFO_LOG_PREFIX + "返回結(jié)果:" + ((ResultInfo) ret).getCode().toString());
}
}
}
6. 集成shiro實(shí)現(xiàn)權(quán)限校驗(yàn)
集成shirl,輕松的實(shí)現(xiàn)了權(quán)限的管理,如果對shiro不熟悉朋友,還需要先把shiro入門一下才好,shiro的集成一般都需要自定義一個realm,來進(jìn)行身份認(rèn)證和授權(quán),因此先來一個自定義realm:
MyShiroRealm.java
public class MyShiroRealm extends AuthorizingRealm {
@Resource
private MenuService menuService;
@Resource
private UserService userService;
/**
* 授權(quán)
*/
@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;
}
/**
* 身份認(rèn)證
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String userName = String.valueOf(token.getUsername());
// 從數(shù)據(jù)庫獲取對應(yīng)用戶名的用戶
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;
}
}
身份認(rèn)證如果簡單的理解的話,你可以理解為登錄的過程。授權(quán)就是授予你權(quán)利,代表你在這個系統(tǒng)中有權(quán)限做什么動作,具體shiro的內(nèi)容小伙伴們自行去學(xué)習(xí)吧。
7. 登錄校驗(yàn),安全攔截
在集成了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);
}
}
}
身份認(rèn)證的操作交給了shiro,利用用戶名和密碼構(gòu)造一個身份的令牌,調(diào)用shiro的login方法,這個時候就會進(jìn)入自定義reaml的身份認(rèn)證方法中,也就是上一步中的doGetAuthenticationInfo方法,具體的認(rèn)證操作看上一步的代碼,無非就是賬號密碼的校驗(yàn)等。身份認(rèn)證的時候,通過拋出異常的方式給登錄操作返回信息,從而在登錄方法中判斷身份認(rèn)證失敗后的信息,從而返回給前臺進(jìn)行提示。
在身份認(rèn)證通過后,拿到當(dāng)前登錄用戶的信息,首先放到session中,便于后續(xù)的使用。其次在放到application對象中,防止同一個賬號的多次登錄。
有了身份任何和授權(quán)自然就少不了安全校驗(yàn),在本示例中使用了一個攔截器來實(shí)現(xiàn)安全校驗(yà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;
}
// 獲取當(dāng)前登錄用戶
User user = (User) req.getSession().getAttribute(AppConst.CURRENT_USER);
// 沒有登錄狀態(tài)下訪問系統(tǒng)主頁面,都跳轉(zhuǎn)到登錄頁,不提示任何信息
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;
}
}
在攔截器中,首先對一些不需要校驗(yàn)的請求進(jìn)行放行,例如登錄動作、登錄頁面請求以及錯誤頁面等。然后獲取當(dāng)前登錄的用戶,如果沒有登錄則自動跳轉(zhuǎn)到登錄頁面。在返回前臺的時候,判斷請求屬于同步請求還是異步請求,如果是同步請求,直接進(jìn)行頁面的跳轉(zhuǎn),跳轉(zhuǎn)到登錄頁面。如果是異步請求,則返回前臺一個json數(shù)據(jù),提示前臺登錄信息失效。這里補(bǔ)充一點(diǎn),前臺可以使用ajaxhook進(jìn)行異步請求的捕獲,相當(dāng)于一個前端的全局?jǐn)r截器,攔截所有的異步請求,可以監(jiān)視所有異步請求的返回結(jié)果,如果返回的是登錄失效,則進(jìn)行跳轉(zhuǎn)到登錄頁面的操作。具體ajaxhook的使用方法請自行學(xué)習(xí),本示例中暫時沒有使用。
下面是判斷同一個賬號有沒有多次登錄,具體方法就是使用當(dāng)前的sessionId,將當(dāng)前登錄用戶和請求sissionId作為一個key-value放到了application中,如果該用戶的sessionId發(fā)生了變化,說明又有一個人登錄了該賬號,然后就進(jìn)行相應(yīng)的提示操作。
8. 配置虛擬路徑
web項(xiàng)目中免不了并上傳的操作,圖片或者文件,如果上傳的是圖片,一般還要進(jìn)行回顯的操作,我們不想將上傳的文件直接存放在項(xiàng)目的目錄中,而是放在一個自定義的目錄,同時項(xiàng)目還可以訪問:

這樣在進(jìn)行上傳操作的時候,就可以將上傳的文件放到項(xiàng)目以外的目錄中,然后外部訪問的時候,通過虛擬路徑進(jìn)行映射訪問。
9. 集成redis緩存
springboot的強(qiáng)悍就是集成一個東西太方便了,如果你不想做任何配置,只需要加入redis的依賴,然后在配置文件(本示例中配置是在數(shù)據(jù)庫中)中添加redis的鏈接信息,就可以在項(xiàng)目中使用redis了。
本示例中使用redis做緩存,首先寫了一個緩存的類,代碼有些長不做展示。然后在service層進(jìn)行緩存的操作:

代碼中使用了double check的騷操作,防止高并發(fā)下緩存失效的問題(雖然我的示例不可能有高并發(fā),哈哈)。另外就是緩存更新的問題,網(wǎng)上說的有很多,先更新數(shù)據(jù)再更新緩存,先更新緩存再更新數(shù)據(jù)庫等等,具體要看你是做什么,本示例中沒有什么需要特殊注意的地方,因此就先更新數(shù)據(jù)庫,然后再移除緩存:

10. 項(xiàng)目代碼和依賴以及靜態(tài)資源分別打包
之前遇到一個問題,springboot打包之后是一個jar文件,如果將所有依賴也打到這個jar包中的話,那么這個jar包動輒幾十兆,來回傳輸不說,如果想改動其中的一個配置內(nèi)容,還異常的繁瑣,因此,將項(xiàng)目代碼,就是自己寫的代碼打成一個jar包(一般只有幾百k),然后將所有的依賴打包到一個lib目錄,然后再將所有的配置信息以及靜態(tài)文件打包到resources目錄,這樣,靜態(tài)文件可以直接進(jìn)行修改,瀏覽器清理緩存刷新即可出現(xiàn)改動效果,而且打包出來的項(xiàng)目代碼也小了很多,至于依賴,一般都是不變的,所以也沒必要每次都打包它。具體操作就是在pom.xml中增加一個插件即可,代碼如下:
代碼太長,不做展示
11. 項(xiàng)目啟動
到現(xiàn)在都沒有貼一個項(xiàng)目的目錄結(jié)構(gòu),先來一張。目錄中項(xiàng)目跟目錄下的demo.sh就是啟動腳本,當(dāng)時從網(wǎng)上抄襲改裝過來的,源代碼出自那位大師之手我就不知道了,先行謝過。在部署到服務(wù)器的時候,如果服務(wù)器上安裝好了jdk、maven、git,每次修改完代碼,直接git pull下來,然后mvn package打包,然后直接./demo.sh start就可以啟動項(xiàng)目,方便快速。慢著,忘記了,如果你提交到github中的application.properties中的數(shù)據(jù)源配置信息是開發(fā)環(huán)境的話,那么你在打包之后,target/resources中的application.properties中的數(shù)據(jù)源需要改成開發(fā)環(huán)境才可以啟動。當(dāng)然如果你嫌麻煩,可以直接將開發(fā)環(huán)境的數(shù)據(jù)源配置push到github中,安不安全就要你自己考慮了。

12. 總結(jié)
示例中可能還有一些細(xì)節(jié)沒有說到,總之這個項(xiàng)目是慢慢的添磚添瓦弄出來的,自己在寫很多其他的項(xiàng)目的時候,都是以此項(xiàng)目為模板進(jìn)行改造出來的,個人感覺很實(shí)用很方便,用著也很舒服。github地址:https://github.com/503612012/demo歡迎收藏。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
IntelliJ IDEA本地代碼覆蓋后恢復(fù)原來的代碼圖解
今天小編就為大家分享一篇關(guān)于IntelliJ IDEA本地代碼覆蓋后恢復(fù)原來的代碼圖解,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2018-10-10
在Java中將List轉(zhuǎn)換為String輸出過程解析
這篇文章主要介紹了在Java中將List轉(zhuǎn)換為String輸出過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-09-09
spring-boot整合ehcache實(shí)現(xiàn)緩存機(jī)制的方法
spring-boot是一個快速的集成框架,其設(shè)計(jì)目的是用來簡化新Spring應(yīng)用的初始搭建以及開發(fā)過程。這篇文章主要介紹了spring-boot整合ehcache實(shí)現(xiàn)緩存機(jī)制,需要的朋友可以參考下2018-01-01
SpringBoot如何使用自定義注解實(shí)現(xiàn)接口限流
這篇文章主要介紹了SpringBoot如何使用自定義注解實(shí)現(xiàn)接口限流,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-06-06
Java設(shè)計(jì)模式以虹貓藍(lán)兔的故事講解代理模式
代理模式是Java常見的設(shè)計(jì)模式之一。所謂代理模式是指客戶端并不直接調(diào)用實(shí)際的對象,而是通過調(diào)用代理,來間接的調(diào)用實(shí)際的對象2022-04-04

