1 用什麼技術實現什麼問題-2 實現步驟-3 遇到的問題
shiro在專案中的應用
shiro
在專案中,我們用shiro充當安全框架來進行許可權的管理控制。
在web.xml配置了shiroFilter,對所有的請求都進行安全控制。之後在shiro的配置檔案配置一個id為shiroFilter的bean物件。這點要保證和web.xml中filter的名字一致。
在進行許可權管理時整體上來說分為認證和授權兩大核心。
認證:就是隻有使用者經過了登入頁面的驗證才能成為合法使用者繼而訪問後臺受保護的資源。
- 在shiro的配置檔案配置了基於url路徑的安全認證。對一些靜態資源如js/css等,包括登入以及驗證碼設定為匿名訪問。對於其他的url路徑設定為authc【認證】即只有經過正常的登入並且驗證成功後才能訪問。
- 為了保證資料庫中使用者密碼的安全性,我們對其密碼進行了md5加密處理,又因為單純的md5加密比較容易破解,所以我們使用了密碼+鹽【salt】的方式,這裡面的鹽是由使用者名稱+隨機數構成的,並且還進行了2次迭代即md5(md5(密碼+鹽)))這樣就更增加了安全性。在使用者新增和重置使用者密碼時,呼叫PasswordHelper將加密後的密碼以及生成的鹽即salt都儲存到資料庫的使用者表中。
- 在使用者進行登入認證時我們會在登入方法中傳入使用者名稱和密碼並建立一個UsernamePasswordToken,之後通過SecurityUtils.getSubject()獲得subject物件,接著就可以通過subject獲取session資訊進而獲取驗證碼和使用者登入時候輸入的驗證碼進行對比,最後呼叫subject.login()方法。
- 這時就會去執行我們自定義的UserRealm【繼承於AuthorizingRealm】物件中的doGetAuthenticationInfo認證方法。在該認證方法中token獲取使用者名稱,並通過注入的userService根據使用者名稱來獲取使用者資訊,最後將使用者物件,密碼,鹽構建成一個SimpleAuthenticationInfo返回即可進行驗證判斷。再通過subject.isAuthenticated()判斷是否通過認證從而跳轉到合適的頁面。
授權:指的是針對不同的使用者給予不同的操作許可權。
- 我們採用RBAC【Resource-Based Access Control】這種基於資源的訪問控制。在資料庫設計時涉及到5張核心表,即使用者表、使用者角色關聯表、角色表、角色許可權關聯表、許可權表【選單表】。
- 在後臺的系統管理模組中包含使用者管理,角色管理,選單管理,給使用者賦角色,給角色賦許可權等操作。這樣就建立起了使用者和角色之間多對多的關係以及角色和許可權之間多對多的關係。角色起到的作用就是包含一組許可權,這樣當用戶不需要這個許可權的時候只需要在給角色賦許可權的操作中去掉該許可權即可,無需改動任何程式碼。
- 在前臺展示頁面中通過shiro:hasPermission標籤來判斷按鈕是否能夠顯示出來,從而可以將許可權控制到按鈕級別。
- 我們的許可權表也就是選單表,在設計選單表的時候我們有id,pid,menuName,menuUrl,type[menu,button]兩種型別,permission[資源:操作 如product:create article:create article:delete]這幾個欄位構成。
- 這樣當用戶登入認證後,我們可以根據使用者id作為查詢條件,將使用者角色關聯表,角色表,角色選單關聯表以及
選單表進行多表聯查獲得該使用者所擁有的選單資訊。從而達到不同的使用者顯示不同的選單樹。 - 當用戶在位址列輸入要訪問的URL時,跳轉到具體Controller的方法中,shiro呼叫UserRealm中的doGetAuthorizationInfo方法,該方法中根據使用者id查詢使用者所擁有的許可權資訊,並將這些許可權資訊都新增到SimpleAuthorizationInfo。
- 之後shiro會將該使用者所擁有的許可權和訪問該url所需要的許可權做個對比。如果擁有許可權則可以訪問,否則將會
丟擲一個UnauthorizedException未授權的異常。 - GlobalExceptionHandler類通過@ControllerAdvice結合@ExceptionHandler會捕獲所有控制層丟擲來的指定異常,然後根據異常資訊跳轉到前臺頁面顯示該使用者無許可權訪問。
web.xml
<!-- spring整合安全框架 -->
<filter>
<filter-name>DelegatingFilterProxy</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<!-- 初始化引數 -->
<init-param>
<param-name>targetBeanName</param-name>
<param-value>shiroFilter</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>DelegatingFilterProxy</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
shiro.xml
<!-- shiro開啟事務註解 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
</bean>
<!--
/** 除了已經設定的其他路徑的認證
-->
<!-- shiro工廠bean配置 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- shiro的核心安全介面 -->
<property name="securityManager" ref="securityManager"></property>
<!-- 要求登入時的連線 -->
<property name="loginUrl" value="/login.jsp"></property>
<!-- 登入成功後要跳轉的連線(此處已經在登入中處理了) -->
<!-- <property name="successUrl" value="/index.jsp"></property> -->
<!-- 未認證時要跳轉的連線 -->
<property name="unauthorizedUrl" value="/refuse.jsp"></property>
<!-- shiro連線約束配置 -->
<property name="filterChainDefinitions">
<value>
<!-- 對靜態資源設定允許匿名訪問 -->
/images/** = anon
/js/** = anon
/css/** = anon
<!-- 可匿名訪問路徑,例如:驗證碼、登入連線、退出連線等 -->
/auth/login = anon
<!-- 剩餘其他路徑,必須認證通過才可以訪問 -->
/** = authc
</value>
</property>
</bean>
<!-- 配置shiro安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realms" ref="customRealm"></property>
</bean>
<!-- 自定義Realm -->
<bean id="customRealm" class="com.zxz.auth.realm.UserRealm">
<property name="credentialsMatcher" ref="credentialsMatcher"></property>
</bean>
<!-- 配置憑證演算法匹配器 -->
<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!-- Md5演算法 -->
<property name="hashAlgorithmName" value="Md5"></property>
</bean>
UserRealm類
package com.how2java.realm;
import java.util.Set;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.codec.CodecException;
import org.apache.shiro.crypto.UnknownAlgorithmException;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import com.how2java.pojo.User;
import com.how2java.service.PermissionService;
import com.how2java.service.RoleService;
import com.how2java.service.UserService;
public class DatabaseRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermissionService permissionService;
// 授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//能進入到這裡,表示賬號已經通過驗證了
String userName =(String) principalCollection.getPrimaryPrincipal();
//通過service獲取角色和許可權
Set<String> permissions = permissionService.listPermissions(userName);
Set<String> roles = roleService.listRoleNames(userName);
//授權物件
SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
//把通過service獲取到的角色和許可權放進去
s.setStringPermissions(permissions);
s.setRoles(roles);
return s;
}
// 認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//獲取賬號密碼
UsernamePasswordToken t = (UsernamePasswordToken) token;
String userName= token.getPrincipal().toString();
//獲取資料庫中的密碼
User user =userService.getByName(userName);
String passwordInDB = user.getPassword();
String salt = user.getSalt();
//認證資訊裡存放賬號密碼, getName() 是當前Realm的繼承方法,通常返回當前類名 :databaseRealm
//鹽也放進去
//這樣通過applicationContext-shiro.xml裡配置的 HashedCredentialsMatcher 進行自動校驗
SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(userName,passwordInDB,ByteSource.Util.bytes(salt),getName());
return a;
}
}
全域性異常處理類
/**
* Created by kinginblue on 2017/4/10.
* @ControllerAdvice + @ExceptionHandler 實現全域性的 Controller 層的異常處理
*/
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 處理所有不可知的異常
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
@ResponseBody
AppResponse handleException(Exception e){
LOGGER.error(e.getMessage(), e);
AppResponse response = new AppResponse();
response.setFail("操作失敗!");
return response;
}
/**
* 處理所有業務異常
* @param e
* @return
*/
@ExceptionHandler(BusinessException.class)
@ResponseBody
AppResponse handleBusinessException(BusinessException e){
LOGGER.error(e.getMessage(), e);
AppResponse response = new AppResponse();
response.setFail(e.getMessage());
return response;
}
/**
* 處理所有介面資料驗證異常
* @param e
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
AppResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e){
LOGGER.error(e.getMessage(), e);
AppResponse response = new AppResponse();
response.setFail(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
return response;
}
}
redis在專案中的應用
為了保證高併發訪問量大的情況,避免過多的連線一下子查詢到資料庫造成資料庫的崩潰,我們採用了redis來實現資料的快取,在查詢資料時,先從redis快取中查詢資料庫是否存在,如果快取中存在我們就直接從快取中取,這樣可以減少資料庫的訪問;如果快取中不存在再去資料庫查詢,並且將查詢出來的資料新增到快取中,因為redis的查詢速度是相當快的(11,000/次)。
另外為了保證redis伺服器的安全,通常會在redis.conf中繫結具體的ip地址,這樣只有該地址才能訪問redis伺服器,並且設定密碼,為了保證redis不會因為佔用記憶體過大而導致系統宕機,通常在將redis當作快取伺服器使用時,設定儲存資料的過期時間,並且通過設定maxmenory【最大記憶體】和maxmemory-policy【資料清除策略】為allkeys-lru來達到預期效果。
我在專案中通常使用redis來充當快取伺服器來快取分類列表,熱銷課程,推薦課程等。使用jedis作為客戶端,並且考慮到效能問題,使用來jedis連線池。考慮到redis伺服器的高可用性,我們做了redis的主從複製,並且通過加入哨兵來使redis伺服器宕機時,從伺服器自動轉換為主伺服器繼續提供服務。
根據內容分類id查詢內容列表(大廣告位)
- 首先新建redis.xml檔案,配置了連線池、連線工廠、模板
- 接著在web.xml引入redis.xml
- 然後在service層的實現類上,新增@Autowired注入RedisTemplate,通過操作模板來進行增刪改查。在新增大廣告位功能加redis邏輯,先查詢redis資料庫是否存在資料,如果有直接返回資料;如果沒有,則呼叫mysql查詢,查詢出的資料在返回前,加到redis資料庫中
//前臺業務service層
public String getContentList() {
// 呼叫服務層 查詢商品內容資訊(即大廣告位)
String result = HttpClientUtil.doGet(REST_BASE_URL + REST_INDEX_AD_URL);
try {
// 把字串轉換成TaotaoResult
TaotaoResult taotaoResult = TaotaoResult.formatToList(result, TbContent.class);
// 取出內容列表
List<TbContent> list = (List<TbContent>) taotaoResult.getData();
List<Map> resultList = new ArrayList<Map>();
// 建立一個jsp頁碼要求的pojo列表
for(TbContent tbContent : list) {
Map map = new HashMap();
map.put("srcB", tbContent.getPic2());
map.put("height", 240);
map.put("alt", tbContent.getTitle());
map.put("width", 670);
map.put("src", tbContent.getPic());
map.put("widthB", 550);
map.put("href", tbContent.getUrl());
map.put("heightB", 240);
resultList.add(map);
}
return JsonUtils.objectToJson(resultList);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
//rest服務層 新增大廣告位顯示redis的邏輯
@Override
public List<TbContent> getContentList(long contentCategoryId) {
try {
// 從快取中取內容
String result = jedisClient.hget(INDEX_CONTENT_REDIS_KEY,
contentCategoryId + "");
if (!StringUtils.isBlank(result)) {
// 把字串轉換成list
List<TbContent> resultList = JsonUtils.jsonToList(result,
TbContent.class);
return resultList;
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
// 根據內容分類id查詢內容列表
TbContentExample example = new TbContentExample();
Criteria criteria = example.createCriteria();
criteria.andCategoryIdEqualTo(contentCategoryId);
// 執行查詢
List<TbContent> list = contentMapper.selectByExample(example);
try {
// 向快取中新增內容
String cacheString = JsonUtils.objectToJson(list);
jedisClient.hset(INDEX_CONTENT_REDIS_KEY, contentCategoryId + "",
cacheString);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
return list;
}
/**
* redisServiceImpl.java
* 前臺修改內容時呼叫此服務,刪除redis中的該內容的內容分類下的全部內容
*/
@Override
public TaotaoResult syncContent(long contentCategoryId) {
try {
jedisClient.hdel(INDEX_CONTENT_REDIS_KEY, contentCategoryId + "");
} catch (Exception e) {
e.printStackTrace();
return TaotaoResult.build(500, ExceptionUtil.getStackTrace(e));
}
return TaotaoResult.ok();
}
資料庫的主從複製讀寫分離
當時我們給資料庫也做了優化,配置了主從複製和通過mycat進行讀寫分離。
MySQL的主從複製架構是目前使用最多的資料庫架構之一,尤其是負載比較大的網站。它的原理是:從伺服器的io執行緒到主伺服器獲取二進位制日誌,並在本地儲存中繼日誌,然後通過sql執行緒在從庫上執行中繼日誌的內容,從而使從庫和主庫保持一致。
配置好主從複製之後,還通過mycat配置讀寫分離,主庫負責寫操作,讀庫負責讀操作,從而降低資料庫壓力,提高效能。
- 主從同步
兩臺資料庫伺服器
搭建兩臺資料庫伺服器,一臺作為主伺服器master,一臺作為從伺服器slave。
Master Server以及Slave Server配置配置
在伺服器修改my.cnf檔案的配置,通過log-bin啟用二進位制日誌記錄,以及建立唯一的server id,主庫與從庫,以及從庫之間的server id不同。
主資料庫使用者
在主庫新增一個使用者,授予replication slave許可權,使得從庫可以使用該MySQL使用者名稱和密碼連線到主庫。
配置主庫通訊
配置主庫通訊。首先,在主庫執行命令show master status,記下file和position欄位對應的值;然後,在從庫設定它的master,將master_log_file和master_log_pos替換為剛才記下的值。
開啟服務
通過start slave開啟服務
測試主從同步
通過show slave status檢查主從同步狀態。如果 Slave_IO_Running 和 Slave_SQL_Running 都為Yes,Seconds_Behind_Master為0,說明配置成功。
- 讀寫分離
搭建mycat
搭建個mycat伺服器
配置mycat的schema.xml
在schema.xml(定義邏輯庫,表、分片節點等內容)中定義了邏輯庫,可以使用schema標籤來區分不同的邏輯庫。配置了mycat邏輯主機dataHost對應的物理主機WriteHost寫庫和ReadHost讀庫,其中也設定對應的mysql登陸資訊。
配置mycat的server.xml
server.xml (定義使用者以及系統相關變數)定義了兩個使用者,一個是隻讀使用者,一個是讀寫使用者,以及可訪問的schema。
啟動mycat
通過mycat start啟動mycat
測試讀寫分離
最後通過mysql連線到mycat,使用的是在server.xml定義的使用者,預設埠號是8066,如果看到mycat server,說明配置成功。
my.cnf
## 10.211.55.10(master)
bind-address=192.168.78.128 #master 伺服器地址
log_bin=mysql-bin
server_id=1
## 10.211.55.15(slave)
bind-address=192.168.78.130 #slave 伺服器地址
log_bin=mysql-bin
server_id=2
主庫使用者
## 建立 test 使用者,指定該使用者只能在主庫 10.211.55.10 上使用 MyPass1! 密碼登入
mysql> create user 'test'@'10.211.55.15' identified by 'MyPass1!';
## 為 test 使用者賦予 REPLICATION SLAVE 許可權。
mysql> grant replication slave on *.* to 'test'@'10.211.55.15';
show master status
mysql> show master status;
+------------------+----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000001 | 629 | | | |
+------------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)
修改從庫master
mysql> CHANGE MASTER TO
-> MASTER_HOST='master_host_name',
-> MASTER_USER='replication_user_name',
-> MASTER_PASSWORD='replication_password',
-> MASTER_LOG_FILE='recorded_log_file_name',
-> MASTER_LOG_POS=recorded_log_position;
mysql> change master to
-> master_host='10.211.55.10',
-> master_user='test',
-> master_password='MyPass1!',
-> master_log_file='mysql-bin.000001',
-> master_log_pos=629;
show slave status
mysql> show slave status\G
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 192.168.252.123
Master_User: replication
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: mysql-bin.000001
Read_Master_Log_Pos: 629
Relay_Log_File: master2-relay-bin.000003
Relay_Log_Pos: 320
Relay_Master_Log_File: mysql-bin.000001
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
......
Slave_IO_State #從站的當前狀態
Slave_IO_Running: Yes #讀取主程式二進位制日誌的I/O執行緒是否正在執行
Slave_SQL_Running: Yes #執行讀取主伺服器中二進位制日誌事件的SQL執行緒是否正在執行。與I/O執行緒一樣
Seconds_Behind_Master #是否為0,0就是已經同步了
schema.xml
<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
<schema name="TESTDB" checkSQLschema="false" sqlMaxLimit="100" dataNode="dn1"></schema>
<dataNode name="dn1" dataHost="node1" database="user_db" />
<dataHost name="node1" maxCon="1000" minCon="10" balance="1" writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<writeHost host="10.211.55.10" url="10.211.55.10:3306" user="root" password="admin">
<readHost host="10.211.55.15" url="10.211.55.15:3306" user="root" password="admin" />
</writeHost>
<writeHost host="10.211.55.15" url="10.211.55.15:3306" user="root" password="admin" />
</dataHost>
</mycat:schema>
server.xml
<user name="root" defaultAccount="true">
<property name="password">123456</property>
<property name="schemas">TESTDB</property>
<!-- 表級 DML 許可權設定 -->
<!--
<privileges check="false">
<schema name="TESTDB" dml="0110" >
<table name="tb01" dml="0000"></table>
<table name="tb02" dml="1111"></table>
</schema>
</privileges>
-->
</user>
<user name="user">
<property name="password">user</property>
<property name="schemas">TESTDB</property>
<property name="readOnly">true</property>
</user>
dubbo+zookeeper
dubbo+zookeeper實現了分散式部署。
dubbo是阿里巴巴開源專案,基於java的高效能rpc分散式服務框架,現已經是apache開源專案。dubbo把專案分為服務提供者和服務消費者,將核心業務抽取出來,作為獨立的服務,逐漸形成穩定的服務中心,從而提高業務複用。
zookeeper是dubbo的提供者用於暴露服務的註冊中心,起一個排程和協調的作用。
註冊中心zookeeper
使用前,先搭建個註冊中心,使用的是dubbo推薦的zookeeper。進入conf下,複製zoo_sample.cfg命名為zoo.cfg,修改相關配置(dataDir,dataLogDir以及server)。
provider.xml
新建dubbo-provider.xml配置服務提供者。通過dubbo:application配置提供方應用名,dubbo:registry配置註冊中心地址,dubbo:protocol配置協議和埠號,以及dubbo:service宣告需要暴露的服務介面。
consumer.xml
新建dubbo-consumer.xml配置服務消費者。通過dubbo:application配置消費方應用名,dubbo:registry配置註冊中心地址,以及dubbo:reference生成遠端服務代理。
provider.xml引數調優
考慮到dubbo的健壯性和效能,我們對它的引數進行調優。通過dubbo:protocol的threadpool="fixed" threads=200來啟用執行緒池,以及dubbo:service的connections=5來指定長連線數量。
dubbo叢集
配置dubbo叢集來提高健壯性和可用性。dubbo預設的叢集容錯機制是失敗自動切換failover,預設重試2次,可以通過(dubbo:service或dubbo:reference的)retries設定重試次數。dubbo預設的負載均衡策略是隨機random,按權重設定隨機概率。
直連測試
我們寫完dubbo的提供者之後,為了測試服務介面的正確性,會進行直連測試。首先,在提供者端,將dubbo:registry的register設定為false,使其只訂閱服務不註冊正在開發的服務;然後,在消費者端,通過dubbo:reference的url指向提供者,進行直連測試。
*7. 所謂dubbo叢集(被動說)
所謂dubbo叢集就是dubbo的服務部署多份,在不同的機器或同一臺機器的不同埠號,從而在啟動時可以向註冊中心註冊服務,這樣結合dubbo的叢集容錯策略和負載均衡策略來提高可用性。
privider.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<!-- 提供方應用資訊,用於計算依賴關係 -->
<dubbo:application name="hello-world-app" />
<!-- 使用multicast廣播註冊中心暴露服務地址 -->
<dubbo:registry address="multicast://224.5.6.7:1234" />
<!-- 用dubbo協議在20880埠暴露服務 -->
<dubbo:protocol name="dubbo" port="20880" />
<!-- 宣告需要暴露的服務介面 -->
<dubbo:service interface="com.alibaba.dubbo.demo.DemoService" ref="demoService" />
<!-- 和本地bean一樣實現服務 -->
<bean id="demoService" class="com.alibaba.dubbo.demo.provider.DemoServiceImpl" />
</beans>
consumer.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<!-- 消費方應用名,用於計算依賴關係,不是匹配條件,不要與提供方一樣 -->
<dubbo:application name="consumer-of-helloworld-app" />
<!-- 使用multicast廣播註冊中心暴露發現服務地址 -->
<dubbo:registry address="multicast://224.5.6.7:1234" />
<!-- 生成遠端服務代理,可以和本地bean一樣使用demoService -->
<dubbo:reference id="demoService" interface="com.alibaba.dubbo.demo.DemoService" />
</beans>
ssm整合
ssm框架是spring mvc、aspring和mybatis框架的整合,是標準的mvc模式,將整個系統分為表現層、controller層、service層和dao層四層。spring mvc負責請求的轉發和檢視管理,spring負責業務物件管理,mybatis作為資料物件的持久化引擎。
controller層
在controller層的類上新增@Controller註解標記為一個控制器,@RequestMapping註解對映訪問路徑,以及@Resource注入service層。
service層
在service層的實現類上新增@Service標記為一個service,以及@Autowired注入dao層。
dao層
dao層只有介面,沒有實現類。是在mybatis對應含有sql語句的xml檔案中,通過namespace指定要實現的dao層介面,並使得sql語句的id和dao層介面的方法名一致,從而明確呼叫指定dao層介面的方法時要執行的sql語句。
web.xml
在web.xml配置spring的監聽器ContextLoaderListner並載入spring的配置檔案spring-common.xml。還配置了spring mvc的核心控制器DispatcherServlet並載入spring mvc的配置檔案spring-mvc-controller.xml。
spring配置檔案
在spring配置檔案spring-common.xml中,配置dbcp資料庫連線池,以及sqlSession來載入mapper下所有的xml檔案,並通過MapperScannerConfigurer對mapper層進行掃描,也就是dao層。還通過AOP的切點表示式對service進行事務控制,並對service進行掃描使得註解生效。
spring mvc配置檔案
在spring mvc配置檔案spring-mvc-controller.xml中,配置component-scan對controller層進行掃描。還配置了內部資源檢視解析器InternalResouceViewResolver,從而在控制層進行頁面跳轉時新增指定的字首和字尾。
controller層
package com.how2java.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import com.how2java.pojo.Category;
import com.how2java.service.CategoryService;
// 告訴spring mvc這是一個控制器類
@Controller
@RequestMapping("")
public class CategoryController {
@Autowired
CategoryService categoryService;
@RequestMapping("listCategory")
public ModelAndView listCategory(){
ModelAndView mav = new ModelAndView();
List<Category> cs= categoryService.list();
// 放入轉發引數
mav.addObject("cs", cs);
// 放入jsp路徑
mav.setViewName("listCategory");
return mav;
}
}
service實現類
package com.how2java.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.how2java.mapper.CategoryMapper;
import com.how2java.pojo.Category;
import com.how2java.service.CategoryService;
@Service
public class CategoryServiceImpl implements CategoryService{
@Autowired
CategoryMapper categoryMapper;
public List<Category> list(){
return categoryMapper.list();
}
}
mapper
package com.how2java.mapper;
import java.util.List;
import com.how2java.pojo.Category;
public interface CategoryMapper {
public int add(Category category);
public void delete(int id);
public Category get(int id);
public int update(Category category);
public List<Category> list();
public int count();
}
mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.how2java.mapper.CategoryMapper">
<insert id="add" parameterType="Category" >
insert into category_ ( name ) values (#{name})
</insert>
<delete id="delete" parameterType="Category" >
delete from category_ where id= #{id}
</delete>
<select id="get" parameterType="_int" resultType="Category">
select * from category_ where id= #{id}
</select>
<update id="update" parameterType="Category" >
update category_ set name=#{name} where id=#{id}
</update>
<select id="list" resultType="Category">
select * from category_
</select>
</mapper>
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
<!-- spring的配置檔案-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- spring mvc核心:分發servlet -->
<servlet>
<servlet-name>mvc-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- spring mvc的配置檔案 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springMVC.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>mvc-dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
spring配置檔案
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<context:annotation-config />
<context:component-scan base-package="com.how2java.service" />
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName">
<value>com.mysql.jdbc.Driver</value>
</property>
<property name="url">
<value>jdbc:mysql://localhost:3306/how2java?characterEncoding=UTF-8</value>
</property>
<property name="username">
<value>root</value>
</property>
<property name="password">
<value>admin</value>
</property>
</bean>
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="typeAliasesPackage" value="com.how2java.pojo" />
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath:com/how2java/mapper/*.xml"/>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.how2java.mapper"/>
</bean>
</beans>
spring mvc配置檔案
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd">
<context:annotation-config/>
<context:component-scan base-package="com.how2java.controller">
<context:include-filter type="annotation"
expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<mvc:annotation-driven />
<mvc:default-servlet-handler />
<!-- 檢視定位 -->
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass"
value="org.springframework.web.servlet.view.JstlView" />
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
</beans>
AOP在專案中的應用/後臺的日誌管理模組
後臺的日誌管理模組是為了讓開發人員根據記錄的日誌資訊及時找到系統產生的錯誤以及知道當前系統在整個執行過程中都執行了哪些類的哪些方法。考慮到對日誌的統一處理,就採用了AOP這項技術。
面向切面程式設計aop,把功能劃分為核心業務功能和切面功能,比如日誌、事務、效能統計等,核心業務功能和切面功能分別獨立開發,通過aop可以根據需求將核心業務功能和切面功能結合在一起,比如增加操作可以和事務切面結合在一起,查詢操作可以和效能統計切面結合在一起。
在專案中我們通常使用AOP進行事務控制和日誌的統一處理。
事務控制
在事務控制方面,是通過Spring自帶的事務管理器,配置切面的切點表示式,對service層指定的方法,比如增刪改進行事務控制,對查詢進行只讀事務控制,從而提高效能。
- 日誌
log4j.properties
在日誌的統一處理方面,首先配置log4j.properties並指定日誌級別為info,將日誌輸入到控制檯以及指定的日誌檔案中。
日誌切面類LogAspect
接著自己寫一個日誌的切面類LogAspect,並通過ProceedingJoinPoint【連線點】獲取目標類名以及執行的方法名,通過呼叫LOG.info方法記錄進入方法時的日誌資訊。為了記錄出現異常時的錯誤日誌,通過對proceed方法進行try...catch捕獲,在catch中用LOG.error記錄錯誤日誌資訊。
spring mvc配置檔案
最後在spring-mvc-controller.xml中配置切面aop:config,並通過aop:pointcut的切點表示式對所有的Controller和裡面的方法進行攔截,aop:arround配置環繞通知,裡面的method屬性指定切面類的方法名。
事務控制
<!--Spring自帶的事物管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!--事務管理通知配置-->
<tx:advice id="txadvice" transaction-manager="transactionManager">
<tx:attributes>
<!--propagation="REQUIRED"指如果當前有事物就在該事物中執行,如果沒有,就開啟一個新的事物(增刪改查中)-->
<!--propagation="SUPPORTS" read-only="true"指如果有就執行該事物,如果沒有,就不會開啟事物(查詢中)-->
<tx:method name="add*" propagation="REQUIRED" rollback-for="Exception" />
<tx:method name="del*" propagation="REQUIRED" rollback-for="Exception"/>
<tx:method name="edit*" propagation="REQUIRED" rollback-for="Exception" />
<tx:method name="update*" propagation="REQUIRED" rollback-for="Exception"/>
<tx:method name="list*" propagation="REQUIRED" rollback-for="Exception"/>
</tx:attributes>
</tx:advice>
<!-配置AOP切面,事務配置-->
<aop:config>
<aop:pointcut id="serviceMethod" expression="execution(* com.how2java.service.*.*(..))"/>
<aop:advisor pointcut-ref="serviceMethod" advice-ref="txadvice"/>
</aop:config>
log4j.properties
#定義LOG輸出級別
log4j.rootLogger=INFO,Console,File#定義日誌輸出目的地為控制檯
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.Target=System.out#可以靈活地指定日誌輸出格式,下面一行是指定具體的格式
log4j.appender.Console.layout = org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=[%c] - %m%n
#檔案大小到達指定尺寸的時候產生一個新的檔案
log4j.appender.File = org.apache.log4j.RollingFileAppender#指定輸出目錄
log4j.appender.File.File = logs/ssm.log#定義檔案最大大小
log4j.appender.File.MaxFileSize = 10MB# 輸出所以日誌,如果換成DEBUG表示輸出DEBUG以上級別日誌
log4j.appender.File.Threshold = ALL
log4j.appender.File.layout = org.apache.log4j.PatternLayout
log4j.appender.File.layout.ConversionPattern =[%p] [%d{yyyy-MM-dd HH\:mm\:ss}][%c]%m%n
日誌切面類LogAspect
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.sf.json.JSONObject;
import org.apache.log4j.Logger;
import org.apache.struts2.ServletActionContext;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import com.opensymphony.xwork2.ActionContext;
public class LogAspect{
private final Logger logger = Logger.getLogger(LogInterceptor.class);//log4j
/**
* 前置方法,在目標方法執行前執行
*/
public void before(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();//執行的方法名
String entity = joinPoint.getTarget().getClass().getName();//執行的類
logger.info("start! "+entity+"."+methodName);
}
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
String methodName = proceedingJoinPoint.getSignature().getName();
String entity = proceedingJoinPoint.getTarget().getClass().getName();
JSONObject result =new JSONObject();
try {
result = (JSONObject) proceedingJoinPoint.proceed();//test方法的返回值
} catch (Exception ex) {
//test方法若有異常,則進行處理寫入日誌
result.put("success", false);
result.put("desc", "exception");
HttpServletRequest request = (HttpServletRequest) ActionContext.getContext().get(ServletActionContext.HTTP_REQUEST);
//獲取請求的URL
StringBuffer requestURL = request.getRequestURL();
//獲取參 數資訊
String queryString = request.getQueryString();
//封裝完整請求URL帶引數
if(queryString != null){
requestURL .append("?").append(queryString);
}
String errorMsg = "";
StackTraceElement[] trace = ex.getStackTrace();
for (StackTraceElement s : trace) {
errorMsg += "\tat " + s + "\r\n";
}
StringBuffer sb=new StringBuffer();
sb.append("exception!!!\r\n");
sb.append(" 請求URL:"+requestURL+"\r\n");
sb.append(" 介面方法:"+entity+"."+methodName+"\r\n");
sb.append(" 詳細錯誤資訊:"+ex+"\r\n");
sb.append(errorMsg+"\r\n");
logger.error(sb.toString());
}
if(result!=null && !result.isEmpty()){
HttpServletResponse response = (HttpServletResponse) ActionContext.getContext().get(ServletActionContext.HTTP_RESPONSE);
response.getWriter().print(result.toString());
}
return null;
}
}
spring mvc配置檔案
<bean id="logAspect" class="yan.joanna.log.LogAspect"></bean>
<aop:config>
<aop:aspect id="aspect" ref="logAspect">
<!--對哪些方法進行日誌記錄,此處遮蔽action內的set get方法 -->
<aop:pointcut id="logService" expression="(execution(* yan.joanna.*.*.*(..)) ) and (!execution(* yan.joanna.action.*.set*(..)) ) and (!execution(* yan.joanna.action.*.get*(..)) )" />
<aop:before pointcut-ref="logService" method="before"/>
<aop:after pointcut-ref="logService" method="after"/>
<aop:around pointcut-ref="logService" method="around"/>
</aop:aspect>
</aop:config>
mongodb在專案中的應用/前臺的日誌管理模組【15k及其以上必說】
前臺的日誌模組的核心價值是為了統計使用者的行為,方便進行使用者行為分析。考慮到對日誌的統一處理以及前臺的訪問量巨大導致的大併發和大資料量的問題。當時是結合AOP和MongoDB來完成這項功能。
面向切面程式設計aop,把功能劃分為核心業務功能和切面功能,比如日誌、事務、效能統計等,核心業務功能和切面功能分別獨立開發,通過aop可以根據需求將核心業務功能和切面功能結合在一起,比如增加操作可以和事務切面結合在一起,查詢操作可以和效能統計切面結合在一起。
Mongodb是nosql的非關係型資料庫,它的儲存資料可以超過上億條,mongodb適合儲存 一些量大表關係較簡單的資料,易於擴充套件,可以進行分散式檔案儲存,適用於大資料量、高併發、弱事務的網際網路應用。
在專案中我們通常結合aop來使用mongodb儲存操作日誌。
- aop和mongodb整合
- 首先得有個使用者實體類,包含使用者的瀏覽器型別(IE/谷歌/火狐)、使用者的裝置型別(手機/平板/pc)、使用者瀏覽過的課程資訊、使用者購買過的課程資訊等。
- 接著寫一個操作mongodb增刪改查的介面,以及實現該介面的實體類,將實體類注入到service層呼叫。
- 然後寫一個日誌切面類,獲取使用者資訊,將這些資訊通過service插入到mongodb資料庫。
- 最後在spring配置檔案spring-common.xml中引入mongodb標籤,配置mongodb客戶端mongo-client,以及mongodb的bean物件MongoTemp。還配置了切面aop:config,通過aop:pointcut對所有的controller以及裡面的方法進行攔截,aop:around配置環繞通知,裡面的method指定切面類的方法。
- 考慮到mongodb的高可用性,我們還搭建了3臺mongodb資料庫來實現副本集以及讀寫分離,這樣不僅可以達到故障自動轉移,而且提高效能,即便主伺服器宕機了,還能投票選舉出下一個主伺服器繼續提供服務。
*6.補充:如果問到副本集是怎麼搭建的,就說我們有專門的運維人員來負責搭建,我只負責用Java程式去進行操作
spring配置檔案
這個必須要的引入mongodb標籤
xmlns:mongo="http://www.springframework.org/schema/data/mongo"
在配置檔案中加入連結mongodb客服端
<mongo:mongo host="localhost" port="27017">
</mongo:mongo>
注入mogondb的bean物件
<bean id="mongoTemplate" class="org.springframework.data.document.mongodb.MongoTemp">
<constructor-arg ref="mongo"/>
<constructor-arg name="databaseName" value="db"/>
<constructor-arg name="defaultCollectionName" value="person" />
</bean>
<bean id="personRepository" class="com.mongo.repository.PersonRepository">
<property name="mongoTemplate" ref="mongoTemplate"></property>
</bean>
操作mongodb增刪查改的介面
public interface AbstractRepository {
public void insert(Person person);
public Person findOne(String id);
public List<Person> findAll();
public List<Person> findByRegex(String regex);
public void removeOne(String id);
public void removeAll();
public void findAndModify(String id);
}
對應介面的實現類
import java.util.List;
import java.util.regex.Pattern;
import org.springframework.data.document.mongodb.MongoTemplate;
import org.springframework.data.document.mongodb.query.Criteria;
import org.springframework.data.document.mongodb.query.Query;
import org.springframework.data.document.mongodb.query.Update;
import com.mongo.entity.Person;
import com.mongo.intf.AbstractRepository;
public class PersonRepository implements AbstractRepository{
private MongoTemplate mongoTemplate;
@Override
public List<Person> findAll() {
return getMongoTemplate().find(new Query(), Person.class);
}
@Override
public void findAndModify(String id) {
getMongoTemplate().updateFirst(new Query(Criteria.where("id").is(id)), new Update().inc("age", 3));
}
@Override
public List<Person> findByRegex(String regex) {
Pattern pattern = Pattern.compile(regex,Pattern.CASE_INSENSITIVE);
Criteria criteria = new Criteria("name").regex(pattern.toString()); return getMongoTemplate().find(new Query(criteria), Person.class); }
@Override
public Person findOne(String id) {
return getMongoTemplate().findOne(new Query(Criteria.where("id").is(id)), Person.class);
}
@Override
public void insert(Person person) {
getMongoTemplate().insert(person);
}
@Override
public void removeAll() {
List<Person> list = this.findAll();
if(list != null){
for(Person person : list){
getMongoTemplate().remove(person);
}
}
}
@Override
public void removeOne(String id){
Criteria criteria = Criteria.where("id").in(id);
if(criteria == null){
Query query = new Query(criteria);
if(query != null && getMongoTemplate().findOne(query, Person.class) != null)
getMongoTemplate().remove(getMongoTemplate().findOne(query, Person.class)); }
}
public MongoTemplate getMongoTemplate() {
return mongoTemplate;
}
public void setMongoTemplate(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
}