项目进阶,构建安全高效的企业服务
Spring SecuritySpring Security底层利用filter(许多专门登录、权限、退出。。。),利用javaee的规范,对整个请求进行拦截。对权限的控制比较靠前,权限不行的话到不了DispatcherServlet,更到不了controller
导包
导过包后会自动对项目进行安全管理,自带登陆页面,控制台有密码,用户名为user
怎么把它的登陆页面换成自己的?用自己数据库里的数据进行登录?
认证授权在业务层进行处理,当前用户的权限怎么体现?可以建立角色表,user的type字段(0普通,1管理员,2版主),用Security做授权时需要一个明确权限的字符串?
1、让user实体类实现UserDetails接口,实现里面的方法。
public class User implements UserDetails {
//true:账号未过期
@Override
public boolean isAccountNonExpired() {
return true;
}
//true:账号未锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
//true:凭证未过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//true:账号可用
@Override
public boolean isEnabled() {
return true;
}
//返回用户所具有的权限
@Override
public Collection extends GrantedAuthority> getAuthorities() {
List list=new ArrayList<>();
list.add(new GrantedAuthority() {
@Override
public String getAuthority() {//每个这个方法封装一个权限(多个封装多个权限)
switch (type){
case 1:
return "ADMIN";
default:
return "USER";
}
}
});
return list;
}
2、让userservice实现UserDetailsService接口
根据用户名查用户,自动判断账号密码对不对
public class UserService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return this.findUserByName(s);
}
}
3、利用security对项目进行授权
在配置类里面进行配置,继承WebSecurityConfigurerAdapter,重写父类方法
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Override
public void configure(WebSecurity web) throws Exception {
//忽略resource下的所有静态资源
web.ignoring().antMatchers("/resources
//1、将指定IP计入UV
public void recordUV(String ip){
//先得到key
String redisKey = RedisKeyUtil.getUVKey(df.format(new Date()));
redisTemplate.opsForHyperLogLog().add(redisKey,ip);//存进redis
}
//统计指定日期范围内的UV
public long calculateUV(Date start,Date end){
//先判断日期是否为空
if(start==null || end==null){
throw new IllegalArgumentException("参数不能为空!");
}
//将该范围内每一天的key合并整理到一个集合里
List keyList=new ArrayList<>();
//利用calendar对日期做运算
Calendar calendar=Calendar.getInstance();//实例化抽象类对象
calendar.setTime(start);
//时间<=end才循环
while (!calendar.getTime().after(end)){
//得到key
String key = RedisKeyUtil.getUVKey(df.format(calendar.getTime()));
//将key加到集合里
keyList.add(key);
//calendar加一天
calendar.add(Calendar.DATE,1);
}
//合并这些数据,存放合并后的值
String redisKey = RedisKeyUtil.getUVKey(df.format(start), df.format(end));//得到合并后的key
redisTemplate.opsForHyperLogLog().union(redisKey,keyList.toArray());//合并存到redis
//返回统计的结果
return redisTemplate.opsForHyperLogLog().size(redisKey);
}
//统计单日的dau
public void recordDAU(int userId){
//得到key
String rediskey = RedisKeyUtil.getDAUKey(df.format(new Date()));
//存入redis
redisTemplate.opsForValue().setBit(rediskey,userId,true);
}
//统计某个区间的dau(在该区间内某一天登录了就算是活跃,所以要用or运算)
public long calculateDAU(Date start,Date end){
//先判断日期是否为空
if(start==null || end==null){
throw new IllegalArgumentException("参数不能为空!");
}
//将该范围内每一天的key合并整理到一个集合里
//bitmap运算需要数组,所以list集合里面存byte数组
List keyList=new ArrayList<>();
//利用calendar对日期做运算
Calendar calendar=Calendar.getInstance();//实例化抽象类对象
calendar.setTime(start);
//时间<=end才循环
while (!calendar.getTime().after(end)){
//得到key
String key = RedisKeyUtil.getDAUKey(df.format(calendar.getTime()));
//将key加到集合里
keyList.add(key.getBytes());
//calendar加一天
calendar.add(Calendar.DATE,1);
}
//得到合并的key
String redisKey = RedisKeyUtil.getDAUKey(df.format(start), df.format(end));
//将合并的or运算结果存入redis
return (long) redisTemplate.execute(new RedisCallback() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
connection.bitOp(RedisStringCommands.BitOperation.OR,
redisKey.getBytes(),keyList.toArray(new byte[0][0]));
return connection.bitCount(redisKey.getBytes());
}
});
}
}
表现层分为两步记录和查看,每次请求都要记录,所以要再拦截器里面实现。
@Component
public class DataInterceptor implements HandlerInterceptor {
@Autowired
private DataService dataService;
@Autowired
private HostHolder hostHolder;
//在请求之前拦截,只是记录数据,所以要返回true,让其继续向下执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//统计单日的UV
//通过request得到ip
String ip = request.getRemoteHost();
dataService.recordUV(ip);//调用service统计
//统计单日的DAU
//得到当前用户,并且判断登陆后才记录
User user = hostHolder.getUser();
if (user!=null){
dataService.recordDAU(user.getId());
}
return true;
}
}
注入拦截器,使其生效
registry.addInterceptor(dataInterceptor)
.excludePathPatterns("*.css","*.js","*.png","*.jpg","*.jpeg");
控制器
@Controller
public class DataController {
@Autowired
private DataService dataService;
//打开统计页面的方法,get,post请求都可处理
@RequestMapping(path = "/data",method ={RequestMethod.GET,RequestMethod.POST} )
public String getDatePage(){
return "/site/admin/data";
}
//页面上传的是日期的字符串,告诉服务器字符串的格式,他就可以帮你转化,
// 利用注解@DateTimeFormat
@RequestMapping(path = "/data/uv",method = RequestMethod.POST)
public String getUV(@DateTimeFormat(pattern = "yyyy-MM-dd") Date start,
@DateTimeFormat(pattern = "yyyy-MM-dd")Date end, Model model){
long calculateUV = dataService.calculateUV(start, end);
model.addAttribute("uvResult",calculateUV);//将统计结果存到model
//将表单的日期也存到model里面,跳转后便于页面显示
model.addAttribute("uvStartDate",start);
model.addAttribute("uvEndDate",end);
return "forward:/data";//转发(这个请求只能完成一部分,下面的部分交给这个请求去完成,即上面那个请求)
}
//统计DAU
@RequestMapping(path = "/data/dau",method = RequestMethod.POST)
public String getDAU(@DateTimeFormat(pattern = "yyyy-MM-dd") Date start,
@DateTimeFormat(pattern = "yyyy-MM-dd")Date end, Model model){
long dau = dataService.calculateDAU(start, end);
model.addAttribute("dauResult",dau);//将统计结果存到model
//将表单的日期也存到model里面,跳转后便于页面显示
model.addAttribute("dauStartDate",start);
model.addAttribute("dauEndDate",end);
return "forward:/data";//转发(这个请求只能完成一部分,下面的部分交给这个请求去完成,即上面那个请求)
}
}
添加这个功能只能管理员访问
.antMatchers(
"/discuss/delete",
"/data/**"
)
.hasAnyAuthority(
AUTHORITY_ADMIN
)
entity是实体,可以对帖子进行评论也可以对帖子的评论进行评论。(1代表帖子,2代表评论,3代表用户)
entityid,某个类型的具体目标。
targetid,对某个评论进行回复(具有指向性)
status,0表示正常,1表示删除禁用
有些任务并不是我们访问服务器才启动的,例如:每隔一个小时计算帖子的分数,每隔半个小时清理服务器上存的文件。
在分布式下为什么使用jdk线程池和Spring线程池会出现问题?需要使用分布式定时任务?
分布式(多个服务器,一个集群),浏览器发给Nginx请求,根据策略Nginx发给服务器,两个服务器代码都一样,对于普通代码没问题,但是对于定时任务,两个同时执行就可能出现问题。
对于QuartZ怎么解决问题呢?
jdk、spring定时任务组件是基于内存的,配置参数在内存里,即服务器1和服务器2没法进行数据共享。QuarZ的定时任务参数保存在数据库里,即便两个服务器同时执行定时任务,会通过数据库加锁的方式抢锁,只有一个线程可以访问,下个线程去访问时,先看一下任务参数是否被修改,修改了,那他就不做了。
使用spring带有的线程池时,首先需要先配置下
#spring普通线程池配置 spring.task.execution.pool.core-size=5 spring.task.execution.pool.max-size=15 spring.task.execution.pool.queue-capacity=100 #spring定时任务的线程池配置 spring.task.scheduling.pool.size=5
另外还需要一个配置类,加上相关注解,配置类、能定时执行、能异步调用
该方法被调用后多长时间被执行,每隔多长时间执行。
只要有程序在跑,他就会被执行。
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class ThreadPoolTests {
private static final Logger logger = LoggerFactory.getLogger(ThreadPoolTests.class);
// JDK普通线程池
private ExecutorService executorService = Executors.newFixedThreadPool(5);
// JDK可执行定时任务的线程池
private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
// Spring普通线程池
@Autowired
private ThreadPoolTaskExecutor taskExecutor;
// Spring可执行定时任务的线程池
@Autowired
private ThreadPoolTaskScheduler taskScheduler;
@Autowired
private AlphaService alphaService;
private void sleep(long m) {
try {
Thread.sleep(m);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 1.JDK普通线程池
@Test
public void testExecutorService() {
Runnable task = new Runnable() {
@Override
public void run() {
logger.debug("Hello ExecutorService");
}
};
for (int i = 0; i < 10; i++) {
executorService.submit(task);
}
sleep(10000);
}
// 2.JDK定时任务线程池
@Test
public void testScheduledExecutorService() {
Runnable task = new Runnable() {
@Override
public void run() {
logger.debug("Hello ScheduledExecutorService");
}
};
scheduledExecutorService.scheduleAtFixedRate(task, 10000, 1000, TimeUnit.MILLISECONDS);
sleep(30000);
}
// 3.Spring普通线程池
@Test
public void testThreadPoolTaskExecutor() {
Runnable task = new Runnable() {
@Override
public void run() {
logger.debug("Hello ThreadPoolTaskExecutor");
}
};
for (int i = 0; i < 10; i++) {
taskExecutor.submit(task);
}
sleep(10000);
}
// 4.Spring定时任务线程池
@Test
public void testThreadPoolTaskScheduler() {
Runnable task = new Runnable() {
@Override
public void run() {
logger.debug("Hello ThreadPoolTaskScheduler");
}
};
Date startTime = new Date(System.currentTimeMillis() + 10000);
taskScheduler.scheduleAtFixedRate(task, startTime, 1000);
sleep(30000);
}
// 5.Spring普通线程池(简化)
@Test
public void testThreadPoolTaskExecutorSimple() {
for (int i = 0; i < 10; i++) {
alphaService.execute1();
}
sleep(10000);
}
// 6.Spring定时任务线程池(简化)
@Test
public void testThreadPoolTaskSchedulerSimple() {
sleep(30000);
}
}
使用QuartZ
先需要先初始化表
2、导包
org.springframework.boot spring-boot-starter-quartz
3、
# QuartzProperties 将配置放到数据库里 spring.quartz.job-store-type=jdbc #调度器的名字 spring.quartz.scheduler-name=communityScheduler #调度器id自动生成 spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate #是否采用集群 spring.quartz.properties.org.quartz.jobStore.isClustered=true spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool spring.quartz.properties.org.quartz.threadPool.threadCount=5
首先定义一个任务(Job接口execute声明我要做什么),具体做什么需要配置(JobDetail名字,组对job进行配置)(Trigger触发器,Job什么时候运行,以什么频率运行),将读取到的配置初始化到数据库,以后直接访问数据库读取配置。
BeanFactory是容器的顶层接口,
FactoryBean可简化Bean的实例化过程:
1、通过FactoryBean封装Bean的实例化过程。
2、将FactoryBean装配到Spring容器里。
3、将FactoryBean注入给其他的Bean。
4、该Bean得到的是FactoryBean所管理的对象实例
例如:
//只有第一次有用,将配置读取到数据库中,以后直接从数据库中读
@Configuration
public class QuartzConfig {
@Bean //将JobDetailFactoryBean装配到容器里
public JobDetailFactoryBean alphaJobDetail(){
return null;
}
@Bean //我这里的参数需要JobDetail,我将上面bean的方法名传进来,这里得到的是JobDetailFactoryBean管理的对象
public SimpleTriggerFactoryBean alphaTrigger(JobDetail alphaJobDetail){
return null;
}
}
使用用例
1、
public class AlphaJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println(Thread.currentThread().getName()+":execute a quartz job.");
}
}
2、
//只有第一次有用,将配置读取到数据库中,以后直接从数据库中读
@Configuration
public class QuartzConfig {
//配置JobDetail
@Bean //将JobDetailFactoryBean装配到容器里
public JobDetailFactoryBean alphaJobDetail(){
JobDetailFactoryBean factoryBean=new JobDetailFactoryBean();
factoryBean.setJobClass(AlphaJob.class);
factoryBean.setName("alphajob");
factoryBean.setGroup("alphaJobGroup");
factoryBean.setDurability(true);//任务不在运行,触发器没有了也不用删,留着
factoryBean.setRequestsRecovery(true);//任务是不是可恢复的
return factoryBean;
}
//配置触发器
@Bean //我这里的参数需要JobDetail,我将上面bean的方法名传进来,这里得到的是JobDetailFactoryBean管理的对象
public SimpleTriggerFactoryBean alphaTrigger(JobDetail alphaJobDetail){
SimpleTriggerFactoryBean factoryBean=new SimpleTriggerFactoryBean();
factoryBean.setJobDetail(alphaJobDetail);//参数名与bean名一致
factoryBean.setName("alphaTrigger");
factoryBean.setGroup("alphsTriggerGroup");
factoryBean.setRepeatInterval(3000);//执行频率
factoryBean.setJobDataMap(new JobDataMap());//指定那个对象来存状态
return factoryBean;
}
}
启动后,配置就会传到数据库里面,每三秒一次
删除任务
@Test
public void testDeleteJob(){
boolean b = false;
try {
b = scheduler.deleteJob(new JobKey("alphajob", "alphaJobGroup"));
System.out.println(b);
} catch (SchedulerException e) {
e.printStackTrace();
}
}
热帖排行
评论、点赞、加精之后立马计算,效率比较低,启动定时任务去计算,再根据分数排列显示。
思路:
评论、点赞、加精之后不立马计算,将其丢到缓存redis里,定时计算变化的帖子,不变的不算
在新添加的帖子后面也计算分数,并将其存到redis里面
置顶直接放在最顶上,所以不用计算分数。
在加精处也计算将贴子放到redis里
评论:对帖子评论才将其帖子放到redis里
点赞:先判断是对帖子点赞才将贴子放到redis里面
写定时任务(帖子刷新)
1、Job(记录日志)
查帖子,将计算后的帖子同步到es搜索引擎中
声明常量(只需要初始化一次,所以在静态块里面初始化),方便计算
private static final Date epoch;
static {
try {
epoch=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2014-08-01 00:00:00");
} catch (ParseException e) {
throw new RuntimeException("初始化牛客纪元失败!",e);
}
}
public class PostScoreRefreshJob implements Job, CommunityConstant{
private static final Logger logger= LoggerFactory.getLogger(PostScoreRefreshJob.class);
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private DiscussPostService discussPostService;
@Autowired
private LikeService likeService;
@Autowired
private ElasticsearchService elasticsearchService;
private static final Date epoch;
static {
try {
epoch=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2014-08-01 00:00:00");
} catch (ParseException e) {
throw new RuntimeException("初始化牛客纪元失败!",e);
}
}
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
//从redis里面取值(先得到key),每一个key都要算一下,反复的操作,所以用BoundSetOperation
String redisKey = RedisKeyUtil.getPostScoreKey();
BoundSetOperations operations=redisTemplate.boundSetOps(redisKey);
//先判断一下缓存中有没有数据,没有变化就不做操作
if(operations.size()==0){
logger.info("[任务取消] 没有要刷新的帖子!");
return;
}
//使用日志记录时间中间过程
logger.info("[任务开始] 正在刷新帖子分数: "+operations.size());
while (operations.size()>0){//只要redis里面有数据就算
//集合中弹出一个值
this.refresh((Integer)operations.pop());
}
}
private void refresh(int postId) {
//先将贴子查出来
DiscussPost post = discussPostService.findDiscussDetail(postId);
//空值判断(帖子被人点赞,但是后来被管理删除)
if(post==null){
logger.error("帖子不存在: id= "+ postId);//日志记录错误提示
return;
}
//计算帖子分值(加精-1、评论数、点赞数)
boolean wonderful = post.getStatus() == 1;
int commentCount = post.getCommentCount();
long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, postId);
//先求权重
double w=(wonderful? 75 : 0) + commentCount*10 + likeCount * 2;
//分数=帖子权重+距离天数
//为了不得到负数,在权重和1之间取最大值。将时间得到的毫秒在换算为天
double score=Math.log10(Math.max(w,1)+
(post.getCreateTime().getTime()-epoch.getTime())/(1000 * 3600 * 24));
//更新帖子的分数
discussPostService.updateDiscussScore(postId, score);
//同步搜索对应帖子的数据(先重设帖子的分数,再保存到es)
post.setScore(score);
elasticsearchService.saveDiscussPost(post);
}
}
写好了定时任务还要去配置别忘了
//刷新帖子分数任务
@Bean //将JobDetailFactoryBean装配到容器里
public JobDetailFactoryBean postScoreRefreshJobDetail(){
JobDetailFactoryBean factoryBean=new JobDetailFactoryBean();
factoryBean.setJobClass(PostScoreRefreshJob.class);
factoryBean.setName("postScoreRefreshJob");
factoryBean.setGroup("communityJobGroup");
factoryBean.setDurability(true);//任务不在运行,触发器没有了也不用删,留着
factoryBean.setRequestsRecovery(true);//任务是不是可恢复的
return factoryBean;
}
@Bean
public SimpleTriggerFactoryBean postScoreRefreshTrigger(JobDetail postScoreRefreshJobDetail){
SimpleTriggerFactoryBean factoryBean=new SimpleTriggerFactoryBean();
factoryBean.setJobDetail(postScoreRefreshJobDetail);//参数名与bean名一致
factoryBean.setName("postScoreRefreshTrigger");
factoryBean.setGroup("communityJobGroup");
factoryBean.setRepeatInterval(1000 * 60 *5);//执行频率
factoryBean.setJobDataMap(new JobDataMap());//指定那个对象来存状态
return factoryBean;
}
五分钟更新一次
重构之前查找帖子的mapper
//查找帖子分页显示(userId是动态需要条件,0表示不拼接,其余拼接) ListselectDiscussPosts(int userId,int offset,int limit,int orderMode);
生成长图
命令:1、将模板的内容生成pdf,2、将网页生成图片
wkhtmltopdf https://www.nowcoder.com
C:nowcoder_communitydatawk-pdfs/1.pdf
wkhtmltoimage https://www.nowcoder.com C:nowcoder_communitydatawk-images/1.png
压缩图片75%
wkhtmltoimage --quality 75 https://www.nowcoder.com C:nowcoder_communitydatawk-images/2.png
java中使用生成长图
package com.nowcoder.community;
import java.io.IOException;
public class WKTests {
public static void main(String[] args) {
String cmd="C:/user/soft/wk/wkhtmltopdf/bin/wkhtmltoimage --quality 75 https://www.nowcoder.com C:/nowcoder_community/data/wk-images/3.png";
try {
Runtime.getRuntime().exec(cmd);
System.out.println("ok!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
操作系统执行命令和程序执行是并发、异步的。
配置一下路径和图片保存的文件路径
#wk wk.image.command=C:/user/soft/wk/wkhtmltopdf/bin/wkhtmltoimage wk.image.storage=C:/nowcoder_community/data/wk-images
路径是否存在,验证文件是否可以自动创建
先删除之前创建的文件夹,测试能否成功。
在服务启动时,创建一个目录。
服务器启动时,先去实例化配置类,自动调@PostConstruct,初始化一次
@Configuration
public class WKConfig {
private static final Logger logger= LoggerFactory.getLogger(WKConfig.class);
//注入路径
@Value("${wk.image.storage}")
private String wkImageStorage;
@PostConstruct
public void init(){
//创建wk图片目录
File file=new File(wkImageStorage);
if(!file.exists()){
file.mkdir();
logger.info("创建wk图片目录:" +wkImageStorage);
}
}
}
处理前端的请求(生成图片,生成一个请求允许你访问图片)
生成图片时间比较长,采用异步的方式,将事件丢给Kafka即可,不需要一直等着它去处理。。
注入域名,项目名,图片存放的位置
//消费分享事件
@KafkaListener(topics = TOPIC_SHARE)
public void handleShareMessage(ConsumerRecord record){
//发了一个空消息
if(record==null || record.value()==null){
logger.error("发送消息为空!");
return;
}
//将json消息转为对象,指定字符串对应的具体类型
Event event = JSONObject.parseObject(record.value().toString(), Event.class);
//转为对象之后再判断
if(event==null){
logger.error("消息格式错误!");
return;
}
//得到htmlUrl、文件名字、后缀
String htmlUrl = (String) event.getData().get("htmlUrl");
String fileName = (String) event.getData().get("fileName");
String suffix = (String) event.getData().get("suffix");
//拼命令
String cmd= wkImageCommand + " --quality 75 "
+htmlUrl+" "+wkImageStorage +"/" +fileName +suffix;
//执行命令,成功和失败都要记录日志
try {
Runtime.getRuntime().exec(cmd);
logger.info("生成长图成功:"+cmd);
} catch (IOException e) {
e.printStackTrace();
logger.error("生成长图失败:"+ e);
}
}
@Controller
public class ShareController implements CommunityConstant {
private static final Logger logger= LoggerFactory.getLogger(ShareController.class);
@Autowired
private EventProducer eventProducer;
@Value("${community.path.domain}")
private String domain;
@Value("${server.servlet.context-path}")
private String contextPath;
@Value("${wk.image.storage}")
private String wkImageStorage;
//分享的请求(异步的返回json,将要分享的功能路径传过来)
@RequestMapping(path = "/share",method = RequestMethod.GET)
@ResponseBody
public String share(String htmlUrl){
//随机生成图片的文件名
String fileName = CommunityUtil.generateUUID();
//异步生成长图 构建事件(主题:分享,携带参数存到map里,htmlUrl,文件名,后缀,)
Event event=new Event();
event.setTopic(TOPIC_SHARE)
.setData("htmlUrl",htmlUrl)
.setData("fileName",fileName)
.setData("suffix",".png");
//触发事件(处理异步事件别忘,消费事件)
eventProducer.fireEvent(event);
//给客户端返回访问图片的访问路径
//将路径存到map里
Map map=new HashMap<>();
map.put("shareUrl",domain+contextPath +"/share/image/"+fileName);
return CommunityUtil.getJSIONString(0,null,map);
}
//获得长图
//返回一个图片用response处理
@RequestMapping(path = "/share/image/{fileName}",method = RequestMethod.GET)
public void getShareImage(@PathVariable("fileName")String fileName, HttpServletResponse response){
//先判断参数空值
if(StringUtils.isBlank(fileName)){
throw new IllegalArgumentException("文件名不能为空!");
}
//声明输出的是什么(图片/格式)
response.setContentType("image/png");
//实例化文件,图片存放的路径
File file = new File(wkImageStorage + "/" + fileName + ".png");
// 图片是字节,所以获取输出字节流
try {
OutputStream os=response.getOutputStream();//输出,就是写入其他文件
FileInputStream fis = new FileInputStream(file);//进入,就是进去读取
//一边读取文件,一边向外输出(读取缓冲区,游标)
byte[] buffer=new byte[1024];
int b=0;
while ((b=fis.read(buffer))!=-1){
os.write(buffer,0,b);
}
} catch (IOException e) {
logger.error("获取长图失败: "+e.getMessage());
}
}
}
将文件上传至云服务器
1、导包
2、配置key、桶名和对应的url
3、注入在配置文件配置的信息,废弃头像上传的upload和xx
4、在setting里面写代码,成功的时候返回json字符串,code:0,只要不是返回这个就认为是失败。
新增方法,返回成功后,在user表的url更新下,异步
更新数据,要穿数据进去所以是post,异步的所以是@ResponseBody
//先判断一下参数空值
//拼接url(空间的url+文件名)
找到表单setting,将之前的注掉,
异步上传,获得id
点击提交触发表单提交事件,return false事件到此为此,不再向下,因为没有action,
不要把表单的内容转换为字符串。不让jquery去设置上传的类型,浏览器会自动配置。
异步更新头像路径,从表单里面取文件名
优化网站性能两级缓存,
一级缓存就是服务器的缓存,存在本地内存。本地缓存中没有就去访问DB,在更新到缓存中。
用户第一次访问落在了服务器1上,生成用户信息缓存(可能是是否登录的状态)。如果用户第二次访问服务器2,里面没有用户信息,就不会去访问DB,而直接认为你没有登陆,所以和用户相关的信息缓存到本地缓存不合适。
但是本地缓存可以放一些热门的帖子,第一次访问服务器1,没有就去数据库,同步到缓存。第二次请求如果落在了服务器2上,进行和服务器1一样的操作,以后的请求不管落在哪一个服务器上,就都有了缓存。
Redis缓存可以放用户相关联的数据。应用看redis里面没有数据,就去访问DB,并缓存到redis里面。下一次用户在访问服务器2,同样先去redis里面看有没有数据,有就直接返回。
Redis可以跨服务器。分布式缓存。本地缓存比redis缓存快。
使用两级缓存:
先去访问本地缓存、再去redis缓存、没有再去访问DB。之后再将数据更新到本地缓存和redis里面。我们要设置缓存的过期时间,提高了访问速度。
缓存基于时间和大小有一定的淘汰策略,
优化热门的帖子列表顺序缓存(数据变化的频率低,分值隔一段时间才更新一下,能保证一段时间不变),
本地缓存使用Caffeine,spring整合它是使用一个缓存管理器管理所有缓存(统一的淘汰、过期时间),每个缓存业务不同,缓存时间不同,不用spring去整合。
com.github.ben-manes.caffeine caffeine 2.7.0
设置参数,自定义参数,缓存帖子列表是,缓存能存多少帖子(15页),缓存过期的时间(3分钟),
主动淘汰(帖子数据发生变化,清掉缓存),自动淘汰(定时)缓存的是一页数据,如果因为一页中某一个帖子的变化将整页数据都淘汰不合适,所以不设置主动淘汰。
优化业务方法(service)DiscussPostService
初始化logger,注入刚才配置的参数
缓存帖子的总行数,调用比较多
使用LoadingCache,一个缓存帖子列表,一个缓存帖子总行数。先声明,在初始化。缓存按照key-value来存值。
缓存只需要在服务启动或者首次调service初始化就可以了。唯一调用一次。
只缓存热门帖子,只缓存首页,首页用户没登陆,userId为0,缓存一页数据,key就有offset和limit有关。
缓存的是帖子列表,当用户查询自己的帖子时传入userId,这个时候是不走缓存的。当userId为0,才走缓存。因为一定要有key,所以就将userId作为key吧,虽然一定为null。
对缓存进行初始化
最大页数,过期时间,让配置生效build(匿名实现),怎么查询数据库得到数据(缓存怎么来的)
@Value("${caffeine.posts.max-size}")
private int maxSize;
@Value("${caffeine.posts.expire-seconds}")
private int expireSeconds;
//帖子列表缓存
private LoadingCache> postListCache;
//帖子总数缓存
private LoadingCache postRowsCache;
public List findDiscussPosts(int userId,int offset,int limit,int orderMode){
//只缓存热门帖子,只缓存首页,首页用户没登陆,userId为0,缓存一页数据,key就有offset和limit有关。
if(userId==0 && orderMode==1){
return postListCache.get(offset+":"+limit);
}
logger.debug("load post list from DB.");
return discussPostMapper.selectDiscussPosts(userId,offset,limit,orderMode);
}
public int findDiscussPostRows(int userId){
//缓存的是帖子列表,当用户查询自己的帖子时传入userId,这个时候是不走缓存的。当userId为0,才走缓存。
if(userId==0){
return postRowsCache.get(userId);
}
logger.debug("load post list from DB.");
return discussPostMapper.selectDiscussPostRows(userId);
}
//初始化热门帖子、帖子总数缓存
@PostConstruct
public void init(){
//初始化帖子列表缓存
postListCache= Caffeine.newBuilder()
.maximumSize(maxSize)
.expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
.build(new CacheLoader>() {
@Nullable
@Override
public List load(@NonNull String key) throws Exception {
if(key==null || key.length()==0){
throw new IllegalArgumentException("参数错误!");
}
//解析数据
String[] params = key.split(":");
//判断解析数据(切割得到的是不是两个)
if(params==null || params.length!=2){
throw new IllegalArgumentException("参数错误!");
}
//有了参数,查数据(缓存)
int offset = Integer.valueOf(params[0]);
int limit = Integer.valueOf(params[1]);
logger.debug("load post list from DB.");
return discussPostMapper.selectDiscussPosts(0,offset,limit,1);
}
});
//初始化帖子总数缓存
postRowsCache=Caffeine.newBuilder()
.maximumSize(maxSize)
.expireAfterWrite(expireSeconds,TimeUnit.SECONDS)
.build(new CacheLoader() {
@Nullable
@Override
public Integer load(@NonNull Integer key) throws Exception {
logger.debug("load post list from DB.");
return discussPostMapper.selectDiscussPostRows(key);
}
});
}
先将其注掉,进行压力测试100个请求
优化后:大概是原来的1.5倍



