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;
}
}